Python、Windows控制台与编码(cp 850对cp1252)

18 投票
3 回答
24433 浏览
提问于 2025-04-17 12:45

我以为我对编码和Python的知识已经很全面了,但今天遇到了一个奇怪的问题:虽然控制台设置为850编码页,而Python也正确地报告了这一点,但我在命令行输入的参数似乎是用1252编码的。如果我用sys.stdin.encoding来解码这些参数,结果就不对。如果我假设是'cp1252',不管sys.stdout.encoding报告的是什么,那就能正常工作。

我是不是漏掉了什么,还是这是Python或Windows的一个bug?注意:我在Windows 7英文版上运行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扩展的方法有点hack。

3 个回答

0

对我来说,以下这段代码有效:

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

import os
import sys

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

在一些Windows系统上用Python 3.8进行比较时,发现os.device_encoding(0)总是能反映终端中的代码页设置。(我在Windows 10和Windows 7的Powershell以及旧的命令提示符下进行了测试)

即使在用命令行更改终端的代码页后,这个情况依然成立:

chcp 850

或者例如:

chcp 1252

现在,使用os.device_encoding(0)来处理一些任务,比如把子进程的标准输出从字节转换成字符串,即使是像é、ö、³、↓这样的非ASCII字符也能正常工作。

所以,正如其他人已经指出的,在Windows上,本地设置其实只是一些关于用户偏好的系统信息,并不代表当前终端实际使用的设置。

2

我试过一些解决办法,但可能还是有一些编码问题。我们需要使用真字体(True Type Fonts)。下面是解决方法:

  1. 在命令提示符(cmd)中输入 chcp 65001,这样可以把编码改成 UTF-8。
  2. 把命令提示符的字体改成像 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))

注意: 在运行程序的时候,你会看到字体发生变化。

28

我自己回复自己:

在Windows系统上,控制台使用的编码(也就是sys.stdin/out的编码)和通过一些系统提供的字符串获取的编码(比如os.getenv()、sys.argv等)是不一样的。

sys.getdefaultencoding()提供的编码其实就是一个默认值,这是Python开发者选择的,目的是为了在极端情况下匹配“最合理的编码”。我在我的Python 2.6上得到的是'ascii',而在便携版的Python 3.1上得到的是'utf-8'。这两者都不是我们想要的,它们只是编码转换函数的备用选项。

正如这个页面所说,系统提供的字符串使用的编码是由活动代码页(ACP)决定的。因为Python没有直接获取这个编码的功能,所以我不得不使用ctypes:

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

补充:不过正如Jacek所建议的,实际上有一种更稳健、更符合Python风格的方法来做到这一点(语义需要验证,但在没有被证明错误之前,我会使用这个方法)

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',所以这个方法有效。我很确定在其他平台上这个方法可能会失效,所以欢迎大家修改并使其更通用。我们确实需要某种翻译表,将Windows报告的ACP和Python的编码名称对应起来——这应该比单纯在前面加上'cp'要好。

这不幸的是一个临时解决方案,尽管我觉得它比这个ActiveState代码食谱(在我问题的补充2中提到的SO问题链接)建议的方案要稍微不那么侵入。我的看法是,这个方法可以应用于os.getenv(),而不仅仅是sys.argv。

撰写回答