2012-02-10 11 views
14

Wydawało mi się, że wiem wszystko o kodowaniu i Pythonie, ale dziś natknąłem się na dziwny problem: chociaż konsola jest ustawiona na stronę kodową 850 - a Python zgłasza to poprawnie - parametry umieszczone w wierszu poleceń wydają się być zakodowane na stronie kodowej 1252. Jeśli spróbuję je rozszyfrować przy pomocy sys.stdin.encoding, otrzymam błędny wynik. Jeśli założę "cp1252", ignorując raporty sys.stdout.encoding, to działa.Python, konsola i kodowanie okien (cp 850 vs cp1252)

Czy czegoś brakuje, czy jest to błąd w Pythonie? Windows? Uwaga: Używam Python 2.6.6 na Windows 7 EN, ustawienia regionalne na francuski (Szwajcaria).

W poniższym programie testowym sprawdzam, czy literały są poprawnie interpretowane i czy można je wydrukować - to działa. Ale wszystkie wartości mijam w wierszu poleceń wydaje się być zakodowane błędnie:

#!/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 "" 

w nowo utworzonej konsoli, gdy uruchomiony

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

uzyskać następujący wynik

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 ] 

podczas Spodziewam się, że czwarty znak ma wartość porządkową zamiast 233 (patrz strony kodowe 850 i 1252).

Uwagi: wartość 128 symbolu euro jest zagadką - ponieważ cp850 jej nie ma. W przeciwnym razie '?' są oczekiwane - cp850 nie może drukować znaków i użyłem "zamień" w konwersjach.

Jeśli zmienić stronę kodową konsoli do 1252 roku wydając chcp 1252 i uruchomić to samo polecenie, I (prawidłowo) uzyskać

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 

Jakieś pomysły co mi brakuje?

Edytuj 1: Właśnie sprawdziłem, czytając sys.stdin. Działa to zgodnie z oczekiwaniami: w cp850 wpisanie "é" daje wartość porządkową równą 130. Tak więc problem dotyczy tylko wiersza poleceń. Czy linia poleceń jest traktowana inaczej niż standardowe wejście?

Edycja 2: Wygląda na to, że miałem złe słowa kluczowe. Znalazłem inny bardzo bliski temat na SO: Read Unicode characters from command-line arguments in Python 2.x on Windows. Nadal, jeśli linia poleceń nie jest zakodowana tak jak sys.stdin, a ponieważ sys.getdefaultencoding() zgłasza 'ascii', wydaje się, że nie ma możliwości poznania jej faktycznego kodowania. Uważam, że odpowiedź przy użyciu rozszerzeń Win32 jest dość hackowata.

Odpowiedz

21

odpowiadanie sobie:

W Windows kodowania wykorzystanego przez konsolę (czyli w przypadku sys.stdin/z) różni się od kodowania z różnych łańcuchów OS warunkiem - uzyskany przez np os.getenv(), sys.argv, a na pewno wiele innych.

Kodowanie dostarczone przez sys.getdefaultencoding() jest tak naprawdę - domyślne, wybrane przez programistów Pythona, aby pasowało do "najbardziej sensownego kodowania" używanego przez interpreter w skrajnych przypadkach. Dostaję "ascii" na moim Pythonie 2.6 i próbuję z przenośnym Pythonem 3.1, który daje "utf-8". Oba nie są tym, czego szukamy - są jedynie awariami do kodowania funkcji konwersji.

Wygląda na to, że kodowanie używane przez łańcuchy dostarczane przez system operacyjny jest regulowane przez aktywną stronę kodową (ACP) zgodnie z this page. Ponieważ Python nie posiada natywną funkcję, aby je odzyskać, musiałem użyć ctypes:

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

Edit: Ale jak sugeruje Jacek, tam faktycznie jest bardziej wytrzymałe i pythonowy sposób to zrobić (semantics musiałyby walidacji, ale dopóki nie zostanie udowodnione źle, będę korzystać z tego)

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

a następnie

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

W moim systemie, os_encoding = 'cp1252', więc to działa. Jestem pewien, że to by się zepsuło na innych platformach, więc możesz je edytować i uczynić bardziej ogólnymi. Z pewnością potrzebowalibyśmy jakiejś tablicy tłumaczeń między ACP zgłaszanym przez Windows a nazwą kodu Pythona - coś lepszego niż tylko przygotowywanie "cp".

To jest niestety hack, chociaż uważam, że jest nieco mniej inwazyjny niż sugerowany przez this ActiveState Code Recipe (związany z przez pytanie SO wspomniano w Edit 2 mojego pytania). Zaletą, którą widzę tutaj, jest to, że można to zastosować do os.getenv(), a nie tylko do sys.argv.

+2

Dla Linuksa zwykle 'locale.getpreferredencoding()' lub, po użyciu 'locale.setlocale()' - 'locale.getlocale() [1]' daje odpowiednie kodowanie dla konsoli i dostępu do środowiska. Chociaż zakodowany na stałe kod UTF-8 jest często wystarczająco dobry dla większości nowoczesnych systemów (więc jest to najlepsza wartość powrotna). –

1

Próbowałem rozwiązania. Może nadal mieć pewne problemy z kodowaniem. Musimy użyć prawdziwych czcionek. Naprawiono: Fix:

  1. Uruchom chcp 65001 w cmd, aby zmienić kodowanie na UTF-8.
  2. Zmiany cmd czcionek do True-Type jednej jak Lucida Console, który obsługuje poprzednich stronach kod przed 65001

Oto mój pełny poprawka dla błędu kodowania:

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)) 

Uwaga: Możesz zobaczyć zmianę czcionki podczas wykonywania programu.