UnicodeDecodeError ao redirecionar para o arquivo

100

Eu executo este snippet duas vezes, no terminal do Ubuntu (codificação definida para utf-8), uma vez com ./test.pye depois com ./test.py >out.txt:

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Sem redirecionamento, ele imprime lixo. Com o redirecionamento, recebo um UnicodeDecodeError. Alguém pode explicar por que recebo o erro apenas no segundo caso, ou melhor ainda, dar uma explicação detalhada do que está acontecendo por trás da cortina em ambos os casos?

Zedoo
fonte
Esta resposta também pode ajudar.
tzot
Quando tento replicar sua descoberta, obtenho um UnicodeEncodeError, não um UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Respostas:

252

A chave para esses problemas de codificação é entender que existem, em princípio, dois conceitos distintos de "string" : (1) string de caracteres e (2) string / array de bytes. Esta distinção foi ignorada por muito tempo devido à onipresença histórica de codificações com não mais que 256 caracteres (ASCII, Latin-1, Windows-1252, Mac OS Roman, ...): essas codificações mapeiam um conjunto de caracteres comuns para números entre 0 e 255 (ou seja, bytes); a troca relativamente limitada de arquivos antes do advento da web tornou tolerável essa situação de codificações incompatíveis, já que a maioria dos programas poderia ignorar o fato de que havia várias codificações, desde que produzissem texto que permanecesse no mesmo sistema operacional: tais programas simplesmente tratar o texto como bytes (por meio da codificação usada pelo sistema operacional). A visão moderna correta separa adequadamente esses dois conceitos de string, com base nos dois pontos a seguir:

  1. A maioria dos personagens não está relacionada a computadores : pode-se desenhá-los em um quadro-negro, etc., como por exemplo بايثون, 中 蟒 e 🐍. "Caracteres" para máquinas também incluem "instruções de desenho" como, por exemplo, espaços, retorno de carro, instruções para definir a direção da escrita (para árabe, etc.), acentos, etc. Uma lista muito grande de caracteres está incluída no padrão Unicode ; ele cobre a maioria dos personagens conhecidos.

  2. Por outro lado, os computadores precisam representar caracteres abstratos de alguma forma: para isso, eles usam matrizes de bytes (números entre 0 e 255 incluídos), porque sua memória vem em blocos de bytes. O processo necessário que converte caracteres em bytes é chamado de codificação . Assim, um computador requer uma codificação para representar os caracteres. Qualquer texto presente em seu computador é codificado (até que seja exibido), seja ele enviado para um terminal (que espera caracteres codificados de uma forma específica), ou salvo em um arquivo. Para serem exibidos ou "compreendidos" adequadamente (por exemplo, pelo interpretador Python), os fluxos de bytes são decodificados em caracteres. Algumas codificações(UTF-8, UTF-16, ...) são definidos por Unicode para sua lista de caracteres (Unicode, portanto, define uma lista de caracteres e codificações para esses caracteres - ainda há lugares onde se vê a expressão "codificação Unicode" como um forma de se referir ao ubíquo UTF-8, mas essa é uma terminologia incorreta, pois o Unicode fornece várias codificações).

Em resumo, os computadores precisam representar internamente os caracteres com bytes , e o fazem por meio de duas operações:

Codificação : caracteres → bytes

Decodificação : bytes → caracteres

Algumas codificações não podem codificar todos os caracteres (por exemplo, ASCII), enquanto (algumas) codificações Unicode permitem que você codifique todos os caracteres Unicode. A codificação também não é necessariamente exclusiva , porque alguns caracteres podem ser representados diretamente ou como uma combinação (por exemplo, de um caractere base e de acentos).

Observe que o conceito de nova linha adiciona uma camada de complicação , uma vez que pode ser representado por caracteres diferentes (de controle) que dependem do sistema operacional (essa é a razão para o modo de leitura de arquivo de nova linha universal do Python ).

