2012-02-10 28 views
14

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 comando parecen estar codificados en la página de códigos 1252. Si intento decodificarlos con sys.stdin.encoding, obtengo el resultado incorrecto. Si asumo 'cp1252', ignorando los informes de sys.stdout.encoding, funciona.Python, consola de Windows y codificaciones (cp 850 vs cp1252)

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

En el programa de prueba a continuación, compruebo que los literales se interpretan correctamente y se pueden imprimir, esto funciona. Sin embargo, todos los valores de los que paso en la línea de comandos parecen ser codificados erróneamente:

#!/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 de nueva creación, cuando se ejecuta

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

Me da 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 <- é [assuming input was actually cp1252 ] 
    Ç -> 128 <- ? [assuming input was actually cp1252 ] 

mientras Esperaría que el 4to carácter tenga un valor ordinal de en vez de 233 (vea 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 de la página de códigos de la consola para 1252 mediante la emisión de chcp 1252 y ejecutar el mismo comando, I (correctamente) Obtener

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

Edit 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 comando. Entonces, ¿la línea de comando se trata de manera diferente a la entrada estándar?

Editar 2: Parece que tuve las palabras clave incorrectas. Encontré otro tema muy cercano en SO: Read Unicode characters from command-line arguments in Python 2.x on Windows. Aún así, si la línea de comando no está codificada como sys.stdin, y dado que sys.getdefaultencoding() informa 'ascii', parece que no hay forma de conocer su codificación real. La respuesta que uso extensiones de win32 me parece bastante hacky.

Respuesta

21

Respondiendo a mí mismo:

En Windows, la codificación utilizada por la consola (por lo tanto, el de sys.stdin/out) difiere de la codificación de varias cadenas OS-proporcionado - obtenida a través de, por ejemplo, os.getenv(), sys.argv, y ciertamente muchos más.

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

Como parece indicar this page, la codificación 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()) 

Editar: Pero como sugiere Jacek, en realidad es una forma más robusta y Pythonic hacerlo (semantics que puedas necesitar validación, pero hasta demuestre lo contrario, voy a utilizar este)

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

y luego

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', entonces funciona. Estoy bastante seguro de que esto se romperá en otras plataformas, así que siéntete libre de editarlo y hacerlo más genérico. Ciertamente, necesitaríamos algún tipo de tabla de traducción entre el ACP reportado por Windows y el nombre de codificación de Python, algo mejor que solo anteponer 'cp'.

Esto es lamentablemente un truco, aunque me parece un poco menos intrusivo que el sugerido por this ActiveState Code Recipe (vinculado a 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.

+2

Para Linux usualmente 'locale.getpreferredencoding()' o, después de usar 'locale.setlocale()' - 'locale.getlocale() [1]' proporciona la codificación correcta para el acceso a la consola y al entorno. Sin embargo, el UTF-8 codificado a menudo es lo suficientemente bueno para la mayoría de los sistemas modernos (por lo que es el mejor valor de recuperación). –

1

Probé las soluciones. Todavía puede tener algunos problemas de codificación. Necesitamos usar fuentes de tipo verdadero. Fix:

  1. Run chcp 65001 en cmd Para cambiar la codificación a UTF-8.
  2. Cambio cmd fuente a un Verdadero-Tipo uno como Lucida Console que soporta las páginas de códigos anteriores antes de 65001

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

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: Puede ver un cambio de fuente mientras ejecuta el programa.