UnicodeDecodeError al redirigir al archivo

100

Ejecuto este fragmento dos veces, en la terminal de Ubuntu (codificación establecida en utf-8), una vez con ./test.pyy luego con ./test.py >out.txt:

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

Sin redirección imprime basura. Con la redirección obtengo un UnicodeDecodeError. ¿Alguien puede explicar por qué obtengo el error solo en el segundo caso, o mejor aún, dar una explicación detallada de lo que sucede detrás de la cortina en ambos casos?

zedoo
fuente
Esta respuesta también podría ser de ayuda.
tzot
Cuando trato de replicar su hallazgo, obtengo un UnicodeEncodeError, no un UnicodeDecodeError. gist.github.com/jaraco/12abfc05872c65a4f3f6cd58b6f9be4d
Jason R. Coombs

Respuestas:

252

La clave de estos problemas de codificación es comprender que, en principio, existen dos conceptos distintos de "cadena" : (1) cadena de caracteres y (2) cadena / matriz de bytes. Esta distinción ha sido mayoritariamente ignorada durante mucho tiempo debido a la ubicuidad histórica de codificaciones con no más de 256 caracteres (ASCII, Latin-1, Windows-1252, Mac OS Roman,…): estas codificaciones mapean un conjunto de caracteres comunes a números entre 0 y 255 (es decir, bytes); el intercambio relativamente limitado de archivos antes de la llegada de la web hizo tolerable esta situación de codificaciones incompatibles, ya que la mayoría de los programas podían ignorar el hecho de que había múltiples codificaciones siempre que produjeran texto que permaneciera en el mismo sistema operativo: tales programas simplemente tratar el texto como bytes (a través de la codificación utilizada por el sistema operativo). La visión moderna y correcta separa adecuadamente estos dos conceptos de cuerdas, basándose en los dos puntos siguientes:

  1. La mayoría de los personajes no están relacionados con las computadoras : uno puede dibujarlos en una pizarra, etc., como por ejemplo بايثون, 中 蟒 y 🐍. Los "caracteres" para máquinas también incluyen "instrucciones de dibujo" como, por ejemplo, espacios, retorno de carro, instrucciones para establecer la dirección de escritura (para árabe, etc.), acentos, etc. El estándar Unicode incluye una lista de caracteres muy grande ; cubre la mayoría de los personajes conocidos.

  2. Por otro lado, las computadoras necesitan representar caracteres abstractos de alguna manera: para esto, usan matrices de bytes (números entre 0 y 255 incluidos), porque su memoria viene en bloques de bytes. El proceso necesario que convierte caracteres en bytes se llama codificación . Por lo tanto, una computadora requiere una codificación para representar caracteres. Cualquier texto presente en su computadora está codificado (hasta que se muestra), ya sea que se envíe a una terminal (que espera caracteres codificados de una manera específica) o se guarde en un archivo. Para que se muestren o "comprendan" correctamente (por ejemplo, el intérprete de Python), los flujos de bytes se decodifican en caracteres. Algunas codificaciones(UTF-8, UTF-16,…) están definidos por Unicode para su lista de caracteres (Unicode define tanto una lista de caracteres como codificaciones para estos caracteres; todavía hay lugares donde uno ve la expresión "codificación Unicode" como un forma de referirse al ubicuo UTF-8, pero esta es una terminología incorrecta, ya que Unicode proporciona múltiples codificaciones).

En resumen, las computadoras necesitan representar internamente caracteres con bytes , y lo hacen a través de dos operaciones:

Codificación : caracteres → bytes

Decodificación : bytes → caracteres

Algunas codificaciones no pueden codificar todos los caracteres (por ejemplo, ASCII), mientras que (algunas) codificaciones Unicode le permiten codificar todos los caracteres Unicode. La codificación tampoco es necesariamente única , porque algunos caracteres se pueden representar directamente o como una combinación (por ejemplo, de un carácter base y de acentos).

Tenga en cuenta que el concepto de nueva línea agrega una capa de complicación , ya que puede estar representado por diferentes caracteres (de control) que dependen del sistema operativo (esta es la razón del modo de lectura de archivos de nueva línea universal de Python ).

Ahora, lo que he llamado "carácter" anteriormente es lo que Unicode llama un " carácter percibido por el usuario ". Un solo carácter percibido por el usuario a veces se puede representar en Unicode combinando partes de caracteres (carácter base, acentos, ...) que se encuentran en diferentes índices de la lista Unicode, que se denominan " puntos de código "; estos puntos de código se pueden combinar para formar un "grupo de grafemas". Unicode conduce así a un tercer concepto de cadena, hecho de una secuencia de puntos de código Unicode, que se encuentra entre cadenas de bytes y caracteres, y que está más cerca de esta última. Los llamaré " cadenas Unicode " (como en Python 2).

