2010年3月5日星期五

Python编程的字符编码与unicode问题

最近用python写了两个应用,不幸涉及到很多字符编码问题,在此总结一下。

首先,python编程中关于字符编码的控制主要有三个方面:
  1. System default encoding,也就是系统默认编码。如果国人在WinXP系统终端下运行脚本那一般就是 GBK 或 ascii 了。

  2. 在代码文件第一行加上coding声明:#-*- coding: xxxx -*- ,其中xxxx是字符编码。告诉解释器你这个文件的字符编码是什么,这样解释器在读取文件中声明的字符串时会用这种编码来解码,若未声明则使用系统默认编码解码。如果系统默认解码是GBK,而你的文件编码是utf8,内中含有非ascii字符的话就杯具了。当然另一方面你的文件编码也得确实是你声明的那个才行,如果编辑器将这个文件保存为GBK编码的你却在文件头声明utf8那也是不行的。

  3. unicode字符串。unicode与utf8不是一个东西,有兴趣的同学可以看看这篇文章
    我的理解是:
    • unicode是一个字符集,其中的字符能以不同的字节长度保存
    • utf8是对unicode的一种编码方式
    也就是说unicode字符串能够用GBK、utf8等方式编码输出。因此unicode是字符串在不同编码间转换的桥梁。
在python中,解释器到底以如何方式解读、输出文字的,最好都在代码中显式的规定好,以免出现各种奇怪的问题。常见的问题有:
  1. 系统默认编码与文件编码不一致,例如:
    #-*- coding: utf-8 -*-
    a = '你好'
    print a
    以上代码在一个简体中文XP系统终端下运行(并且安装的是python.org提供的官方包,以后的例子都是这个运行环境)会打印出乱码,原因是操作系统终端字符环境并非utf8。不过没蹦出异常就不错了。

  2. 用错误的编码解析字符串,例如:
    #-*- coding: utf-8 -*-
    a = '你好'
    print a.encode('gbk')
    上面这个例子中,a 是 utf8 编码的字符串,当程序要求以 GBK 对 a 进行再编码并打印时,解释器首先将 a 用系统默认编码“解码”转为 unicode(别问我为什么不用文件头声明的编码来解码...),接着再用 GBK 将解码后的 unicode 字符串“编码”后输出。因此,这个例子输出一个异常:
    Traceback (most recent call last):
      File "t.py", line 3, in
        print a.encode('gbk')
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe4 in position 0: ordinal not in range(128)
    其实这个例子跟上一个本身是同一种,只不过第一个例子中是系统尝试用错误的编码来解码不会蹦出异常而已。
  3. python内建函数的非预期行为。这应该是最让人哭的情况了。例如这个例子:
    #-*- coding: utf-8 -*-
    import sys
    reload(sys)
    sys.setdefaultencoding('utf-8')
    import base64
    a = u'''Hello:
       你好,我是
       Please remove this message after reading,
       and I hope this won't bother you for a long time
    '''
    print base64.encodestring(a)
    print '------------------------------------'
    print base64.encodestring(a.encode('utf-8'))
    首先注意黄色底的代码,这是显式地设置系统默认编码的一种方式,在这里我设为utf8。如果不设的话在执行
    print base64.encodestring(a)
    这一句时会蹦出第2个例子中的异常……(为什么既然系统已经尝试对 unicode 进行编码了,然而base64的加密结果还是不一样呢?而如果是不编码就加密,为什么后来还要进行一次莫名其妙的编码,导致蹦出了异常呢?哭了!)
    注意 a 是一个 unicode 字符串(注意等号后面,引号前面的u)。这段代码的输出是:
    SGVsbG86CiAgIOS9oOWlve+8jOaIkeaYrwogICBQbGVhc2UgcmVtb3Zl
    IHRoaXMgbWVzc2FnZSBhZnRlciByZWFkaQ==
    bmcsCiAgIGFuZCBJIGhvcGUgdGhpcyB3b24ndCBib3RoZXIgeW91IGZv
    ciBhIGxvbmcgdGltZQo=
    ------------------------------------
    SGVsbG86CiAgIOS9oOWlve+8jOaIkeaYrwogICBQbGVhc2UgcmVtb3Zl
    IHRoaXMgbWVzc2FnZSBh
    ZnRlciByZWFkaW5nLAogICBhbmQgSSBob3BlIHRoaXMgd29uJ3QgYm
    90aGVyIHlvdSBmb3IgYSBsb25nIHRpbWUK
    可以注意到,使用同一个函数对 unicode 的 a 和经utf8编码后的 a 进行 base64 加密,结果是不一样的。偏偏有的解码系统只支持第二种加密结果,杯具了。

  4. 自己指定的编码与系统有冲突。例如你在代码中 sys.setdefaultencoding('utf-8') 来指定系统默认编码为 utf8 ,然后你把这个程序放到中文路径下执行。在个别情况下(应该与所使用的库有关)解释器尝试以你设置的编码解析路径,结果出现异常。
上面的例子都是血的教训,浪费了我许多宝贵的时间。在编写代码时,我建议做到如下几条:
  1. 显式的指定系统编码,即在主干脚本中加入如第三个例子中黄色的部分的语句,或写在另一个文件中并 import 之。

  2. 使用 unicode 定义字符串,这样解释器就会使用文件头部制定的编码来解码你声明的字符串了。另外尽量将所有的外部输入转换为 unicode ,如文件读入,命令行参数等。程序内部尽量使用 unicode 形式保存字符串。

  3. 对 unicode 行为不明的函数,多做几个实验确定一下。除了我说的第三个例子外,还有一个特例就是当你将一个字典以 **kw 的方式作为参数传给函数时,字典中的key不能是unicode(>_<)
以上。

没有评论:

发表评论