TL;DR
相信很多用 Sublime Text 来写 Python 2 的同学都遇到过以下这个问题(例如这位同学 /t/100435 和这位同学 /t/163012 ):
在 Sublime Text 里用 Cmd (Ctrl) + B 运行代码 print u'中文'
,想要打印出 unicode 类型的字符串时,会出现以下报错:
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
传说中的 Python 2 编码坑(笑)
而同样的 print u'中文'
代码在 Mac 的终端里却能正常打印出 “中文”
结果,没有任何报错。
虽然在网上能查到多种解决方法,但一直以来知其然而不知其所以然,不了解为什么那些方法能解决问题的真正原因,也不知道为什么同样的代码在终端里就可以运行而在 Sublime Text 里就不行了?
因此我研究学习了下这个问题相关的一些 Python 2 编码问题,在这里分享下我的理解。
以下属于新手向,参考了网上多篇文章,如有错误,望指正。
先说下我的环境:
Python 在向控制台 (console) print
的时候,因为控制台只能看得懂由 bytes(字节序列)组成的字符串,而 Python 中 "unicode" 对象存储的是 code points(码点),因此 Python 需要将输出中的 "unicode" 对象用编码转换为储存 bytes(字节序列)的 "str" 对象后,才能进行输出。
而在报错里看到 UnicodeEncodeError
, 那就说明 Python 在将 unicode 转换为 str 时使用了错误的编码。而为什么是 'ascii'
编码呢?那是因为 Python 2 的默认编码就是 ASCII,可以通过以下命令来查看 Python 的默认编码:
>>> import sys
>>> print sys.getdefaultencoding()
ascii
所以此时在 Sublime Text 里运行 print u'中文'
,实际上等于是运行了:
print u'中文'.encode('ascii')
ASCII 编码无法对 unicode 的中文进行编码,因此就报错了。
那为什么同样的代码 print u'中文'
在 Mac 的终端里却能正常输出中文,难道是因为终端下的 Python 2 的默认编码不是 ASCII?非也,在终端下运行 sys.getdefaultencoding()
结果一样是 ascii
。那同样是 ascii
为什么会有不同的结果?难倒这里 Python 用了另外一个编码来转换?
是的,其实 Python 在 print unicode
时真正涉及到的是另一组编码:stdin/stdout/stderr 的编码,也就是标准输入、标准输出和标准错误输出的编码。可以通过以下命令来查看,这里是在我的终端下运行的结果:
>>> import sys
>>> print sys.stdin.encoding
UTF-8
>>> print sys.stdout.encoding
UTF-8
>>> print sys.stderr.encoding
UTF-8
在正常情况下,Python 2 在 print unicode
时用来转换的编码并不是 Python 的默认编码 sys.getdefaultencoding()
,而是 sys.stdout.encoding
所设的编码。
因为在我的终端下 Python 的 sys.stdout.encoding
编码是 UTF-8,所以在终端里运行 print u'中文'
时,实际上是等于运行了:
print u'中文'.encode('UTF-8')
编码正确,运行正常,因此没有报错。
在类 UNIX 系统下,Python 应该是通过环境变量 LC_CTYPE
来判断 stdin/stdout/stderr 的编码的。因此一般只要将 shell 的 LANG
环境变量设置对为 **_**.UTF-8
后,应该就能在终端里直接 print
出 unicode 类型的字符串了,而不需要在 print
时手动加上 .encode('utf-8')
进行编码了。
但在 Sublime Text 里事情就没那么美好了。在 Sublime Text 里运行查看 stdout 编码的命令,发现:
import sys
print sys.stdout.encoding
-----------------------------"""
None
[Finished in 0.1s]
结果甚至不是 'ascii'
而是 None
。可能是因为 Sublime Text 的 Build System 是用 subprocess.Popen 来运行 Python 的,导致 Python 无法判断出正确的 stdin/stdout/stderr 编码,于是都变成 None 了。
这种情况也发生在输出的目标是管道的情况下:
$ python -c 'import sys; print sys.stdout.encoding' | tee /tmp/foo.txt
None
那么在这种 sys.stdout.encoding
为 None
情况下的 print unicode
怎么办呢?答案就是 Python 只能很无奈地使用 sys.getdefaultencoding()
的默认编码 ascii
来对 unicode 进行转换了。这样就出现了本文开头所说的那个 UnicodeEncodeError
问题。
print
输出时的流程:LC_CTYPE
,来设法判断出 sys.stdin/stdout/stderr.encoding
编码值。sys.stdin/stdout/stderr.encoding
的值设置为 None
。print
时判断字符串是否是 unicode 类型。sys.stdout.encoding
不为 None
时,就使用 sys.stdout.encoding
编码对 unicode 编码成 str 后输出。如果 sys.stdout.encoding
为 None
的话,就使用 sys.getdefaultencoding()
默认编码来对 unicode 进行转换成 str 后输出。
if sys.stdout.encoding:
print unicode.encode(sys.stdout.encoding)
else:
print unicode.encode(sys.getdefaultencoding())
以上参考: https://wiki.python.org/moin/PrintFails
先说最不正确的解决方法:在文件头部加上
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
这种方法通过 dirty hack 的方式在 Python 刚启动时更改了 Python 的默认编码为 utf-8。此后:
>>> print sys.getdefaultencoding()
utf-8
但就本文所讨论的问题来说,这个方法并不是真正地直接解决了问题。就如上述所说,Python 只是在 sys.stdout.encoding
为 None
时才会使用默认编码来转换需要 print
的 unicode 字符串。那万一在 sys.stdout.encoding
存在,但为 ascii
的情况下呢?这样即使更改了 Python 的默认编码,同样还是会出现 UnicodeEncodeError
报错。所以对本问题来说,这个方法治标不治本。
除此之外,很多人都用这个方法来解决 Python 2 下遇到的其它各种各样的编码问题,在 v2ex 的各种 Python 编码问题讨论帖中也常常能见到有人推荐用这个方法来解决问题的。
但实际上很多大牛都不推荐用这个方法来解决 Python 2 的编码问题,这里引用下 StackOverflow 相关回答 里的一句话:
the use of sys.setdefaultencoding() has always been discouraged
为什么这个方法不被推荐呢?我们来看下 Python 文档里对这个 function 是怎么说的:
This function is only intended to be used by the site module implementation and, where needed, by sitecustomize. Once used by the site module, it is removed from the sys module’s namespace.
可以看到这个方法原本就不是用户向的方法,并没有打算让用户用这个方法来更改 Python 2 的默认编码。
那为什么不建议我们更改 Python 的默认编码呢?
这里引用 Python 核心开发者、Python Unicode 支持的设计者和实现者: Marc-André Lemburg,他在一个邮件列表上的回复:
The only supported default encodings in Python are:
Python 2.x: ASCII
Python 3.x: UTF-8If you change these, you are on your own and strange things will
start to happen. The default encoding does not only affect
the translation between Python and the outside world, but also
all internal conversions between 8-bit strings and Unicode.Hacks like what's happening in the pango module (setting the
default encoding to 'utf-8' by reloading the site module in
order to get the sys.setdefaultencoding() API back) are just
downright wrong and will cause serious problems since Unicode
objects cache their default encoded representation.Please don't enable the use of a locale based default encoding.
If all you want to achieve is getting the encodings of
stdout and stdin correctly setup for pipes, you should
instead change the .encoding attribute of those (only).--
Marc-Andre Lemburg
eGenix.com
从此可见,Python 2 唯一支持的内部编码只有 ASCII,更改其默认编码为其它编码可能会导致各种各样奇怪的问题。在这里他也说了使用 sys.setdefaultencoding()
的方法是彻彻底底的错误,正确的方法应该是更改 stdout 和 stdin 的编码。
所以这个方法是最不正确的填坑方法,请大家慎用。
然后说说应当是姿势最正确的、也是大家都懂的方法:
在 print
的时候显式地用正确的编码来对 unicode 类型的字符串进行 encode('正确的编码')
为 str 后, 再进行输出。
而在 print
的时候,这个正确的编码一般就是 sys.stdout.encoding
的值。但也正如上述所说,这个值并不是一直是可靠的,因此需要根据所使用的平台和控制台环境来判断出这个正确的编码。
而在 Mac 下这个正确的编码一般都是 utf-8,因此若不考虑跨环境的话,可以无脑地一直用 encode('utf-8') 和 decode('utf-8') 来进行输入输出转换。
在我的经验中,这个策略也是解决 Python 2 其它 unicode 相关编码问题的最佳方法。在 PyCon 2012 的一个演讲中(关于 Python Unicode 问题很好的一个演讲,这里有演讲稿的中文翻译版),对这个方法有一个很形象的比喻:
因为在程序中进进出出的只有存储 bytes(字节序列)的 str。因此最好的策略是将输入的 bytes 马上解码成 unicode,而在程序内部中均使用 unicode,而当在进行输出的时候,尽早将之编码成 bytes。
也就是要形成一个 Unicode 三明治(如图), bytes 在外, Unicode 在内。在边界的地方尽早进行 decode
和 encode
。不要在内部混用 str 和 unicode,尽可能地让程序处理的字符串都为 Unicode。
虽然解决方法 2 是最正确的方式,但是有时候在 Sublime Text 里调试些小脚本,实在是懒得再在每个 print
语句后面写一个尾巴 .encode('utf-8')
。那么有没有办法能让 Sublime Text 像在终端里一样直接就能 print u'中文'
呢?也就是说能不能解决 sys.stdin/stdout/stderr.encoding
为 None
的情况呢?
答案肯定是有的,一种方法是用类似更改默认编码的方法一样,用 dirty hack 的方式在 Python 代码中去显式地更改 sys.stdin/stdout/stderr.encoding
的值。一样是不推荐,我也没尝试过,在这里就不详说了。
另一种方法则是通过设置 PYTHONIOENCODING
环境变量来强制要求 Python 设置 stdin/stdout/stderr 的编码值为我们想要的,这是一个相对比较干净的解决方法。见文档:
PYTHONIOENCODING
Overrides the encoding used for stdin/stdout/stderr, in the syntax encodingname:errorhandler. The :errorhandler part is optional and has the same meaning as in str.encode().New in version 2.6.
在 Mac 下对全局 GUI 程序设置环境变量的方法是:使用 launchctl setenv <<key> <value>, ...>
命令对所有 launchd 启动的未来子进程设置环境变量。
在这里顺便科普下,为什么对所有 launchd 启动的未来子进程设置环境变量可以使得对 Mac 下所有 GUI 程序生效。这是因为 launchd 是 OS X 系统启动后运行的第一个非内核进程。我们可以在 activity monitor(活动监视器)里看到,它的 pid 是很帅气的 1。而之后所有的进程都将是它的子进程。
另外还可以通过 launchd 在 Mac 下实现类 crontab 的功能。
launchctl setenv
命令设置的全局环境变量会在电脑重启后失效,因此就需要通过上面说的 launchd 的开机启动任务的功能来在重启后再设置一遍环境变量,其配置方法可以参考这里。也因为这个原因,我并没有使用这个方法来设置 PYTHONIOENCODING
环境变量。
而 Sublime Text 提供了一个设置 Build System 环境变量的方法,这个方法各平台的 Sublime Text 都适用。
设置 Sublime Text 的 Python Build System 环境变量的步骤如下:
PYTHONIOENCODING
环境变量为 UTF-8 编码的内容,并保存:{
"shell_cmd": "python -u \"$file\"",
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+ "env": {"PYTHONIOENCODING": "utf8"},
"selector": "source.python"
}
这样一来终于在这么长的文章后能在 Sublime Text 里直接运行 print u'中文'
,而不用再出现万恶的 UnicodeEncodeError
了。
既然都研究到这了,不妨我们试试把 PYTHONIOENCODING
设置成其它编码看看会出现什么情况,例如设置成简体中文 Windows 的默认编码 cp936:"env": {"PYTHONIOENCODING": "cp936"}
import sys
print sys.stdout.encoding
print u'你好'
----------------------------------"""
cp936
[Decode error - output not utf-8]
[Finished in 0.1s]
[Decode error - output not utf-8]
,这就是 Sublime Text 在 Windows 下可能会出现的问题(例如这两位同学 /t/45391 /t/88428 )。这是因为 Sublime Text 的 Build System 默认是用 utf-8 编码去解读运行的输出的,而我们指定了让 Python 用 cp936 编码来生成 str 字符串进行输出,那么就会出现 Sublime Text 无法识别输出的情况了。
同样在对终端 export PYTHONIOENCODING=cp936
后,在终端下 print u'你好'
输出的就会是 ���
这样的乱码。
解决办法之一就是同样在 Python.sublime-build 文件里设置 "env": {"PYTHONIOENCODING": "utf8"}
来使得输出统一为 utf-8。
或者是更改 Sublime Text 的 Build System 所接受的输出编码,将其改为一致的 cp936 编码,同样也是更改 Python.sublime-build 文件,加入一行:
{
"shell_cmd": "python -u \"$file\"",
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+ "encoding": "cp936",
"selector": "source.python"
}
那我们再试试把这两个设置同时都加到 Python.sublime-build 文件里,也就是让 Python 输出 utf8 编码的字符串,而让 Sublime Text 用 cp936 编码来解读,看看会发生什么情况?
{
"shell_cmd": "python -u \"$file\"",
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
+ "env": {"PYTHONIOENCODING": "utf8"},
+ "encoding": "cp936",
"selector": "source.python"
}
print u'你好'
----------------------"""
浣犲ソ
[Finished in 0.1s]
笑,居然不是 [Decode error - output not cp936]
,而是这么喜感的 “浣犲ソ”
!
这是因为 “你好”
的 utf-8 编码刚好和 “浣犲ソ”
的 cp936 编码重合了,都是 '\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd'
,所以使用 cp936 编码去解读的 Sublime Text 就认为这段字符串就是 “浣犲ソ”
而显示了出来。
>>> print repr('浣犲ソ') # cp936 编码
'\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd'
>>> print repr(u'你好'.encode('utf-8')) # utf-8 编码
'\xe6\xb5\xa3\xe7\x8a\xb2\xe3\x82\xbd'
我偶尔会用 Python 2 自带的 IDLE 快速测试一两行代码,但在我的 Mac 下的 IDLE 交互模式里输入中文会出现报错:
>>> '中文'
Unsupported characters in input
这个问题在 v2ex 上同样有同学问过: /t/44975 ,而他是在 Windows 下出现的,所以这个问题可能是普遍的。我原本以为这个问题同样是因为上述的 stdin/stdout/stderr 的编码问题而造成,就想顺便解决掉。然而即使设置全局环境变量 PYTHONIOENCODING
为 utf-8 后仍旧不管用,IDLE 里输入中文还是会报错,sys.stdin.encoding
编码还依旧是 us-ascii。
后来搜索后发现,貌似这个问题是由 IDLE 输入输出的内部实现机制导致的,可能跟 stdin/stdout/stderr 没有关系。根据这里所说,IDLE 的交互模式下会根据机子的本地语言环境设置来判断编码,再用其对输入进行转换后再执行,而在我的 Mac 下这个编码是 ascii,所以导致了 Unsupported characters in input
。
而我搜到了一个可行的解决方法,其通过在 IDLE 的 IO 相关源码(lib/python2.7/idlelib/IOBinding.py)中插入一行代码强行覆盖变量 encoding
的值为 'utf-8'
来解决这个问题。
不过后来经过我测试后发现,在 Mac 下其实更为简单的一个解决方法是,设置 IDLE 的环境变量 LANG
为 "en_US.UTF-8"
。同样我不想通过 launchctl 设置全局环境变量来解决,而我采用的解决方法是:
LANG
的语句:+ os.environ["LANG"] = "en_US.UTF-8" # 第 24 行
os.environ["PYTHONEXECUTABLE"] = executable
os.environ["DYLD_LIBRARY_PATH"] = libdir
保存文件,重新打开 IDLE 就可以在其交互模式里输入中文了。
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.