python unicode 和string那些事

开发过程中总是会碰到string, unicode, ASCII, 中文字符等编码的问题, 每次碰到都要现搜, 很是浪费时间, 于是这次狠下心, 一定要搞清楚python 的string和Unicode到底怎么回事.

我们都知道计算机只认0和1, 要想在计算机显示26个字母, 就要给他们一套映射规则: 计算机能认得的符号 --> 人类可读的符号. 这转换的过程就是一套编码规则.

  • 字符集: 就是一套字符的集合(比如中文4000个汉字集合)
  • 字符编码: 一套法则, 能够将0/1和人类的语言之间进行转换的法则
  • 最初字符集比较少, ASCII 码就够用了(一些控制符和26个字母), 随着计算机的发展, 各国语言都有自己独特的编码, 汉字的编码也不断地扩展, 从GBK到 GB18030/DBCS. 这个时候Unicode应运而生.

    Unicode就是为了统一各国各地区的编码规则, 重新搞了一套包罗地球上所有文化, 符号的字符集! Unicode没有编码规则, 只是一套包含全世界符号的字符集. Unicode也不完美, 于是后续有了众多UTF编码(UTF-8, UTF-16).

    总之搞清楚一件事情, 一个字符用了UTF-8编码的, 就要用UTF-8去解码, 不然就会出现乱码.

    在python-2.x, 处理文本时, 有string和unicode两种类型

    str 类型就是一串bytes, 这种类型跟C语言中处理string是非常相似的 unicode 就是一串unicode的数字映射( code point ), 用于映射某个字符与一个unicode的对应关系.

    看看代码出来是如何的:

    >>> a = "简书"
    >>> type(a)
    <type 'str'>
    '\xe7\xae\x80\xe4\xb9\xa6'
    >>> print a
    >>> u = u"简书"
    >>> type(u)
    <type 'unicode'>
    u'\u7b80\u4e66'
    >>> print u
    

    从上面的代码可以看到, a = "简书"是string类型, 可以看到a是一串 '\xe7\xae\x80\xe4\xb9\xa6' byte字符, 而u = u"简书"是一串\uxxxx的unicode数字, 通过print aprint u可以显示出中文字符.

    常见问题#1

    大家经常犯的一个错误就是混淆了unicode以及通过unicode编码存储在string里面的类型.
    比如上面的例子中 u'\u7b80' 是unicode, '\xe7\xae\x80'是byte string, byte和unicode之间一一对应, 可以相互转换, 转换规则如下:

    >>> '\xe7\xae\x80'.decode('utf-8')
    u'\u7b80'
    >>> print '\xe7\xae\x80'.decode('utf-8')
    >>> u'\u7b80'.encode('utf-8')
    '\xe7\xae\x80'
    >>> print u'\u7b80'.encode('utf-8')
    

    总结一下, 上面例子中

  • unicode和byte都指
  • byte string 里面存储的是unicode通过utf-8编码后得到的bytes
  • 所以byte string解码(decode)后即可得到unicode
  • unicode是byte string通过utf-8解码后得到的
  • unicode用utf-8编码(encode)可以得到对应的bytes
  • Note: 
    总而言之 Unicode ------编码------> byte string
            Unicode <-----解码------- byte string
    

    Unicode就像是加密传输中的明文, 可以用UTF-8, UTF-16, UTF-7, UTF-32等对unicode进行加密, 最后解密还是要用回原本的加密方式来解密, 不然就解出乱码啦.

    常见问题#2

    对unicode或者byte string编码解码方向搞错

    >>> u'\u7b80'.decode('utf-8')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "/Users/serena/Documents/data-pipeline/data-ci-sqlbuffet-env/lib/python2.7/encodings/utf_8.py", line 16, in decode
        return codecs.utf_8_decode(input, errors, True)
    UnicodeEncodeError: 'ascii' codec can't encode character u'\u7b80' in position 0: ordinal not in range(128)
    >>> '\xe7\xae\x80'.encode('utf-8')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeDecodeError: 'ascii' codec can't decode byte 0xe7 in position 0: ordinal not in range(128)
    

    unicode应该是进行编码的, 如果进行decode, 是会出现UnicodeEncodeError 异常的. bytes string同理, 应该进行解码, 如果硬要进行编码的话, 则抛出UnicodeDecodeError

    常见问题#3

    API调用不一致的问题. 在调用别人的API的时候, 需要看清楚是传unicode还是byte string作为参数. 因为第三方的API有的是支持unicode, 有的是byte string, 甚至有的两种类型都支持. 这个时候要清楚自己传进去的参数是什么, 比如一些变量值是从http requests里面拉过来的, 这个时候你获得的变量值很有可能是unicode类型(python requests get/post把返回值都转成了unicode), 而如果第三方的API需要byte string, name就需要自己判断一下并进行转换. 否则就会出现各种奇怪的UnicodeError

    虽然python 社区规定了在所有的API中使用unicode, 但是少数一部分的API处于安全考虑还是要求使用byte string. 需要注意一下.

    常见问题#4

    输出类型不一致.
    既然python社区推动到处使用unicode, 那么我们只要在开发过程中全部都转成unicode是不是就万事大吉了? 并不是, 当你要输出文本到terminal或者到文件, 这个文本必须是byte string类型的.
    如果不是的话, python会隐式地帮你将unicode转成string, python默认采用ascii编码,而中文编码不在ascii编码能够表示的范围之内,所以string无法将“你好”作为ascii编码保存为str类型。

    >>> string = unicode('你好', 'utf8')
    >>> print string
    >>> log = open('/var/tmp/debug.log', 'w')
    >>> log.write(string)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-1: ordinal not in range(128)
    

    所以当你需要输出的时候, 需要将你的unicode转换成byte string再写文件, 如果有中文的话, 要用'utf-8'或'GBK'等支持中文的编码.

    >>> string.encode('utf-8')
    

    python 2.x的unicode & str其实搞清楚之后来来回回就是那些小问题, 希望对大家有帮助.

    Reference