Python, consola de Windows y codificaciones (cp 850 vs cp1252)

Pensé que sabía todo sobre codificaciones y Python, pero hoy me encontré con un problema extraño: aunque la consola está configurada en la página de códigos 850 (y Python lo informa correctamente), los parámetros que puse en la línea de comandos parecen estar codificados en la página de códigos 1252 . Si bash descodificarlos con sys.stdin.encoding, obtengo el resultado incorrecto. Si asumo ‘cp1252’, ignorando lo que informa sys.stdout.encoding, funciona.

¿Me estoy perdiendo algo o es un error en Python? Windows? Nota: Estoy ejecutando Python 2.6.6 en Windows 7 EN, configuración regional establecida en francés (Suiza).

En el siguiente progtwig de prueba, verifico que los literales se interpretan correctamente y se pueden imprimir; esto funciona. Pero todos los valores que paso en la línea de comando parecen estar codificados incorrectamente:

#!/usr/bin/python # -*- encoding: utf-8 -*- import sys literal_mb = 'utf-8 literal: üèéÃÂç€ÈÚ' literal_u = u'unicode literal: üèéÃÂç€ÈÚ' print "Testing literals" print literal_mb.decode('utf-8').encode(sys.stdout.encoding,'replace') print literal_u.encode(sys.stdout.encoding,'replace') print "Testing arguments ( stdin/out encodings:",sys.stdin.encoding,"/",sys.stdout.encoding,")" for i in range(1,len(sys.argv)): arg = sys.argv[i] print "arg",i,":",arg for ch in arg: print " ",ch,"->",ord(ch), if ord(ch)>=128 and sys.stdin.encoding == 'cp850': print "<-",ch.decode('cp1252').encode(sys.stdout.encoding,'replace'),"[assuming input was actually cp1252 ]" else: print "" 

En una consola recién creada, cuando se ejecuta

 C:\dev>test-encoding.py abcé€ 

Obtengo la siguiente salida

 Testing literals utf-8 literal: üèéÃÂç?ÈÚ unicode literal: üèéÃÂç?ÈÚ Testing arguments ( stdin/out encodings: cp850 / cp850 ) arg 1 : abcÚÇ a -> 97 b -> 98 c -> 99 Ú -> 233  128 <- ? [assuming input was actually cp1252 ] 

aunque esperaría que el 4º carácter tuviera un valor ordinal de 130 en lugar de 233 (consulte las páginas de códigos 850 y 1252 ).

Notas: el valor de 128 para el símbolo del euro es un misterio, ya que cp850 no lo tiene. De lo contrario, el ‘?’ se esperan – cp850 no puede imprimir los caracteres y he usado ‘reemplazar’ en las conversiones.

Si cambio la página de códigos de la consola a 1252 emitiendo chcp 1252 y ejecuto el mismo comando, chcp 1252 (correctamente)

 Testing literals utf-8 literal: üèéÃÂç€ÈÚ unicode literal: üèéÃÂç€ÈÚ Testing arguments ( stdin/out encodings: cp1252 / cp1252 ) arg 1 : abcé€ a -> 97 b -> 98 c -> 99 é -> 233 € -> 128 

¿Alguna idea de lo que me estoy perdiendo?

Edición 1: acabo de probar leyendo sys.stdin. Esto funciona como se esperaba: en cp850, escribir ‘é’ da como resultado un valor ordinal de 130. Por lo tanto, el problema es realmente solo para la línea de comandos. Entonces, ¿se trata la línea de comando de manera diferente a la entrada estándar?

Edit 2: Parece que tuve las palabras clave equivocadas. Encontré otro tema muy cercano en SO: Lee caracteres Unicode de los argumentos de la línea de comandos en Python 2.x en Windows . Aún así, si la línea de comandos no está codificada como sys.stdin, y dado que sys.getdefaultencoding () informa ‘ascii’, parece que no hay forma de saber su encoding real. Encuentro la respuesta usando extensiones win32 bastante hacky.

Respondiendo a mi mismo:

