Подтвердить что ты не робот

Почему декодер python заменяет более чем недопустимые байты из кодированной строки?

Попытка декодирования неверной кодированной страницы utf-8 html дает разные результаты в python, firefox и chrome.

Недопустимый кодированный фрагмент с тестовой страницы выглядит как 'PREFIX\xe3\xabSUFFIX'

>>> fragment = 'PREFIX\xe3\xabSUFFIX'
>>> fragment.decode('utf-8', 'strict')
...
UnicodeDecodeError: 'utf8' codec can't decode bytes in position 6-8: invalid data

ОБНОВЛЕНИЕ. Этот вопрос заключен в отчет об ошибках в компонент юникода Python. Сообщается, что проблема исправлена ​​в Python 2.7.11 и 3.5.2.


Ниже приведены политики замены, используемые для обработки ошибок декодирования в Python, Firefox и Chrome. Обратите внимание, как они отличаются и особенно Встроенный python удаляет допустимый S (плюс недопустимую последовательность байтов).

Python

Встроенный обработчик ошибок replace заменяет неверный \xe3\xab плюс S из SUFFIX через U + FFFD

>>> fragment.decode('utf-8', 'replace')
u'PREFIX\ufffdUFFIX'
>>> print _
PREFIX�UFFIX

Браузеры

Чтобы проверить, как браузеры декодируют неверную последовательность байтов, будет использоваться cgi script:

#!/usr/bin/env python
print """\
Content-Type: text/plain; charset=utf-8

PREFIX\xe3\xabSUFFIX"""

Браузер Firefox и Chrome:

PREFIX�SUFFIX

Почему встроенный обработчик ошибок replace для str.decode удаляет S из SUFFIX

(было ОБНОВЛЕНИЕ 1)

Согласно wikipedia UTF-8 (спасибо mjv), следующие диапазоны байтов используются для указания начала последовательности байты

  • 0xC2-0xDF: начало 2-байтовой последовательности
  • 0xE0-0xEF: начало 3-байтовой последовательности
  • 0xF0-0xF4: начало 4-байтовой последовательности

'PREFIX\xe3\abSUFFIX' Тестовый фрагмент имеет 0xE3, он инструктирует декодер python что последует 3-байтная последовательность, последовательность найдена недействительной и python декодер игнорирует всю последовательность, включая '\xabS', и продолжает после нее игнорируя любую возможную правильную последовательность, начиная с середины.

Это означает, что для неверной кодированной последовательности, такой как '\xF0SUFFIX', она будет декодировать u'\ufffdFIX' вместо u'\ufffdSUFFIX'.

Пример 1: Представление ошибок анализа DOM

>>> '<div>\xf0<div>Price: $20</div>...</div>'.decode('utf-8', 'replace')
u'<div>\ufffdv>Price: $20</div>...</div>'
>>> print _
<div>�v>Price: $20</div>...</div>

Пример 2: Проблемы безопасности (также см. Unicode соображения безопасности):

>>> '\xf0<!-- <script>alert("hi!");</script> -->'.decode('utf-8', 'replace')
u'\ufffd- <script>alert("hi!");</script> -->'
>>> print _
�- <script>alert("hi!");</script> -->

Пример 3: удалить действительную информацию для приложения скремблирования

>>> '\xf0' + u'it\u2019s'.encode('utf-8') # "it’s"
'\xf0it\xe2\x80\x99s'
>>> _.decode('utf-8', 'replace')
u'\ufffd\ufffd\ufffds'
>>> print _
���s

Использование cgi script для рендеринга в браузерах:

#!/usr/bin/env python
print """\
Content-Type: text/plain; charset=utf-8

\xf0it\xe2\x80\x99s"""

Рендеринг:

�it’s

Есть ли какой-либо официальный рекомендуемый способ обработки замеров декодирования?

(было ОБНОВЛЕНИЕ 2)

В публичный отзыв, Технический комитет Юникода выбрал вариант 2 следующих кандидатов:

  • Замените всю некорректную подпоследовательность одним U + FFFD.
  • Замените каждую максимальную подгруппу некорректной подпоследовательности на один U + FFFD.
  • Замените каждый блок кода плохо сформированной подпоследовательности на один U + FFFD.

Разрешение UTC было в 2008-08-29, источник: http://www.unicode.org/review/resolved-pri-100.html

