Python vs. Unicode:两个Python下的输出Unicode字符问题的解决方案

前几天开始自学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再输出:

QQ截图20150816202253

虽然这是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#的好了。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s