Agora, o que chamei de "caractere" acima é o que o Unicode chama de " caractere percebido pelo usuário ". Um único caractere percebido pelo usuário às vezes pode ser representado em Unicode combinando partes de caracteres (caractere base, acentos, ...) encontrados em diferentes índices na lista Unicode, que são chamados de " pontos de código " - esses pontos de código podem ser combinados para formar um "aglomerado de grafemas". Unicode, portanto, leva a um terceiro conceito de string, feito de uma sequência de pontos de código Unicode, que fica entre as strings de byte e de caracteres, e que está mais próximo do último. Vou chamá-los de " strings Unicode " (como em Python 2).

Enquanto o Python pode imprimir strings de caracteres (percebidos pelo usuário), strings não-byte do Python são essencialmente sequências de pontos de código Unicode , não de caracteres percebidos pelo usuário. Os valores de ponto de código são aqueles usados ​​na sintaxe de string do Python \ue \UUnicode. Eles não devem ser confundidos com a codificação de um caractere (e não precisam ter nenhum relacionamento com ele: os pontos de código Unicode podem ser codificados de várias maneiras).

Isso tem uma consequência importante: o comprimento de uma string Python (Unicode) é seu número de pontos de código, que nem sempre é o número de caracteres percebidos pelo usuário : assim s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) dá 각 len 3apesar de ster um único caractere percebido pelo usuário (coreano) caractere (porque é representado com 3 pontos de código - mesmo que não seja necessário, como print("\uac01")mostra). No entanto, em muitas circunstâncias práticas, o comprimento de uma string é o número de caracteres percebidos pelo usuário, porque muitos caracteres são normalmente armazenados pelo Python como um único ponto de código Unicode.

No Python 2 , strings Unicode são chamadas de… "strings Unicode" ( unicodetipo, forma literal u"…"), enquanto matrizes de bytes são "strings" ( strtipo, onde a matriz de bytes pode, por exemplo, ser construída com literais de string "…"). No Python 3 , as strings Unicode são simplesmente chamadas de "strings" ( strtipo, forma literal "…"), enquanto as matrizes de bytes são "bytes" ( bytestipo, forma literal b"…"). Como consequência, algo como "🐍"[0]dá um resultado diferente em Python 2 ( '\xf0', um byte) e Python 3 ( "🐍", o primeiro e único caractere).

Com esses poucos pontos-chave, você deve ser capaz de entender a maioria das questões relacionadas à codificação!


Normalmente, quando você imprime u"…" em um terminal , você não deve pegar lixo: Python conhece a codificação de seu terminal. Na verdade, você pode verificar qual codificação o terminal espera:

% python
Python 2.7.6 (default, Nov 15 2013, 15:20:37) 
[GCC 4.2.1 Compatible Apple LLVM 5.0 (clang-500.2.79)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import sys
>>> print sys.stdout.encoding
UTF-8

Se seus caracteres de entrada puderem ser codificados com a codificação do terminal, o Python fará isso e enviará os bytes correspondentes ao seu terminal sem reclamar. O terminal fará o possível para exibir os caracteres após decodificar os bytes de entrada (na pior das hipóteses, a fonte do terminal não possui alguns dos caracteres e, em vez disso, imprimirá algum tipo de espaço em branco).

Se os caracteres de entrada não puderem ser codificados com a codificação do terminal, isso significa que o terminal não está configurado para exibir esses caracteres. Python irá reclamar (em Python com um uma UnicodeEncodeErrorvez que a string de caracteres não pode ser codificada de uma forma que se adapte ao seu terminal). A única solução possível é usar um terminal que pode exibir os caracteres (seja configurando o terminal para que ele aceite uma codificação que possa representar seus caracteres, ou usando um programa de terminal diferente). Isso é importante quando você distribui programas que podem ser usados ​​em diferentes ambientes: as mensagens impressas devem ser representadas no terminal do usuário. Às vezes, é melhor usar strings que contenham apenas caracteres ASCII.

No entanto, quando você redireciona ou canaliza a saída de seu programa, geralmente não é possível saber qual é a codificação de entrada do programa receptor, e o código acima retorna alguma codificação padrão: Nenhum (Python 2.7) ou UTF-8 ( Python 3):

% python2.7 -c "import sys; print sys.stdout.encoding" | cat
None
% python3.4 -c "import sys; print(sys.stdout.encoding)" | cat
UTF-8

A codificação de stdin, stdout e stderr pode, no entanto, ser definida por PYTHONIOENCODINGmeio da variável de ambiente, se necessário:

% PYTHONIOENCODING=UTF-8 python2.7 -c "import sys; print sys.stdout.encoding" | cat
UTF-8

Se a impressão para um terminal não produzir o que você espera, você pode verificar se a codificação UTF-8 que você colocou manualmente está correta; por exemplo, seu primeiro caractere ( \u001A) não pode ser impresso, se não estou enganado .

Em http://wiki.python.org/moin/PrintFails , você pode encontrar uma solução como a seguinte, para Python 2.x:

import codecs
import locale
import sys

# Wrap sys.stdout into a StreamWriter to allow writing unicode.
sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) 

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni

Para Python 3, você pode verificar uma das perguntas feitas anteriormente no StackOverflow.

Eric O Lebigot
fonte
2
@singularity: Obrigado! Eu adicionei algumas informações para Python 3.
Eric O Lebigot
2
Obrigada cara! Eu precisava dessa explicação por tanto tempo ... É uma pena que eu possa dar a você apenas um voto positivo.
mik01aj
3
Estou feliz por ter ajudado, @ m01! Uma das motivações para escrever esta resposta foi que havia muitas páginas na web sobre Unicode e Python, mas descobri que, apesar de serem interessantes, eles nunca me permitiram resolver problemas de codificação concretos ... Eu realmente acredito nisso, tendo em mente que princípios encontrados nesta resposta e dedicar algum tempo para usá-los na solução de problemas de codificação concretos ajuda muito.
Eric O Lebigot
3
Esta é de longe a melhor explicação sobre Unicode e Python de todos os tempos. O Python Unicode HOWTO deve ser substituído por este.
stantonk
1
Aqui, deixe-me desenhar o caractere "substituição da direita para a esquerda" neste quadro-negro ...
icktoofay
20

Python sempre codifica strings Unicode ao gravar em um terminal, arquivo, canal, etc. Ao gravar em um terminal, o Python geralmente pode determinar a codificação do terminal e usá-lo corretamente. Ao gravar em um arquivo ou canal, o padrão do Python é a codificação 'ascii', a menos que seja explicitamente informado o contrário. Pode-se dizer ao Python o que fazer ao canalizar a saída por PYTHONIOENCODINGmeio da variável de ambiente. Um shell pode definir essa variável antes de redirecionar a saída do Python para um arquivo ou canal para que a codificação correta seja conhecida.

No seu caso, você imprimiu 4 caracteres incomuns que seu terminal não suportava em sua fonte. Aqui estão alguns exemplos para ajudar a explicar o comportamento, com caracteres que são realmente suportados pelo meu terminal (que usa cp437, não UTF-8).

Exemplo 1

Observe que o #codingcomentário indica a codificação na qual o arquivo de origem é salvo. Escolhi utf8 para poder suportar caracteres na origem que meu terminal não podia. Codificação redirecionada para stderr para que possa ser vista quando redirecionada para um arquivo.

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ'
print >>sys.stderr,sys.stdout.encoding
print uni

Saída (executado diretamente do terminal)

cp437
αßΓπΣσµτΦΘΩδ∞φ

Python determinou corretamente a codificação do terminal.

Saída (redirecionado para o arquivo)

None
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-13: ordinal not in range(128)

Python não conseguiu determinar a codificação (Nenhuma), então usou o padrão 'ascii'. ASCII suporta apenas a conversão dos primeiros 128 caracteres de Unicode.

Saída (redirecionado para o arquivo, PYTHONIOENCODING = cp437)

cp437

e meu arquivo de saída estava correto:

C:\>type out.txt
αßΓπΣσµτΦΘΩδ∞φ

Exemplo 2

Agora, colocarei um caractere na fonte que não é compatível com meu terminal:

#coding: utf8
import sys
uni = u'αßΓπΣσµτΦΘΩδ∞φ马' # added Chinese character at end.
print >>sys.stderr,sys.stdout.encoding
print uni

Saída (executado diretamente do terminal)

cp437
Traceback (most recent call last):
  File "C:\ex.py", line 5, in <module>
    print uni
  File "C:\Python26\lib\encodings\cp437.py", line 12, in encode
    return codecs.charmap_encode(input,errors,encoding_map)
UnicodeEncodeError: 'charmap' codec can't encode character u'\u9a6c' in position 14: character maps to <undefined>

Meu terminal não entendia o último caractere chinês.

Saída (executar diretamente, PYTHONIOENCODING = 437: substituir)

cp437
αßΓπΣσµτΦΘΩδ∞φ?

Manipuladores de erros podem ser especificados com a codificação. Neste caso, caracteres desconhecidos foram substituídos por ?. ignoree xmlcharrefreplacealgumas outras opções. Ao usar UTF8 (que suporta a codificação de todos os caracteres Unicode), as substituições nunca serão feitas, mas a fonte usada para exibir os caracteres ainda deve suportá-los.

Mark Tolonen
fonte
Não é exatamente verdade que "Ao gravar em um arquivo ou canal, o padrão do Python é a codificação 'ascii', a menos que seja explicitamente informado o contrário.". Na verdade, Python 3 usa UTF-8, no Mac OS X / Fink.
Eric O Lebigot
2
Sim, o padrão do Python 3 é 'utf8', mas com base na amostra do OP, ele está usando o Python 2.X, cujo padrão é 'ascii'.
Mark Tolonen
Não consegui obter a saída correta manipulando PYTHONIOENCODING. Fazer print string.encode("UTF-8")como sugerido por @Ismail funcionou para mim.
tripleee
você pode ver os caracteres chineses se sua fonte for compatível, mesmo que a chcppágina de código não os ofereça . Para evitar UnicodeEncodeError: 'charmap', você pode instalar o win-unicode-consolepacote.
jfs
Meu problema é que a CLI do python-gitlab imprime bem os caracteres chineses em cmd, mas os caracteres são lixo após serem redirecionados para os arquivos. PYTHONIOENCODING=utf-8resolve o problema.
ElpieKay
12

Codifique durante a impressão

uni = u"\u001A\u0BC3\u1451\U0001D10C"
print uni.encode("utf-8")

Isso ocorre porque quando você executa o script manualmente, o python o codifica antes de enviá-lo para o terminal, quando você o canaliza, o python não o codifica sozinho, então você tem que codificar manualmente ao fazer E / S.

ismail
fonte
4
Ele ainda não responde à pergunta WTH está acontecendo aqui. Por que, do nada, ele decide codificar apenas quando redirecionado, quando isso deveria ser completamente transparente para o processo.
Maxim Sloyko
Por que o python não o codifica ao executar o redirecionamento? O python verifica explicitamente e decide que fará as coisas de maneira diferente apenas para ser difícil?
Arafangion
1
o python ainda tem uma maneira de distinguir as duas situações? Eu pensei (até agora ...) que não há como saber.
zedoo
4
Python pode verificar se a saída é um terminal, se a saída é para um tubo, o tipo de terminal será "burro". Eu acho que "idiota" deve lhe dizer por que o Python não tenta fazer nada automático neste caso, ele pode falhar.
ismail
1
ele produz mojibake se o ambiente usar uma codificação de caracteres incompatível com utf-8 (por exemplo, é comum no Windows). Não fixe a codificação de caracteres de seu ambiente dentro de seu script. Configure sua localidade, ou PYTHONIOENCODING, ou instale win-unicode-console(Windows), ou aceite um parâmetro de linha de comando (se for necessário).
jfs