UTC Public Review 121 также включает недопустимый байтовый поток в качестве примера '\x61\xF1\x80\x80\xE1\x80\xC2\x62', он показывает результаты декодирования для каждого вариант.

            61      F1      80      80      E1      80      C2      62
      1   U+0061  U+FFFD                                          U+0062
      2   U+0061  U+FFFD                  U+FFFD          U+FFFD  U+0062
      3   U+0061  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+0062

В простом Python три результата:

  • u'a\ufffdb' отображается как a�b
  • u'a\ufffd\ufffd\ufffdb' отображается как a���b
  • u'a\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdb' отображается как a������b

И вот что делает python для недопустимого примера bytestream:

>>> '\x61\xF1\x80\x80\xE1\x80\xC2\x62'.decode('utf-8', 'replace')
u'a\ufffd\ufffd\ufffd'
>>> print _
a���

Опять же, используя cgi script, чтобы проверить, как браузеры отображают багги-закодированные байты:

#!/usr/bin/env python
print """\
Content-Type: text/plain; charset=utf-8

\x61\xF1\x80\x80\xE1\x80\xC2\x62"""

Оба браузера Chrome и Firefox:

a���b

Обратите внимание, что отображаемые в браузере результаты соответствуют опции 2 рекомендации PR121

Хотя вариант 3 выглядит легко реализуемым в python, опция 2 и 1 являются проблемой.

>>> replace_option3 = lambda exc: (u'\ufffd', exc.start+1)
>>> codecs.register_error('replace_option3', replace_option3)
>>> '\x61\xF1\x80\x80\xE1\x80\xC2\x62'.decode('utf-8', 'replace_option3')
u'a\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdb'
>>> print _
a������b
4b9b3361

Ответ 1

Вы знаете, что ваш S действителен, с учетом как перспективных, так и задним числом:-) Предположим, что изначально была законная 3-байтная последовательность UTF-8, а 3-й байт был поврежден в передаче... с изменением, которое вы упомянули, вы будете жаловаться, что ложный S не был заменен. Нет никакого "правильного" способа сделать это, без использования кодов с исправлением ошибок или хрустального шара, или тамборин.

Update

Как заметил @mjv, проблема UTC связана с количеством U + FFFD.

Фактически, Python не использует ЛЮБОЙ из параметров UTC 3.

Ниже приведен пример UTC:

      61      F1      80      80      E1      80      C2      62
1   U+0061  U+FFFD                                          U+0062
2   U+0061  U+FFFD                  U+FFFD          U+FFFD  U+0062
3   U+0061  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+FFFD  U+0062

Вот что делает Python:

>>> bad = '\x61\xf1\x80\x80\xe1\x80\xc2\x62cdef'
>>> bad.decode('utf8', 'replace')
u'a\ufffd\ufffd\ufffdcdef'
>>>

Почему?

F1 должен начать 4-байтовую последовательность, но E1 недействителен. Одна плохая последовательность, одна замена.
Начните снова в следующем байте, третий 80. Взрыв, еще один FFFD.
Начните снова с C2, который вводит 2-байтную последовательность, но C2 62 недействителен, поэтому снова ударяйте.

Интересно, что в UTC не упоминалось, что делает Python (перезапуск после количества байтов, обозначенных ведущим символом). Возможно, это фактически запрещено или устарело где-то в стандарте Unicode. Требуется больше чтения. Смотрите это место.

Обновить 2 Хьюстон, у нас есть проблема.

=== Цитата из Глава 3 Unicode 5.2 ===

Ограничения на процессы конверсии

Требование не интерпретировать любые некорректные подстроки блока кода в строке как символы (см. условие соответствия C10) имеет важные последствия для процессов преобразования.

Такие процессы могут, например, интерпретировать последовательности блоков кода UTF-8 как символы Unicode последовательности. Если преобразователь встречает неправильную последовательность кода кода UTF-8, которая начинается с действительного первого байта, но не продолжается с действительными байтами-преемниками (см. Таблица 3-7), , он не должен потреблять байты-преемники как часть плохо сформированной подпоследовательности всякий раз, когда сами байты-преемники составляют часть хорошо сформированного кода UTF-8 Единичная подпоследовательность.

Если реализация процесса преобразования UTF-8 останавливается при первой обнаруженной ошибке, не сообщая конца какой-либо плохо сформированной подпоследовательности кода кода UTF-8, тогда требование мало практических различий. Однако это требование вводит существенное ограничение, если преобразователь UTF-8 продолжает проходить через точку обнаруженной ошибки, возможно, путем замены одного или нескольких символов замены U + FFFD для неинтерпретируемых, некорректная подпоследовательность кода кода UTF-8. Например, с кодом ввода UTF-8 единичная последовательность <C2 41 42>, такой процесс преобразования UTF-8 не должен возвращать <U+FFFD> или <U+FFFD, U+0042>, потому что любой из этих выходов будет результатом неправильного толкования хорошо сформированной подпоследовательности как части плохо сформированной подпоследовательности. ожидаемое возвращаемое значение для такого процесса будет <U+FFFD, U+0041, U+0042>.

