Python,Windows控制台和编码(cp850 vs cp1252)

18
我曾以为自己对编码和Python了解得很透彻,但今天我遇到了一个奇怪的问题:尽管控制台设置为代码页850 - 并且Python正确报告了它 - 我在命令行上输入的参数似乎是使用代码页1252进行编码的。如果我尝试使用sys.stdin.encoding对它们进行解码,我会得到错误的结果。如果我假设“cp1252”,忽略sys.stdout.encoding报告的内容,它就可以工作。

我有什么遗漏的吗?还是这是Python的一个bug?Windows?注:我在运行Windows 7 EN的Python 2.6.6上,语言环境设置为法语(瑞士)。

在下面的测试程序中,我检查文字是否被正确解释并可以打印 - 这个是有效的。但是我传递给命令行的所有值似乎都被错误地编码:

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

尽管我期望第四个字符的序数值为130而不是233(请参见代码页8501252)。

注:欧元符号为128的值是一个谜 - 因为cp850没有它。否则,“?”是预期的 - cp850无法打印这些字符,我在转换中使用了“replace”。

如果我通过发出chcp 1252将控制台的代码页更改为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上找到了另一个非常相似的主题:在Windows上使用Python 2.x从命令行参数读取Unicode字符。尽管如此,如果命令行未像sys.stdin一样编码,并且由于sys.getdefaultencoding()报告“ascii”,似乎没有办法知道它的实际编码。我认为使用win32扩展程序的答案相当巧妙。

3个回答

28

回复自己:

在Windows上,控制台使用的编码(因此,sys.stdin/out使用的编码)与各种操作系统提供的字符串的编码不同-例如通过os.getenv(),sys.argv等获得的编码。确实还有更多。

由sys.getdefaultencoding()提供的编码确实是默认值,由Python开发人员选择以匹配解释器在极端情况下使用的“最合理的编码”。我在我的Python 2.6上获得了'ascii',并尝试使用便携式Python 3.1,它产生'utf-8'。两者都不是我们想要的-它们仅仅是用于编码转换函数的后备。

正如this page所述,操作系统提供的字符串使用的编码由Active Code Page (ACP)管理。由于Python没有本地功能来检索它,我不得不使用ctypes:

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

编辑:但正如Jacek所建议的那样,实际上有一种更强大和Pythonic的方法来做到这一点(semantics需要验证,但在被证明错误之前,我会使用它)

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与Python编码名称之间的转换表——比仅仅添加“cp”更好。
不幸的是,这是一种 hack 方法,尽管我认为它比此ActiveState Code Recipe(链接到我问题的Edit 2中提到的SO问题)中建议的方法要稍微少一些侵入性。我在这里看到的优点是,这可以应用于os.getenv(),而不仅仅是sys.argv。

2
对于Linux系统,通常使用locale.getpreferredencoding()或在使用locale.setlocale()后,使用locale.getlocale()[1]可以获得正确的控制台和环境访问编码。尽管如此,在大多数现代系统中,硬编码UTF-8通常已经足够好了(因此它是最好的备用值)。 - Jacek Konieczny

2

我尝试了这些解决方案,但仍可能存在一些编码问题。我们需要使用True Type字体。

修复方法:

  1. 在命令提示符中运行chcp 65001以将编码更改为UTF-8。
  2. 将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))

注意:在执行程序时,您可以看到字体的变化。


1
你可以使用 os.system('chcp 65001 >nul') 命令来删除 chcp 的输出。 - Nuno André

0

对我有效的是使用以下代码片段:

# -*- coding: utf-8 -*-

import os
import sys

print (f"OS: {os.device_encoding(0)}, sys: {sys.stdout.encoding}")

在某些安装了Python 3.8的Windows系统上进行比较,结果表明os.device_encoding(0)始终反映了终端中所设置的代码页。这已经通过在Windows 10和Windows 7上使用Powershell和旧版cmd-shell测试验证。

即使使用shell命令更改了终端的代码页,这一点仍然成立。

chcp 850

例如:

chcp 1252

现在使用os.device_encoding(0)来处理诸如将子进程的stdout结果从字节解码为字符串等任务,即使是像é、ö、³、↓这样的非ASCII字符也可以正常工作。

因此,正如其他人已经指出的那样,在Windows本地设置实际上只是系统信息,关于用户偏好,但并不代表shell当前实际使用的设置。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接