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

188
我将翻译以下内容:

我正在编写一个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 格式文档。在使用 IDLE GUI for Windows 时似乎没有问题,但是当我改用 Windows 控制台时出现了问题。返回的页面有一个 U+2014 字符(em-dash),print 函数可以在 Windows GUI(可能是 Code Page 1252)中正确地转换,但在 Windows 控制台(Code Page 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安装中是默认的)。
所以,问题是:
是否有更好的解决方案可以使我的代码不依赖于输出接口编码?

请查看此答案 https://dev59.com/sI_ea4cB1Zd3GeqPSb0l#49004993 - Sreeragh A R
你说得完全正确,这很丑陋但却非常有效,在我的情况下使用latin-1。打印(data.decode('cp850').encode('latin-1','replace').decode('latin-1')) - Yuri
6个回答

117

我看到了三种解决方案:

  1. 更改输出编码,使其始终输出UTF-8。例如参见在Python中管道传输stdout时设置正确的编码方式,但我无法让这些示例工作。

  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,而无需使整个代码看起来丑陋。

在软件开头全局重置输出编码:

该页面http://www.macfreek.nl/memory/Encoding_of_Python_stdout对如何更改输出编码进行了很好的总结。特别是“围绕标准输出的StreamWriter包装器”部分很有趣。 它本质上是说要像这样更改I/O编码函数:

在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

1
我目前没有Windows测试平台,但我在Linux(Ubuntu)上进行了测试,我的终端设置为UTF-8,它可以正常工作。然而,我将终端更改为ISO-8859-15后,输出不正确。输出结果如下: UTF-8 Stöcker СДСПескеñ - Carlos Eugenio Thompson Pinzón
1
嗨,我添加了第三点,解决了我的问题。关于上面的内容:“sys.stdout.encoding” 可能不知道您终端的编码方式,但只知道系统范围内的编码方式。如果这与终端不同,则只有解析一些环境变量可能会有所帮助。但这并不总是有效的,因为终端不一定会告诉对方它的编码方式。因此,在不同于系统的情况下,发送方不能始终知道编码方式。 - Dirk Stöcker
6
Python 3: sys.stdout = io.TextIOWrapper(sys.stdout.detach(), sys.stdout.encoding, 'replace'). 或者使用'backslashreplace'来保留原始值(但可能会导致输出格式错位)。 - Eryk Sun
3
@eryksun:或者使用 win-unicode-console 软件包直接打印Unicode,无论 chcp 是什么。或者设置 PYTHONIOENCODING=:replace 环境变量(在脚本之外)。如果你的脚本已经可以打印Unicode,则无需更改脚本,而是配置环境。 - jfs
@eryksun:是的,但是如果输出被重定向到文件/管道,则envvar很有用。 - jfs
显示剩余7条评论

38

基于Dirk Stöcker的回答,这是一个很好的Python 3打印函数的包装器函数。使用它就像你使用print一样。

作为额外的奖励,与其他答案相比,由于最后一步解码,这不会将您的文本打印为bytearray('b"content"'),而是打印为普通字符串('content')。

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

输出:Antonín Dvořák - Don Reba
1
是的,如果输出流(例如您的控制台)不支持 ř 字符,则会使用回退:它将以反斜杠表示法输出 Unicode 代码点:\uXXXX。如果需要,您可以将 'backslashreplace' 替换为其他偏好:https://docs.python.org/3.5/library/stdtypes.html#str.encode - Jelle Fresen
非常好的代码片段,满足我的需求。让我补充一下,我使用了 uprint(f"Description: {repo_dict['description']}") 而不是 print(f"Description: {repo_dict['description']}")。在实践中,只需要在 print 前面加上 u 即可。 - Olgierd Wiśniewski

25

现在Python 3.6已经发布,或许也可以将其作为一个推荐版本(因为该版本基本上切换到了与win-unicode-console包相同的解决方案)。 - Martijn Pieters
@MartijnPieters 如果您点击链接,您将看到Python3.6的推荐。 - jfs
啊,那么最近一篇关于Windows控制台打印行为的帖子就可以更好地成为重复目标了。 - Martijn Pieters
1
很遗憾,该问题使用屏幕截图作为输出。这对于搜索来说几乎没有用处。 - Martijn Pieters
@MartijnPieters “Windows控制台打印行为”的规范重复目标反过来在链接的答案中位于最顶部的链接 - jfs
显示剩余2条评论

25
我深入研究了这个问题,发现最好的解决方案在这里。

http://blog.notdot.net/2010/07/Getting-unicode-right-in-Python

在我的情况下,我解决了“UnicodeEncodeError:'charmap'编解码器无法编码字符”的问题。
原始代码:
print("Process lines, file_name command_line %s\n"% command_line))

新代码:
print("Process lines, file_name command_line %s\n"% command_line.encode('utf-8'))  

2
在我的情况下,使用encode('utf-8')是正确的选择。谢谢。 - alvaro562003

14

如果你正在使用Windows命令行打印数据,那么你应该使用

chcp 65001

这对我有效!


4
这不好。在Windows 8之前,输出极其有缺陷,非ASCII输入一直到Windows 10都会失败。升级到Python 3.6或安装win_unicode_console是获得Windows控制台中正确的Unicode支持的方法。 - Eryk Sun

2
如果您使用Python 3.6(可能是3.5或更高版本),我不再会遇到这个错误。我曾经遇到过类似的问题,因为我使用的是v3.4,但是在重新安装后问题消失了。

5
这不是我认为的问题所在。我使用3.5.2版本,但仍然出现错误。 - khaled4vokalz
3
@khaled4vokalz,不,升级到Python 3.6可以解决这个问题。我们在3.6中开始使用Windows控制台的Unicode API,不再有旧代码页的麻烦了。 - Eryk Sun
3
这个信息不正确。我正在使用Python 3.8,但仍然出现错误。 - EMT
1
我正在使用Python 3.10.4,但仍然遇到问题(使用AWS CLI获取一些国际电子邮件模板)...我尝试导出/设置PYTHONIOENCODING=UTF-8,像一些人建议的那样,但最初没有起作用。然而,在Windows重新启动后,该设置或此Beta Windows-10 UTF-8设置似乎起作用了。 https://dev59.com/HVMI5IYBdhLWcg3wFXSg#57134096 - armyofda12mnkeys
我正在使用Python 3.9.16版本。Python使用UTF-8作为默认编码$ python3 -c 'import sys; print(sys.getdefaultencoding())' utf-8,但仍然无法正常工作。 - undefined

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