Как напечатать строку юникода в python в консоли Windows

Я работаю над приложением python, которое может печатать текст на нескольких языках в консоли на нескольких платформах. Программа хорошо работает на всех платформах UNIX, но в Windows есть ошибки, печатающие строки unicode в командной строке.

Там уже есть соответствующая тема: (Изменение кодировки Windows cmd вызывает сбой Python) но я не мог найти там своего конкретного ответа.

Например, для следующего азиатского текста в Linux я могу запустить:

>>> print u"\u5f15\u8d77\u7684\u6216".encode("utf-8")
引起的或

Но в окнах я получаю:

>>> print u"\u5f15\u8d77\u7684\u6216".encode("utf-8")
σ╝ץΦ╡╖τתהµטצ

Мне удалось отобразить правильный текст с полем сообщения при выполнении чего-то подобного:

>>> file("bla.vbs", "w").write(u'MsgBox "\u5f15\u8d77\u7684\u6216", 4, "MyTitle"'.encode("utf-16"))
>>> os.system("cscript //U //NoLogo bla.vbs")

Но я хочу иметь возможность делать это в консоли Windows и, желательно, - не требуя слишком большой конфигурации вне моего кода на Python (потому что мое приложение будет распространено на многие хосты).

Возможно ли это?

Изменить: Если это невозможно - я был бы рад принять некоторые другие предложения по написанию консольного приложения в окнах, отображающих unicode, например. реализация python альтернативной консоли Windows

Ответы

Ответ 1

Здесь есть решение WriteConsoleW, которое предоставляет unicode argv и stdout (print), но не stdin: Изменение кода Windows cmd вызывает сбой Python

Единственное, что я изменил, это sys.argv, чтобы сохранить его в unicode. Исходная версия utf-8 по какой-то причине закодировала ее.

#!/usr/bin/env python
# -*- coding: utf-8 -*-

""" /questions/11792/windows-cmd-encoding-change-causes-python-crash
"""

import sys

