前几天开始自学Python,这语言确实看上去很简练外加高度抽象,但是对Unicode字符串的处理简直要让人发疯。
长篇大论讲这个问题的在网上随便一搜“Python 中文”就有,我这里只想特别讲讲今天遇到的两个问题和解决方案。
2.X Python官方IDLE的BUG
2.X官方的IDLE有个很严重的BUG:即使你显式定义一个Unicode字符(准确地说是对象),他居然也会用系统ANSI编码来存储,而不是Unicode。
>>> import sys >>> import locale >>> sys.getdefaultencoding() 'ascii' >>> locale.getpreferredencoding() 'cp936' >>> s='中文' >>> s '\xd6\xd0\xce\xc4' >>> u=u'中文' >>> u u'\xd6\xd0\xce\xc4'
可以看到,我们的Unicode对象u,实际上却是用了GBK编码,而不是Unicode。len(u)也会因此变成4而不是2。更严重的后果是,你似乎无法还原输出这个字符串的字符本身:
>>> print s
中文
>>> print s.decode('gbk')
中文
>>> print u
ÖÐÎÄ
>>> print u.encode('utf8')
脰脨脦脛
>>> print u.encode('gbk')
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
print u.encode('gbk')
UnicodeEncodeError: 'gbk' codec can't encode character u'\xd6' in position 0: illegal multibyte sequence
可以看到,对于str类型、GBK编码的s可以直接输出,或者显式用GBK解码成Unicode对象后再输出。但是对于我们的u,理论上一个Unicode对象正确的做法是编码成本地locale(GBK)或者utf-8输出,但是很显然都不好使。
那么,既然我们前面说了u被错误地用GBK编码了,那么我们就把他当成str然后用GBK解码行不行呢?
>>> print u.decode('gbk')
Traceback (most recent call last):
File "<pyshell#17>", line 1, in <module>
print u.decode('gbk')
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-3: ordinal not in range(128)
答案是否定的。
值得注意的是,这个错误使用Python命令行并不会出现。输入的Unicode中文会逐字符(而不是逐字节)地正确存为Unicode字符串(所以结果是2个字符/对象),输出时既可直接输出(本质上还会被Python先编码成GBK,因为CMD是GBK的),或者自己手动编码成GBK再输出:
虽然这是IDLE独有的BUG。但是由于初学者会大量使用IDLE来进行测试,相信会对很多人造成困扰。事实上中文圈有很多文章都提到了IDLE这一BUG:文章1,文章2。
经过一番搜索,我发现这个BUG对应的报告应该是官方tracker上的issue15809。可怕的是,早在2012年就已经提出,居然过了3年都没有修复。不过幸运的是,已经有人做出patch,相信在不久的将来有修复的可能。
在这篇文章中还无意得知了在当下BUG的情况下的临时解决方案:
>>> u.encode('latin1')
'\xd6\xd0\xce\xc4'
>>> u
u'\xd6\xd0\xce\xc4'
>>> print u.encode('latin1')
中文
没错……就是先用Latin1编码把原代码完全一样地转换成完全对应的str类型,然后再输出(默认GBK解码)。为什么是Latin1?天知道。
用Sublime Text Build Python的编码问题
先说Python 2.x的情况。
其实Python 2.x下如果用控制台,输出个Unicode字符串是蛮简单的。
直接u=u’中文’然后print u就可以了。其实这种做法等效于print u.encode(‘gbk’)——因为Unicode对象存的是字符本身(这只是便于理解的说法,准确地说也是用UTF-16编码),得先编码成byte。而你用简体中文系统的CMD直接隐含了默认编码成gbk了。
但是在Sublime Text里一切就变得很复杂。
还是上面的代码原封不动:
u=u'中文' print u
输出:
SyntaxError: Non-ASCII character '\xe4' in file C:\Users\Administrator\Desktop\test2.py on line 1, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details
什么,你居然敢不声明文件的编码就让老子跑还夹杂非ACSII代码!是在下错了,毕竟不是console不能这么凑乎……老老实实最前面加上# -*- coding: utf-8 -*-:
结果:
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
这又是为什么?看报错信息可以看出,是python试图用ascii来编码我输入的“中文”,二字,很显然地失败了。但是为什么会用ascii去编码?经过一番搜索,在这篇文章里提到,这里编码的选择和sys.stdout.encoding这一环境变量有关。在控制台下,该值是cp936(GBK);但是在Sublime Text下,该值居然是None。
解决方法是上面提过的,把变量u显式编码成utf-8再输出:
# -*- coding: utf-8 -*-
u=u'中文'
print u.encode('utf-8')
这次终于成功输出“中文”二字了。不过为啥在控制台用gbk这里用utf-8?事实上是,你可以用gbk,但是结果就是编译不会出错但是输出结果是空白。应该是Sublime Text的result输出窗口只支持utf-8码所致。同理,你也可以在控制台里编码成utf-8输出,只是显示出来是乱码而已(因为控制台的是GBK)。
说完2.X+Submine Text的解决方案,再来说说3.X。由于Python 2.X的Unicode支持就是一笔糊涂账,我想了想干脆换用3.X算了反正我也没啥包袱。结果上来就出问题了:
由于3.X默认的字符串就是Unicode的,也没必要再加u了。于是我在Sublime Text 3下随便试了个字符串输出
u='你好' print (u)
可以编译无问题,但是输出是空的?拿控制台和CMD都试了下,无法重现。看来又是Sublime Text的问题。按照上面的尿性先检查下sys.stdout.encoding:这次不再是None了,是cp936。但是还是不行啊我们上面说了Sublime Text只接受utf-8输出。那再用上面的老方法,把字符串手动编码成utf-8试试?
u='你好'
print(u.encode('utf-8'))
输出:
b'\xe4\xbd\xa0\xe5\xa5\xbd'
[Finished in 0.1s]
不妙,结果直接变成bytes了……这里需要厘清一个概念。Py2和3的print默认期望接受的类型是不一样的。在py2里由于str默认就是bytes,所以如果你输出的是一个Unicode类型的字符串,则需要自动(控制台下)或手动(sublime Text里)先编码成bytes。而这个byte最后又会被你的控制台或者别的什么东西再解码回字符输出(好绕)。py3里反过来了,默认str就是Unicode,所以期望接受一个没编码过的字符本体,如果你编码成byte他反而不理解了,直接把byte原封不动给你输出出来。那么既然我们无法再显式控制这编码成byte的过程,如何让python给我编码成utf-8呢?
答案是,手动修改Sublime Text的build system,修改相应的参数。默认的python build我们是不能用了,因为参数改不了。那么手动去Tools->Build System->New Build System.. 新建一个.sublime-build文件,内容写
{
"cmd": ["python", "-u", "$file"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"selector": "source.python",
"env": {"PYTHONIOENCODING": "UTF-8"}
}
前面几行是默认的。重点就是env这个参数,他让py把所有的标准输入输出接口的编码方式都改成utf-8。将这个build system保存之后(默认那个users文件夹就好),我们再看看sys.stdout.encoding,是不是就变成utf-8了?
现在,我们可以完美地直接输出字符串’中文’了。
除此之外,还有另外一个修改build system的办法,就是修改encoding参数:
{
"cmd": ["python", "-u", "$file"],
"file_regex": "^[ ]*File \"(...*?)\", line ([0-9]*)",
"selector": "source.python",
"encoding": "cp936"
}
和PYTHONIOENCODING不同,这里的encoding控制的是Sublime Text这边接口的编码,粗略可以理解成下方输出栏的解码方式。自然,只要这个和py那边输出的output的编码一致,自然也可以正确地显示出结果。
我个人还是推荐第一种方法,因为毕竟全Unicode的workflow的兼容性更好。另外提示一点,两条参数不能共用,否则结果又会变成乱码(想想为什么)。
顺便一提,在某些网站查到了一种修改env参数中的”LANG”为utf-8或者en_US.UTF-8,我这边并没有作用。不过可能对解决一些别的编码问题有帮助,可以参见此文的附带部分。
总而言之,Python的输出就是这么恶心,各种编码玩死你。一个字符串被翻来覆去编码解码好多回,每个流程都有可能出错。在这个Stackoverflow的答案中建议直接使用sys.stdout.buffer.write(data)或os.write(sys.stdout.fileno(), data)来输出数据(要先自行编码成bytes),绕开问题多多的print,也不失为一个好选择。
唉,这种时候就怀念全盘Unicode化的C#的好了。

