Python3开始区分string和bytes之后,写代码时需要考虑的问题便多了…… 这回在Windows下碰到了一个stdout输出时编码错误的问题。

输出你好,世界!遇到的问题

有下面一个简单的Python3程序msg.py,输出你好,世界!这几个字:

# vim:fileencoding=utf-8
msg = "你好,世界!"
print(msg)

在Python脚本中可以在第一行使用# -*- coding: <encoding-name> -*-或者# vim:fileencoding=<encoding-name>来指定文件编码。前者是emacs风格,后者是vim风格。

在Windows的Cmd.exe运行的时候,是正常。但是一旦重定向到一个文件,比如C:\> python msg.py > out1,就会出现一个错误

> python msg.py > out1
Traceback (most recent call last):
  File "msg.py", line 9, in <module>
    print(msg)
  File "C:\Python36\lib\encodings\cp1252.py", line 19, in encode
    return codecs.charmap_encode(input,self.errors,encoding_table)[0]
UnicodeEncodeError: 'charmap' codec can't encode characters in position 0-5: cha
racter maps to <undefined>

把stdout重定向到一个文件的时候竟然编码错误,依据错误显示,应该是使用了cp1252这种编码格式来编码了utf-8的内容呢。 不知道这跟我使用的操作系统有没有关系,我使用的是英文的Windows,用chcp命令显示的code page是437。可是就算我用chcp 65001把代码页切换成代表utf-8的65001,结果也不行。

于是Google了一下,发现了这个帖子:[How to set sys.stdout encoding in Python 3?]。 帖子的内容说到,如果是Python2的话,用下面的方式重新定义一下stdout就可以了:

sys.stdout = codecs.getwriter("utf-8")(sys.stdout)

当时在Python3中有一些小复杂,有几种方式可以修正这个问题。

设置PYTHONIOENCODING环境变量

依据下面的方法,设置PYTHONIOENCODING环境变量,结果就OK了:

set PYTHONIOENCODING=utf-8:surrogateescape
python msg.py > out1
# 没有出现错误

stdout.buffer.write

第二个步骤是越过stdout的编码转换,直接往其buffer里面写入内容:

# vim:fileencoding=utf-8

import sys

msg = "你好,世界!"

print(sys.stdout.encoding)
sys.stdout.buffer.write(msg.encode('utf-8'))

sys.stdout.buffer.write直接把内容写到buffer中,错误没有出现,但是输出的内容却是如下:

你好,世界!cp1252

你好,世界!这几个字符后输出的,却跑到了前面!

使用io.TextIOBase.detach()

Python3的io获得了新的设计,是基于io.TextIOBase的。io.TextIOBase有一个detach功能,可以帮助你获取stdout的buffer。

# vim:fileencoding=utf-8

import sys

msg = "你好,世界!"

buf = sys.stdout.detach()
buf.write(msg.encode('utf-8'))

上面的代码也能够得到正确的结果,可是会有下面的提示:

Exception ignored in: <_io.TextIOWrapper mode='w' encoding='UTF-8'>
ValueError: underlying buffer has been detached

翻阅了一下Python3的文档,是这么说得:After the underlying buffer has been detached, the TextIOBase is in an unusable state. 所以,这并不是一个十分可行的方式。

重新包装stdout

最后一个方式,是使用sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())重新包装一下stdout,一切都OK了。可惜stdout就不是以前那个stdout了,如果你在包装之后的stdout上使用print(sys.stdout.encoding),会告诉你encoding属性不存在的。

# vim:fileencoding=utf-8

import sys, codecs

msg = "你好,世界!"

print(sys.stdout.encoding)
sys.stdout = codecs.getwriter("utf-8")(sys.stdout.detach())
print(msg)

上面的程序会得到下面的结果:

cp1252

你好,世界!

当然,包装stdout的方式肯定不止一种,下面的也可行:

sys.stdout = open(sys.stdout.fileno(), mode='w', encoding='utf8', buffering=1)

小结

通过这篇文章,我们稍微窥探了一下Python3中io.TextIOBase的设计。本文涉及的问题,只在Windows平台碰到过,其他平台尚未遇见。

其他参考

(完)