Por que a decodificação python está substituindo mais do que os bytes inválidos de uma sequência codificada?

Tentar decodificar uma página utf-8 html codificada inválida fornece resultados diferentes em python, firefox e chrome.

O fragmento codificado inválido da página de teste se parece com'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

ATUALIZAR: Esta questão foi concluída em umrelatório de erro ao componente unicode do Python. O problema foi relatado como corrigido no Python 2.7.11 e 3.5.2.

A seguir, são apresentadas as políticas de substituição usadas para lidar com erros de decodificação no Python, Firefox e Chrome. Observe como eles diferem e, especialmente, como o python builtin remove o válidoS (mais a sequência inválida de bytes).

Pitão

O builtinreplace O manipulador de erros substitui o inválido\xe3\xab mais oS deSUFFIX por U + FFFD

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

Para testar como os navegadores decodificam a sequência inválida de bytes, usará um script cgi:

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

PREFIX\xe3\xabSUFFIX"""

Navegadores Firefox e Chrome renderizados:

PREFIX�SUFFIX
Por que builtinreplace manipulador de erros parastr.decode está removendo oS deSUFFIX

(ATUALIZAÇÃO 1)

De acordo com a wikipediaUTF-8 (obrigado mjv), os seguintes intervalos de bytes são usados para indicar o início de uma sequência de bytes

0xC2-0xDF: início da sequência de 2 bytes0xE0-0xEF: início da sequência de 3 bytes0xF0-0xF4: início da sequência de 4 bytes

'PREFIX\xe3\abSUFFIX' fragmento de teste tem0xE3, instrui o decodificador python que segue uma sequência de 3 bytes, a sequência é considerada inválida e o decodificador python ignora toda a sequência, incluindo'\xabS'e continua após ignorar qualquer sequência correta possível iniciando no meio.

Isso significa que, para uma sequência codificada inválida como'\xF0SUFFIX', decodificaráu'\ufffdFIX' ao invés deu'\ufffdSUFFIX'.

Exemplo 1: Apresentando erros de análise do 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>

Exemplo 2: Problemas de segurança (veja tambémConsiderações de segurança Unicode):

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

Exemplo 3: Remover informações válidas para um aplicativo de raspagem

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

Usando um script cgi para renderizar isso nos navegadores:

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

\xf0it\xe2\x80\x99s"""

Processado:

�it’s
Existe alguma maneira oficial recomendada para lidar com substituições de decodificação?

(ATUALIZAÇÃO 2)

Em umrevisão pública, o Comitê Técnico Unicode optou pela opção 2 dos seguintes candidatos:

Substitua toda a subsequência mal formada por um único U + FFFD.Substitua cada subparte máxima da subsequência mal formada por um único U + FFFD.Substitua cada unidade de código da subsequência mal formada por um único U + FFFD.

A resolução UTC estava em 29/08/2008, fonte:http://www.unicode.org/review/resolved-pri-100.html

UTC Revisão Pública 121 também inclui um bytestream inválido como exemplo'\x61\xF1\x80\x80\xE1\x80\xC2\x62', mostra os resultados da decodificação para cada opção.

            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

No Python simples, os três resultados são:

u'a\ufffdb' mostra comoa�bu'a\ufffd\ufffd\ufffdb' mostra comoa���bu'a\ufffd\ufffd\ufffd\ufffd\ufffd\ufffdb' mostra comoa������b

E aqui está o que o python faz para o exemplo inválido bytestream:

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

Novamente, usando um script cgi para testar como os navegadores processam os bytes codificados com bugs:

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

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

Tanto o Chrome quanto o Firefox renderizaram:

a���b

Observe que o resultado renderizado pelos navegadores corresponde à opção 2 da recomendação PR121

Enquantoopção 3 parece facilmente implementável em python,opção 2 e 1 são um desafio.

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

questionAnswers(4)

yourAnswerToTheQuestion