Si bien Python puede imprimir cadenas de caracteres (percibidos por el usuario), las cadenas sin bytes de Python son esencialmente secuencias de puntos de código Unicode , no de caracteres percibidos por el usuario. Los valores de los puntos de código son los que se utilizan en la sintaxis de cadenas de Python \uy \UUnicode. No deben confundirse con la codificación de un carácter (y no tienen que tener ninguna relación con él: los puntos de código Unicode se pueden codificar de varias formas).

Esto tiene una consecuencia importante: la longitud de una cadena de Python (Unicode) es su número de puntos de código, que no siempre es su número de caracteres percibidos por el usuario : así s = "\u1100\u1161\u11a8"; print(s, "len", len(s))(Python 3) da a 각 len 3pesar de stener un solo usuario percibido (coreano) carácter (porque está representado con 3 puntos de código, incluso si no es necesario, como se print("\uac01")muestra). Sin embargo, en muchas circunstancias prácticas, la longitud de una cadena es el número de caracteres percibidos por el usuario, porque Python normalmente almacena muchos caracteres como un único punto de código Unicode.

En Python 2 , las cadenas Unicode se denominan ... "cadenas Unicode" ( unicodetipo, forma literal u"…"), mientras que las matrices de bytes son "cadenas" ( strtipo, donde la matriz de bytes se puede construir, por ejemplo, con cadenas literales "…"). En Python 3 , las cadenas Unicode se denominan simplemente "cadenas" ( strtipo, forma literal "…"), mientras que las matrices de bytes son "bytes" ( bytestipo, forma literal b"…"). Como consecuencia, algo como "🐍"[0]da un resultado diferente en Python 2 ( '\xf0', un byte) y Python 3 ( "🐍", el primer y único carácter).

Con estos pocos puntos clave, ¡debería poder comprender la mayoría de las preguntas relacionadas con la codificación!


Normalmente, cuando imprime u"…" en una terminal , no debería recibir basura: Python conoce la codificación de su terminal. De hecho, puede verificar qué codificación espera el terminal:

% 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

Si sus caracteres de entrada se pueden codificar con la codificación de la terminal, Python lo hará y enviará los bytes correspondientes a su terminal sin quejarse. El terminal hará todo lo posible para mostrar los caracteres después de decodificar los bytes de entrada (en el peor de los casos, la fuente del terminal no tiene algunos de los caracteres y en su lugar imprimirá algún tipo de espacio en blanco).

Si sus caracteres de entrada no se pueden codificar con la codificación del terminal, significa que el terminal no está configurado para mostrar estos caracteres. Python se quejará (en Python con un UnicodeEncodeErrordado que la cadena de caracteres no se puede codificar de una manera que se adapte a su terminal). La única solución posible es usar una terminal que pueda mostrar los caracteres (ya sea configurando la terminal para que acepte una codificación que pueda representar sus caracteres, o usando un programa de terminal diferente). Esto es importante cuando distribuye programas que se pueden utilizar en diferentes entornos: los mensajes que imprima deben poder representarse en el terminal del usuario. A veces, por lo tanto, es mejor ceñirse a cadenas que solo contienen caracteres ASCII.

Sin embargo, cuando redirige o canaliza la salida de su programa, generalmente no es posible saber cuál es la codificación de entrada del programa receptor, y el código anterior devuelve alguna codificación predeterminada: Ninguna (Python 2.7) o 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

Sin embargo, la codificación de stdin, stdout y stderr se puede configurar a través de la PYTHONIOENCODINGvariable de entorno, si es necesario:

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

Si la impresión en un terminal no produce lo que espera, puede verificar que la codificación UTF-8 que ingresó manualmente sea correcta; por ejemplo, su primer carácter ( \u001A) no se puede imprimir, si no me equivoco .

En http://wiki.python.org/moin/PrintFails , puede encontrar una solución como la siguiente, 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, puede consultar una de las preguntas que se hicieron anteriormente en StackOverflow.

Eric O Lebigot
fuente
2
@singularity: ¡Gracias!
Agregué
2
¡Gracias hombre! Necesitaba esta explicación durante tanto tiempo ... Es una pena que solo pueda darte un voto a favor.
mik01aj
3
¡Me alegro de haber sido de ayuda, @ m01! Una de las motivaciones para escribir esta respuesta fue que había muchas páginas en la web sobre Unicode y Python, pero descubrí que a pesar de ser interesantes, nunca me permitieron resolver por completo problemas de codificación concretos ... Realmente creo que teniendo en cuenta el principios que se encuentran en esta respuesta y tomarse el tiempo para usarlos cuando se resuelven problemas de codificación concretos ayuda mucho.
Eric O Lebigot
3
Esta es sin duda la mejor explicación de Unicode y Python. El CÓMO de Python Unicode debe reemplazarse por esto.
Stantonk
1
Aquí, déjame dibujar el carácter de "anulación de derecha a izquierda" en esta pizarra ...
icktoofay
20