Для процесса преобразования UTF-8 для использования допустимых байтов-преемников не только несоответствие но также оставляет конвертер открытым для эксплойтов безопасности. См. Технический отчет Unicode #36, "Вопросы безопасности Unicode."

=== Конец цитаты ===

Далее мы продолжим подробно обсуждать с примерами вопрос о том, сколько проблем выдается FFFD.

Используя их пример во втором последнем цитированном абзаце:

>>> bad2 = "\xc2\x41\x42"
>>> bad2.decode('utf8', 'replace')
u'\ufffdB'
# FAIL

Обратите внимание, что это проблема как с параметрами 'replace' , так и 'ignore' для str.decode('utf_8') - это все об исключении данных, а не о том, сколько U + FFFD испускаются; получить часть данных, излучающую часть, и проблема U + FFFD выпадает естественным образом, как объясняется в той части, которую я не приводил.

Обновление 3. Текущие версии Python (включая 2.7) имеют unicodedata.unidata_version как '5.1.0', которые могут указывать или не указывать, что код, связанный с Unicode, должен соответствовать Unicode 5.1.0. В любом случае, слововой запрет на то, что делает Python, не отображается в стандарте Unicode до 5.2.0. Я буду поднимать вопрос на трекер Python без упоминания слова 'oht'.encode('rot13').

Сообщено здесь

Ответ 2

Байт 0xE3 является одним (из возможных) первых байтов, указывающим на 3-байтовый символ.

По-видимому, логика декодирования Python берет эти три байта и пытается их декодировать. Они оказываются не соответствующими фактической точке кода ( "символ" ), поэтому Python создает UnicodeDecodeError и испускает символ замены Похоже, однако, что при этом логика декодирования Python не придерживается рекомендации Unicode Consortium в отношении символов замещения для "плохо сформированных" последовательностей UTF-8.

См. статья UTF-8 в Википедии для получения дополнительной информации о кодировке UTF-8.

Новый (окончательный?) Редактировать: re Рекомендация UniCode Consortium для замены символов (PR121)
(BTW, поздравляю dangra, чтобы продолжать копать и копать и, следовательно, сделать вопрос лучше)
Как дангра, так и я были частично неверны, по-своему, относительно интерпретации этой рекомендации; мое последнее понимание заключается в том, что действительно рекомендация также говорит о попытке и "повторной синхронизации".
Ключевым понятием является понятие максимального подчасти [плохо сформированной последовательности].
Ввиду (одиночного) примера, представленного в документе PR121, "максимальная подчасти" подразумевает не чтение байтов, которые не могут быть частью последовательности. Например, 5-й байт в последовательности, 0xE1, возможно, не может быть "вторым, третьим или четвертым байтом последовательности", поскольку он не находится в диапазоне x80-xBF, и, следовательно, это завершает неудачную последовательность, которая началась с xF1. Затем нужно попробовать и начать новую последовательность с помощью xE1 и т.д. Аналогично, при попадании x62, который тоже не может быть интерпретирован как второй/третий/четвертый байт, неудачная последовательность завершается, а "b" (x62) спасены"...

В этом свете (и до исправления;-)) логика декодирования на Python оказывается ошибочной.

Также см. ответ Джона Мачина в этом сообщении для более конкретных котировок базового стандарта/рекомендаций Unicode.

Ответ 3

В 'PREFIX\xe3\xabSUFFIX' символ \xe3 указывает, что он и следующие два укуса образуют одну кодовую точку Юникода. (\xEy для всех y.) Однако \xe3\xabS, очевидно, не относится к допустимой кодовой точке. Поскольку Python знает, что он должен принимать три байта, он все равно всасывает все три, так как он не знает, что ваш S является S, а не только некоторым байтом, представляющим 0x53 по какой-то другой причине.

Ответ 4

Кроме того, существует ли какой-либо официальный стандарт в Unicode для обработки замеров декодирования?

Нет. Unicode рассматривает их как условие ошибки и не рассматривает какие-либо альтернативные варианты. Таким образом, ни одно из приведенных выше действий не является правильным.