if sys.platform == "win32":
    import codecs
    from ctypes import WINFUNCTYPE, windll, POINTER, byref, c_int
    from ctypes.wintypes import BOOL, HANDLE, DWORD, LPWSTR, LPCWSTR, LPVOID

    original_stderr = sys.stderr

    # If any exception occurs in this code, we'll probably try to print it on stderr,
    # which makes for frustrating debugging if stderr is directed to our wrapper.
    # So be paranoid about catching errors and reporting them to original_stderr,
    # so that we can at least see them.
    def _complain(message):
        print >>original_stderr, message if isinstance(message, str) else repr(message)

    # Work around <http://bugs.python.org/issue6058>.
    codecs.register(lambda name: codecs.lookup('utf-8') if name == 'cp65001' else None)

    # Make Unicode console output work independently of the current code page.
    # This also fixes <http://bugs.python.org/issue1602>.
    # Credit to Michael Kaplan <http://www.siao2.com/2010/04/07/9989346.aspx>
    # and TZOmegaTZIOY
    # <https://stackoverflow.com/questions/878972/windows-cmd-encoding-change-causes-python-crash/1432462#1432462>.
    try:
        # <http://msdn.microsoft.com/en-us/library/ms683231(VS.85).aspx>
        # HANDLE WINAPI GetStdHandle(DWORD nStdHandle);
        # returns INVALID_HANDLE_VALUE, NULL, or a valid handle
        #
        # <http://msdn.microsoft.com/en-us/library/aa364960(VS.85).aspx>
        # DWORD WINAPI GetFileType(DWORD hFile);
        #
        # <http://msdn.microsoft.com/en-us/library/ms683167(VS.85).aspx>
        # BOOL WINAPI GetConsoleMode(HANDLE hConsole, LPDWORD lpMode);

        GetStdHandle = WINFUNCTYPE(HANDLE, DWORD)(("GetStdHandle", windll.kernel32))
        STD_OUTPUT_HANDLE = DWORD(-11)
        STD_ERROR_HANDLE = DWORD(-12)
        GetFileType = WINFUNCTYPE(DWORD, DWORD)(("GetFileType", windll.kernel32))
        FILE_TYPE_CHAR = 0x0002
        FILE_TYPE_REMOTE = 0x8000
        GetConsoleMode = WINFUNCTYPE(BOOL, HANDLE, POINTER(DWORD))(("GetConsoleMode", windll.kernel32))
        INVALID_HANDLE_VALUE = DWORD(-1).value

        def not_a_console(handle):
            if handle == INVALID_HANDLE_VALUE or handle is None:
                return True
            return ((GetFileType(handle) & ~FILE_TYPE_REMOTE) != FILE_TYPE_CHAR
                    or GetConsoleMode(handle, byref(DWORD())) == 0)

        old_stdout_fileno = None
        old_stderr_fileno = None
        if hasattr(sys.stdout, 'fileno'):
            old_stdout_fileno = sys.stdout.fileno()
        if hasattr(sys.stderr, 'fileno'):
            old_stderr_fileno = sys.stderr.fileno()

        STDOUT_FILENO = 1
        STDERR_FILENO = 2
        real_stdout = (old_stdout_fileno == STDOUT_FILENO)
        real_stderr = (old_stderr_fileno == STDERR_FILENO)

        if real_stdout:
            hStdout = GetStdHandle(STD_OUTPUT_HANDLE)
            if not_a_console(hStdout):
                real_stdout = False

        if real_stderr:
            hStderr = GetStdHandle(STD_ERROR_HANDLE)
            if not_a_console(hStderr):
                real_stderr = False

        if real_stdout or real_stderr:
            # BOOL WINAPI WriteConsoleW(HANDLE hOutput, LPWSTR lpBuffer, DWORD nChars,
            #                           LPDWORD lpCharsWritten, LPVOID lpReserved);

            WriteConsoleW = WINFUNCTYPE(BOOL, HANDLE, LPWSTR, DWORD, POINTER(DWORD), LPVOID)(("WriteConsoleW", windll.kernel32))

            class UnicodeOutput:
                def __init__(self, hConsole, stream, fileno, name):
                    self._hConsole = hConsole
                    self._stream = stream
                    self._fileno = fileno
                    self.closed = False
                    self.softspace = False
                    self.mode = 'w'
                    self.encoding = 'utf-8'
                    self.name = name
                    self.flush()

                def isatty(self):
                    return False

                def close(self):
                    # don't really close the handle, that would only cause problems
                    self.closed = True

                def fileno(self):
                    return self._fileno

                def flush(self):
                    if self._hConsole is None:
                        try:
                            self._stream.flush()
                        except Exception as e:
                            _complain("%s.flush: %r from %r" % (self.name, e, self._stream))
                            raise

                def write(self, text):
                    try:
                        if self._hConsole is None:
                            if isinstance(text, unicode):
                                text = text.encode('utf-8')
                            self._stream.write(text)
                        else:
                            if not isinstance(text, unicode):
                                text = str(text).decode('utf-8')
                            remaining = len(text)
                            while remaining:
                                n = DWORD(0)
                                # There is a shorter-than-documented limitation on the
                                # length of the string passed to WriteConsoleW (see
                                # <http://tahoe-lafs.org/trac/tahoe-lafs/ticket/1232>.
                                retval = WriteConsoleW(self._hConsole, text, min(remaining, 10000), byref(n), None)
                                if retval == 0 or n.value == 0:
                                    raise IOError("WriteConsoleW returned %r, n.value = %r" % (retval, n.value))
                                remaining -= n.value
                                if not remaining:
                                    break
                                text = text[n.value:]
                    except Exception as e:
                        _complain("%s.write: %r" % (self.name, e))
                        raise

                def writelines(self, lines):
                    try:
                        for line in lines:
                            self.write(line)
                    except Exception as e:
                        _complain("%s.writelines: %r" % (self.name, e))
                        raise

            if real_stdout:
                sys.stdout = UnicodeOutput(hStdout, None, STDOUT_FILENO, '<Unicode console stdout>')
            else:
                sys.stdout = UnicodeOutput(None, sys.stdout, old_stdout_fileno, '<Unicode redirected stdout>')

            if real_stderr:
                sys.stderr = UnicodeOutput(hStderr, None, STDERR_FILENO, '<Unicode console stderr>')
            else:
                sys.stderr = UnicodeOutput(None, sys.stderr, old_stderr_fileno, '<Unicode redirected stderr>')
    except Exception as e:
        _complain("exception %r while fixing up sys.stdout and sys.stderr" % (e,))


    # While we're at it, let unmangle the command-line arguments:

    # This works around <http://bugs.python.org/issue2128>.
    GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32))
    CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))(("CommandLineToArgvW", windll.shell32))

    argc = c_int(0)
    argv_unicode = CommandLineToArgvW(GetCommandLineW(), byref(argc))

    argv = [argv_unicode[i] for i in xrange(0, argc.value)]