Python siempre codifica cadenas Unicode cuando escribe en una terminal, archivo, canalización, etc. Cuando escribe en una terminal, Python generalmente puede determinar la codificación de la terminal y usarla correctamente. Al escribir en un archivo o canalización, Python utiliza de forma predeterminada la codificación 'ascii' a menos que se indique explícitamente lo contrario. A Python se le puede decir qué hacer cuando canaliza la salida a través de la PYTHONIOENCODINGvariable de entorno. Un shell puede establecer esta variable antes de redirigir la salida de Python a un archivo o canalización para que se conozca la codificación correcta.

En su caso, ha impreso 4 caracteres poco comunes que su terminal no admitía en su fuente. Aquí hay algunos ejemplos para ayudar a explicar el comportamiento, con caracteres que realmente son compatibles con mi terminal (que usa cp437, no UTF-8).

Ejemplo 1

Tenga en cuenta que el #codingcomentario indica la codificación en la que se guarda el archivo de origen . Elegí utf8 para poder admitir caracteres en la fuente que mi terminal no podía. Codificación redirigida a stderr para que se pueda ver cuando se redirige a un archivo.

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

Salida (ejecutar directamente desde el terminal)

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

Python determinó correctamente la codificación de la terminal.

Salida (redirigida a archivo)

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 no pudo determinar la codificación (Ninguna), por lo que usó 'ascii' por defecto. ASCII solo admite la conversión de los primeros 128 caracteres de Unicode.

Salida (redirigido a archivo, PYTHONIOENCODING = cp437)

cp437

y mi archivo de salida era correcto:

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

Ejemplo 2

Ahora incluiré un carácter en la fuente que no es compatible con mi terminal:

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

Salida (ejecutar directamente desde el 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>

Mi terminal no entendió ese último carácter chino.

Salida (ejecutar directamente, PYTHONIOENCODING = 437: reemplazar)

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

Los controladores de errores se pueden especificar con la codificación. En este caso, los caracteres desconocidos fueron reemplazados por ?. ignorey xmlcharrefreplacehay algunas otras opciones. Cuando se usa UTF8 (que admite la codificación de todos los caracteres Unicode), nunca se realizarán reemplazos, pero la fuente utilizada para mostrar los caracteres aún debe admitirlos.

Mark Tolonen
fuente
No es exactamente cierto que "Cuando se escribe en un archivo o canalización, Python utiliza por defecto la codificación 'ascii' a menos que se indique explícitamente lo contrario". De hecho, Python 3 usa UTF-8, en Mac OS X / Fink.
Eric O Lebigot
2
Sí, Python 3 por defecto es 'utf8', pero según la muestra del OP, está usando Python 2.X, que por defecto es 'ascii'.
Mark Tolonen
No pude obtener la salida correcta manipulando PYTHONIOENCODING. Hacer print string.encode("UTF-8")lo sugerido por @Ismail funcionó para mí.
tripleee
puede ver los caracteres chinos si su fuente los admite incluso si la chcppágina de códigos no los admite. Para evitarlo UnicodeEncodeError: 'charmap', puede instalar el win-unicode-consolepaquete.
jfs
Mi problema es que la CLI de python-gitlab imprime bien los caracteres chinos en cmd, pero los caracteres son basura después de ser redirigidos a archivos. PYTHONIOENCODING=utf-8resuelve el problema.
ElpieKay
12

Codifíquelo mientras imprime

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

Esto se debe a que cuando ejecuta el script manualmente, Python lo codifica antes de enviarlo a la terminal, cuando lo canaliza, Python no lo codifica en sí mismo, por lo que debe codificarlo manualmente al hacer E / S.

ismail
fuente
4
Todavía no responde la pregunta que WTH está sucediendo aquí. Por qué, de la nada, decide codificar solo cuando se redirige, cuando se supone que esto es completamente transparente para el proceso.
Maxim Sloyko
¿Por qué Python no lo codifica al realizar la redirección? ¿Python comprueba y decide explícitamente que hará las cosas de manera diferente solo para ser difícil?
Arafangion
1
¿Python tiene una forma de distinguir las dos situaciones? Pensé (hasta ahora ...) que no hay forma de que pueda saberlo.
zedoo
4
Python puede verificar si la salida es una terminal, si se envía a una tubería, entonces el tipo de terminal será "tonto". Supongo que "tonto" debería decirte por qué Python no intenta hacer nada automáticamente en este caso, puede fallar.
ismail
1
produce mojibake si el entorno usa una codificación de caracteres que es incompatible con utf-8 (por ejemplo, es común en Windows). No codifique la codificación de caracteres de su entorno dentro de su secuencia de comandos. Configure su configuración regional, o PYTHONIOENCODING, o instale win-unicode-console(Windows), o acepte un parámetro de línea de comandos (si debe hacerlo).
jfs