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).
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
NavegadoresPara 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�b
u'a\ufffd\ufffd\ufffdb'
mostra comoa���b
u'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