Почему декодирование python заменяет больше, чем недопустимые байты из закодированной строки? - PullRequest
28 голосов
/ 30 марта 2010

Попытка декодирования неверно закодированной HTML-страницы utf-8 дает разные результаты в 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

ОБНОВЛЕНИЕ : Этот вопрос заключен в сообщении об ошибке для компонента Unicode 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:

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

>>> '<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 для рендеринга в браузерах:

#!/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 была в 2008-08-29, источник: 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 делает для некорректного примера bytestream:

>>> '\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

Хотя опция 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

Ответы [ 4 ]

10 голосов
/ 31 марта 2010

Вы знаете, что ваш 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 недопустим. Одна плохая последовательность, одна замена.
Начните снова со следующего байта, 3-го 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').

Отмечено здесь

8 голосов
/ 30 марта 2010

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

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

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

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

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

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

5 голосов
/ 30 марта 2010

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

0 голосов
/ 30 марта 2010

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

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

...