Попытка декодировать недопустимую закодированную 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
(плюс недопустимая последовательность байтов).
Встроенное 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), следующие диапазоны байтов используются для указания на запуск последовательности байтов
'PREFIX\xe3\abSUFFIX'
тестовый фрагмент имеет 0xE3, он сообщает декодеру Python, за которым следует 3-байтовая последовательность, последовательность найдена недопустимой, и декодер Python игнорирует целую последовательность включая '\xabS'
, и продолжается после него игнорирующий любую возможную корректную последовательность, запускающуюся в середине.
Это означает это для недопустимой закодированной последовательности как '\xF0SUFFIX'
, это будет декодировать u'\ufffdFIX'
вместо u'\ufffdSUFFIX'
.
Пример 1: Представление DOM парсинг ошибок
>>> '\xf0Price: $20...'.decode('utf-8', 'replace')
u'\ufffdv>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 следующих кандидатов:
Разрешение 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 три результата:
u'a\ufffdb'
шоу как a�b
u'a\ufffd\ufffd\ufffdb'
шоу как a���b
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
Вы знаете, что ваш 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 ')
.
Сообщается здесь
В 'PREFIX \ xe3 \ xabSUFFIX'
\ xe3
указывает, что это а следующие два байта образуют одну кодовую точку Юникода. ( \ xEy
подходит для всех y.) Однако \ xe3 \ xabS
явно не относится к действительной кодовой точке. Поскольку Python знает, что предполагается, что занимает три байта, он все равно поглощает все три, поскольку не знает, что ваш S является S, а не просто каким-то байтом, представляющим 0x53 по какой-то другой причине.
Кроме того, существует ли какой-либо официальный рекомендуемый способ Unicode для обработки замен декодирования?
Нет. Unicode считает их ошибочным и не рассматривает никаких альтернативных вариантов. Таким образом, ни одно из приведенных выше действий не является «правильным».
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.