Почему Python, декодируют замену больше, чем недопустимые байты от закодированной строки?

Попытка декодировать недопустимую закодированную utf-8 страницу HTML дает различные результаты в Python, Firefox и хроме.

Недопустимый закодированный фрагмент от тестовой страницы похож '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 unicode компонент. О Проблеме сообщают, чтобы быть зафиксированной в 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 сценарий:

#!/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),

Согласно Википедии 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 парсинг ошибок

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

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

>>> '\xf0'.decode('utf-8', 'replace')
u'\ufffd-  -->'
>>> print _
�-  -->

Пример 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 сценарий для рендеринга этого в браузерах:

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

\xf0it\xe2\x80\x99s"""

Представленный:

�it’s

Там какому-либо чиновнику рекомендуют путь к обработке замен декодирования?

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

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

  1. Замените всю плохо сформированную подпоследовательность единственным U+FFFD.
  2. Замените каждое максимальное подразделение плохо сформированной подпоследовательности единственным U+FFFD.
  3. Замените каждый элемент кода плохо сформированной подпоследовательности единственным U+FFFD.

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

Обзор Общественности UTC 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 три результата:

  1. u'a\ufffdb' шоу как a�b
  2. u'a\ufffd\ufffd\ufffdb' шоу как a���b
  3. u'a\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdb' шоу как a������b

И вот то, что Python делает для недопустимого потока байтов в качестве примера:

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

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

#!/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 recomendation

В то время как опция 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

27
задан 19 revs 14 July 2016 в 09:08
поделиться

4 ответа

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

Обновление

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

Фактически, Python не использует НИКАКОЙ из 3 опций UTC.

Вот единственный пример 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 как последовательности символов Юникода. Если преобразователь обнаруживает некорректную последовательность кодовых единиц UTF-8, которая начинается с действительного первого байта, но не продолжается с действительными байтами-преемниками (см. Таблицу 3-7), , он не должен использовать последующие байты как часть некорректно сформированной подпоследовательности всякий раз, когда эти последующие байты сами составляют часть правильно сформированной подпоследовательности модуля кода UTF-8 .

Если реализация процесса преобразования UTF-8 останавливается на первой обнаруженной ошибке, без сообщения о конце какой-либо некорректной подпоследовательности кодовых единиц UTF-8, то требование делает небольшая практическая разница. Однако это требование вводит существенное ограничение , если преобразователь UTF-8 продолжает работу после точки обнаруженной ошибки , возможно, путем замены одного или нескольких заменяющих символов U + FFFD на неинтерпретируемый, { {1}} некорректная подпоследовательность кодовых единиц UTF-8. Например, с входным кодом UTF-8 последовательность единиц , такой процесс преобразования UTF-8 не должен возвращать или , потому что любой из этих выходов будет результатом неправильной интерпретации правильно сформированной подпоследовательности как части неправильно сформированной подпоследовательности. . Вместо этого ожидаемое возвращаемое значение для такого процесса будет .

Для процесса преобразования 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 ') .

Сообщается здесь

9
ответ дан 28 November 2019 в 05:50
поделиться

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

4
ответ дан 28 November 2019 в 05:50
поделиться

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

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

0
ответ дан 28 November 2019 в 05:50
поделиться

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

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

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

Новый (окончательный?) Edit: рекомендуемая Рекомендация консорциума UniCode для замены символов (PR121)
(Кстати, поздравляю dangra продолжать копать и копать и, следовательно, сделать вопрос лучше)
И дангра, и я были частично неправы, по-своему, в отношении толкования этой рекомендации; Мое последнее мнение заключается в том, что на самом деле рекомендация также говорит о попытке и «повторной синхронизации».
Ключевой концепцией является максимальная подчасти [плохо сформированной последовательности].
С учетом (одиночного) примера, представленного в документе PR121, «максимальная подчасти» подразумевает не чтение байтов, которые не могут быть частью последовательности. Например, 5-й байт в последовательности, 0xE1 НЕ может быть «вторым, третьим или четвертым байтом последовательности», поскольку он не находится в диапазоне x80-xBF, и, следовательно, это завершает плохо сформированную последовательность, которая началась с xF1.Затем нужно попытаться начать новую последовательность с xE1 и т. Д. Точно так же при нажатии на x62, который тоже не может быть интерпретирован как второй/третий/четвертый байт, плохая последовательность заканчивается, и «b» (x62) «сохраняется»...

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

Также см. ответ John Machin в этом посте для более конкретных цитат из базового стандарта / рекомендаций Unicode.

8
ответ дан 28 November 2019 в 05:50
поделиться
Другие вопросы по тегам:

Похожие вопросы: