Подтвердить что ты не робот

Python, консоль и кодировки Windows (cp 850 vs cp1252)

Мне показалось, что я знаю все о кодировках и Python, но сегодня я столкнулся с какой-то странной проблемой: хотя консоль настроена на кодовую страницу 850 - и Python сообщает об этом правильно - параметры, которые я помещаю в командной строке, кажутся закодированными в code page 1252. Если я попытаюсь декодировать их с помощью sys.stdin.encoding, я получаю неправильный результат. Если я принимаю 'cp1252', игнорируя то, что сообщает sys.stdout.encoding, он работает.

Мне что-то не хватает, или это ошибка в Python? Windows? Примечание. Я запускаю Python 2.6.6 в Windows 7 EN, языковой стандарт установлен на французский (Швейцария).

В приведенной ниже тестовой программе я проверяю правильность интерпретации литералов и их можно печатать - это работает. Но все значения, которые я передаю в командной строке, выглядят неправильно:

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

Во вновь созданной консоли при запуске

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

Я получаю следующий вывод

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 ]

в то время как я ожидал бы, что 4-й символ имеет порядковое значение 130 вместо 233 (см. кодовые страницы 850 и 1252).

Примечания: значение 128 для символа евро является загадкой - поскольку cp850 его не имеет. В противном случае '?' ожидаются - cp850 не может печатать символы, и я использовал "replace" в преобразованиях.

Если я изменил кодовую страницу консоли на 1252, выпустив chcp 1252 и запустив ту же команду, я (правильно) получаю

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

Любые идеи, что мне не хватает?

Изменить 1: Я только что проверил, прочитав sys.stdin. Это работает так, как ожидалось: в cp850 ввод "é" приводит к порядковому значению 130. Таким образом, проблема действительно для командной строки. Итак, является ли командная строка обработанной иначе, чем стандартный ввод?

Изменить 2: Кажется, у меня были неправильные ключевые слова. Я нашел еще одну очень близкую тему о SO: Чтение символов Unicode из аргументов командной строки в Python 2.x в Windows. Тем не менее, если командная строка не закодирована как sys.stdin, а так как sys.getdefaultencoding() сообщает "ascii", кажется, что нет способа узнать ее фактическую кодировку. Я нахожу ответ, используя расширения win32 довольно хаки.

4b9b3361

Ответ 1

Отвечая на вопрос:

В Windows кодировка, используемая консолью (таким образом, для sys.stdin/out), отличается от кодирования различных строк, предоставленных OS, - полученных, например, os.getenv(), sys.argv и, конечно, еще много.

Кодировка, предоставляемая sys.getdefaultencoding(), действительно такова - по умолчанию, выбранные разработчиками Python для соответствия "наиболее разумной кодировке", используемой интерпретатором в крайних случаях. Я получаю "ascii" на моем Python 2.6 и пытаюсь использовать портативный Python 3.1, который дает "utf-8". И то, и другое не то, что мы ищем - это просто резервные копии для функций преобразования кодирования.

Как эта страница, как представляется, кодировка, используемая строками, предоставленными ОС, регулируется активной кодовой страницей (ACP). Поскольку Python не имеет встроенной функции для ее получения, мне пришлось использовать ctypes:

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

Изменить: Но, как предлагает Яцек, на самом деле существует более надежный и путинский способ сделать это (семантика понадобится проверка, но пока не будет доказано, что я использую это)

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

а затем

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

В моей системе os_encoding = 'cp1252', поэтому он работает. Я совершенно уверен, что это сломается на других платформах, поэтому не стесняйтесь редактировать и делать их более универсальными. Нам определенно понадобится какая-то таблица переводов между ACP, сообщенным Windows, и именем кодировки Python - что-то лучше, чем просто "cp".

Это, к сожалению, хак, хотя я нахожу его немного менее навязчивым, чем тот, который предлагается этот рецепт ActiveState Code (связанный вопрос SO, упомянутый в Edit 2 моего вопроса). Преимущество, которое я вижу здесь, это то, что это можно применить к os.getenv(), а не только к sys.argv.

Ответ 2

Я попробовал решения. У него могут быть некоторые проблемы с кодированием. Нам нужно использовать шрифты истинного типа. Fix:

  • Запустите chcp 65001 в cmd, чтобы изменить кодировку на UTF-8.
  • Измените cmd-шрифт на True-Type, например Lucida Console, который поддерживает предшествующие кодовые страницы до 65001

Здесь мое полное исправление для ошибки кодирования:

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

Примечание. При выполнении программы вы можете увидеть изменение шрифта.