#    argv = [argv_unicode[i].encode('utf-8') for i in xrange(0, argc.value)]

    if not hasattr(sys, 'frozen'):
        # If this is an executable produced by py2exe or bbfreeze, then it will
        # have been invoked directly. Otherwise, unicode_argv[0] is the Python
        # interpreter, so skip that.
        argv = argv[1:]

        # Also skip option arguments to the Python interpreter.
        while len(argv) > 0:
            arg = argv[0]
            if not arg.startswith(u"-") or arg == u"-":
                break
            argv = argv[1:]
            if arg == u'-m':
                # sys.argv[0] should really be the absolute path of the module source,
                # but never mind
                break
            if arg == u'-c':
                argv[0] = u'-c'
                break

    # if you like:
    sys.argv = argv

Ответ 2

Используйте другую консольную программу. Следующее работает в mintty, эмуляторе терминала по умолчанию в Cygwin.

>>> print u"\u5f15\u8d77\u7684\u6216"
引起的或

Существуют и другие альтернативы консоли для Windows, но я не оценил их поддержку Unicode.

Ответ 3

Это просто происходит из-за того, что cmd и powershell consoel не поддерживают шрифты переменной ширины. Фиксированные шрифты не содержат китайского script. Cygwin в том же случае.
Шпатлевка более продвинутая, поддерживающая шрифты переменной ширины с кириллицей, вьетнамскими, арабскими сценариями, но пока не китайская.

НТН

Ответ 4

Можете ли вы попробовать использовать программу iconv в Windows и транслировать через нее свой Python? Было бы так:

python foo.py | iconv -f utf-8 -t utf-16

Возможно, вам придется немного поработать, чтобы получить iconv в Windows - это часть Cygwin, но вы можете создать его отдельно, если это необходимо.

Ответ 5

Ответ на вопрос в PrintFails статье.

По умолчанию консоль в Microsoft Windows отображает только 256 символы (cp437, Код страницы 437, оригинальный ASCII-символ IBM-PC 1981 множество.)

Для России это означает CP866, другие страны также используют свои собственные кодовые страницы. Это означает, что для правильного чтения вывода Python в консоли Windows вы должны иметь конфигурацию Windows с собственной кодировкой, настроенной для отображения печатных символов.

Я предлагаю вам всегда печатать текст Unicode без какой-либо кодировки, чтобы обеспечить максимальную совместимость с различными платформами.

Если вы попытаетесь напечатать непечатаемый символ, вы получите UnicodeEncodeError или увидите искаженный текст.

В некоторых случаях, если Python не сможет правильно определить кодировку вывода, вы можете попытаться установить переменную среды PYTHONIOENCODING, однако обратите внимание, что это, вероятно, не будет работать для вашего примера, так как ваша консоль не может отображать азиатский текст в текущем конфигурации.

Чтобы перенастроить консоль, используйте Панель управления- > Языковые и региональные настройки- > Дополнительно (вкладка) → Язык не-Unicode (раздел). Обратите внимание, что имена меню переводится мной с русского языка.

См. также ответы на очень похожий вопрос.