En Windows, la encoding utilizada por la consola (por lo tanto, la de sys.stdin / out) difiere de la encoding de varias cadenas proporcionadas por el sistema operativo, obtenidas a través de, por ejemplo, os.getenv (), sys.argv, y ciertamente muchas más.

La encoding proporcionada por sys.getdefaultencoding () es realmente eso: un valor predeterminado, elegido por los desarrolladores de Python para que coincida con la “encoding más razonable” que utiliza el intérprete en casos extremos. Obtengo ‘ascii’ en mi Python 2.6, y lo probé con Python 3.1 portátil, que produce ‘utf-8’. Ambos no son lo que estamos buscando, son simplemente una alternativa para codificar las funciones de conversión.

Como esta página parece indicar, la encoding utilizada por las cadenas proporcionadas por el sistema operativo se rige por la página de códigos activos (ACP). Como Python no tiene una función nativa para recuperarlo, tuve que usar ctypes:

 from ctypes import cdll os_encoding = 'cp' + str(cdll.kernel32.GetACP()) 

Edit: Pero como Jacek sugiere, en realidad hay una forma más robusta y Pythonic de hacerlo (la semántica necesitaría una validación, pero hasta que se demuestre lo contrario, la usaré)

 import locale os_encoding = locale.getpreferredencoding() # This returns 'cp1252' on my system, yay! 

y entonces

 u_argv = [x.decode(os_encoding) for x in sys.argv] u_env = os.getenv('myvar').decode(os_encoding) 

En mi sistema, os_encoding = 'cp1252' , por lo que funciona. Estoy bastante seguro de que esto se rompería en otras plataformas, así que siéntase libre de editar y hacerlo más genérico. Sin duda, necesitaríamos algún tipo de tabla de traducción entre el ACP reportado por Windows y el nombre de encoding de Python, algo mejor que simplemente anteponer ‘cp’.

Desafortunadamente, este es un truco, aunque me parece un poco menos intrusivo que el sugerido por esta Receta de Código de ActiveState (vinculado por la pregunta SO mencionada en la Edición 2 de mi pregunta). La ventaja que veo aquí es que esto se puede aplicar a os.getenv (), y no solo a sys.argv.

Probé las soluciones. Todavía puede tener algunos problemas de encoding. Necesitamos usar fonts de tipo verdadero. Fijar:

  1. Ejecute chcp 65001 en cmd para cambiar la encoding a UTF-8.
  2. Cambie la fuente cmd por una True-Type como la Consola Lucida que admita las páginas de códigos anteriores antes de 65001

Aquí está mi solución completa para el error de encoding:

 def fixCodePage(): import sys import codecs import ctypes if sys.platform == 'win32': if sys.stdout.encoding != 'cp65001': os.system("echo off") os.system("chcp 65001") # Change active page code sys.stdout.write("\x1b[A") # Removes the output of chcp command sys.stdout.flush() LF_FACESIZE = 32 STD_OUTPUT_HANDLE = -11 class COORD(ctypes.Structure): _fields_ = [("X", ctypes.c_short), ("Y", ctypes.c_short)] class CONSOLE_FONT_INFOEX(ctypes.Structure): _fields_ = [("cbSize", ctypes.c_ulong), ("nFont", ctypes.c_ulong), ("dwFontSize", COORD), ("FontFamily", ctypes.c_uint), ("FontWeight", ctypes.c_uint), ("FaceName", ctypes.c_wchar * LF_FACESIZE)] font = CONSOLE_FONT_INFOEX() font.cbSize = ctypes.sizeof(CONSOLE_FONT_INFOEX) font.nFont = 12 font.dwFontSize.X = 7 font.dwFontSize.Y = 12 font.FontFamily = 54 font.FontWeight = 400 font.FaceName = "Lucida Console" handle = ctypes.windll.kernel32.GetStdHandle(STD_OUTPUT_HANDLE) ctypes.windll.kernel32.SetCurrentConsoleFontEx(handle, ctypes.c_long(False), ctypes.pointer(font)) 

Nota : Puedes ver un cambio de fuente mientras ejecutas el progtwig.