UnicodeEncodeError: 'charmap' 编码器无法编码 - 字符映射到<未定义>, 打印函数

189 投票
6 回答
544175 浏览
提问于 2025-04-17 14:20

我正在写一个Python(Python 3.3)程序,目的是通过POST方法向网页发送一些数据。为了调试,我会获取网页的结果,并使用print()函数把它显示在屏幕上。

我的代码大致是这样的:

conn.request("POST", resource, params, headers)
response = conn.getresponse()
print(response.status, response.reason)
data = response.read()
print(data.decode('utf-8'));

这里的HTTPResponse.read()方法会返回一个bytes类型的数据,这个数据是网页的内容(它是一个格式良好的UTF-8文档)。在我使用Windows的IDLE图形界面时,这一切看起来都没问题,但当我换成Windows控制台时,就出现了问题。返回的网页中有一个U+2014字符(即长破折号),在Windows GUI中,print()函数能很好地显示它(我猜是使用了1252编码),但在Windows控制台中(使用850编码)就显示不正常了。由于默认的strict行为,我遇到了以下错误:

UnicodeEncodeError: 'charmap' codec can't encode character '\u2014' in position 10248: character maps to <undefined>

我用一段看起来不太优雅的代码解决了这个问题:

print(data.decode('utf-8').encode('cp850','replace').decode('cp850'))

现在它把那个有问题的字符“—”替换成了?。虽然这不是最理想的解决方案(用连字符替换可能更好),但对我来说已经足够用了。

不过,我对这个解决方案有几个不满意的地方。

  1. 代码看起来很乱,涉及到很多解码、编码和再解码。
  2. 这个解决方案只适用于当前的情况。如果我把程序移植到其他编码的系统(比如latin-1、cp437、再回到cp1252等),它就无法识别目标编码了。(例如,当我再次使用IDLE GUI时,长破折号也会消失,这在之前是不会发生的)
  3. 如果长破折号能被翻译成连字符,而不是问号,那就更好了。

问题不在于长破折号(我可以想到几种方法来解决这个特定的问题),而是我需要写出更健壮的代码。我从数据库中获取数据并发送到网页,这些数据可能会返回。我可以预见到许多其他可能出现冲突的情况:比如一个'Á'(U+00c1,可能在我的数据库中存在)在CP-850(适用于西欧语言的DOS/Windows控制台编码)中能正常显示,但在CP-437(美国英语的编码,许多Windows安装的默认编码)中就不行了。

所以,我的问题是:

有没有更好的解决方案,让我的代码不受输出界面编码的影响?

6 个回答

25

为了调试,你可以使用 print(repr(data)) 来查看数据的详细信息。

在显示文本时,记得要打印Unicode格式的内容。不要在你的代码里直接写死你环境的字符编码,比如 Cp850。如果你想解码HTTP响应,可以参考这个链接:在Python中获取HTTP响应的字符集/编码的好方法

如果你想在Windows控制台打印Unicode,可以使用 win-unicode-console 这个包

38

根据Dirk Stöcker的回答,这里有一个很方便的包装函数,可以用在Python 3的print函数上。你可以像使用print一样使用它。

另外,相比其他的回答,这个函数不会把你的文本打印成字节数组(像这样'b"内容"),而是会以普通字符串的形式打印(像这样'内容'),这是因为最后有一个解码的步骤。

def uprint(*objects, sep=' ', end='\n', file=sys.stdout):
    enc = file.encoding
    if enc == 'UTF-8':
        print(*objects, sep=sep, end=end, file=file)
    else:
        f = lambda obj: str(obj).encode(enc, errors='backslashreplace').decode(enc)
        print(*map(f, objects), sep=sep, end=end, file=file)

uprint('foo')
uprint(u'Antonín Dvořák')
uprint('foo', 'bar', u'Antonín Dvořák')
117

我看到有三种解决方法:

  1. 改变输出编码,这样它就会始终输出UTF-8格式。你可以参考一下这个链接:在Python中设置正确的编码,不过我自己试过这些例子,没能成功。

  2. 下面的示例代码可以让输出适应你想要的字符集。

    # -*- coding: utf-8 -*-
    import sys
    
    print sys.stdout.encoding
    print u"Stöcker".encode(sys.stdout.encoding, errors='replace')
    print u"Стоескер".encode(sys.stdout.encoding, errors='replace')
    

    这个例子会把我名字里任何不可打印的字符替换成问号。

    如果你创建一个自定义的打印函数,比如叫myprint,用这种方法来正确编码输出,你就可以在需要的地方把print替换成myprint,这样代码看起来就不会那么乱了。

  3. 在软件开始时全局重置输出编码:

    这个页面http://www.macfreek.nl/memory/Encoding_of_Python_stdout对如何改变输出编码有个很好的总结。特别是“Stdout的StreamWriter包装器”这一部分很有意思。基本上它的意思是像这样改变输入输出的编码函数:

    在Python 2中:

    if sys.stdout.encoding != 'cp850':
      sys.stdout = codecs.getwriter('cp850')(sys.stdout, 'strict')
    if sys.stderr.encoding != 'cp850':
      sys.stderr = codecs.getwriter('cp850')(sys.stderr, 'strict')
    

    在Python 3中:

    if sys.stdout.encoding != 'cp850':
      sys.stdout = codecs.getwriter('cp850')(sys.stdout.buffer, 'strict')
    if sys.stderr.encoding != 'cp850':
      sys.stderr = codecs.getwriter('cp850')(sys.stderr.buffer, 'strict')
    

    如果在CGI中输出HTML,你可以把'strict'替换成'xmlcharrefreplace',这样就能为不可打印字符生成HTML编码的标签。

    你可以随意修改这些方法,设置不同的编码等等……不过要注意,输出不指定的数据仍然是行不通的。所以任何数据、输入、文本都必须能够正确转换成unicode:

    # -*- coding: utf-8 -*-
    import sys
    import codecs
    sys.stdout = codecs.getwriter("iso-8859-1")(sys.stdout, 'xmlcharrefreplace')
    print u"Stöcker"                # works
    print "Stöcker".decode("utf-8") # works
    print "Stöcker"                 # fails
    

撰写回答