Попытка декодирования неверно закодированной 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
из следующих кандидатов:
- Заменить всю плохо сформированную подпоследовательность одним U + FFFD.
- Заменить каждую максимальную подпоследовательность плохо сформированной подпоследовательности одним U + FFFD.
- Заменить каждую кодовую единицу неправильно сформированной подпоследовательности одним 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 три результата:
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, чтобы проверить, как браузеры отображают ошибочно закодированные байты:
#!/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