# Python2字符编码问题小结 [Python docs - Unicode HOWTO](https://docs.python.org/2/howto/unicode.html) [Python docs - Built-in Types](https://docs.python.org/2/library/stdtypes.html) [Stack Overflow - Why does Python print unicode characters when the default encoding is ASCII?](http://stackoverflow.com/questions/2596714/why-does-python-print-unicode-characters-when-the-default-encoding-is-ascii) ## 理论 ### 编码中的Unicode和UTF-8 `Unicode`是字符集,`UTF-8`是`Unicode`的一种编码方式,并列的还包括`UTF-16`、`UTF-32`等。 某个字符的`Unicode`通过查询标准得到,其`UTF-8`编码由`Unicode`码计算得到。 ### Python2中的str和unicode `str`和`unicode`是两个不同的类。 `str`存储的是已经编码后的字节序列,输出时看到每个字节用16进制表示,以`\x`开头。每个汉字会占用3个字节的长度。 >>> a = '啊哈哈' >>> type(a) <type 'str'> >>> a '\xe5\x95\x8a\xe5\x93\x88\xe5\x93\x88' >>> len(a) 9 >>> a[2] '\x8a' `unicode`是“字符”串,存储的是编码前的字符,输出是看到字符以`\u`开头。每个汉字占用一个长度。定义一个`Unicode`对象时,以`u` 开头。 >>> b = u'哟呵呵' >>> type(b) <type 'unicode'> >>> b u'\u54df\u5475\u5475' >>> len(b) 3 >>> b[2] u'\u5475' `str`可以通过`decode()`方法转化为`unicode`对象,参数指明编码方式。 >>> a.decode('utf-8') u'\u554a\u54c8\u54c8' `unicode`可以通过`encode()`方法转化为`str`对象,参数指明编码方式。 >>> b.encode('utf-8') '\xe5\x93\x9f\xe5\x91\xb5\xe5\x91\xb5' ### 默认编码 Python2中的默认编码,有多个不同的变量。 1. 代码文件开头的`coding` # -*- coding: utf-8 -*- 或 # coding=utf-8 指明代码文件中的字符编码,用于代码文件中出现中文的情况。 % cat hello.py #! /usr/bin/env python # coding=utf-8 print '泥壕' % python hello.py 泥壕 如果不设置,默认是`ascii`,当出现中文字符时就不能正常识别。 % cat hello.py #! /usr/bin/env python print '泥壕' % python hello.py File "hello.py", line 2 SyntaxError: Non-ASCII character '\xe6' in file hello.py on line 2, but no encoding declared; see http://python.org/dev/peps/pep-0263/ for details 2. `sys.stdin.encoding`和`sys.stdout.encoding` `sdtin`和`stdout`输入输出使用的编码,包命令行参数和`print`输出,由`locale`环境变量决定。 在`en_US.UTF-8`的系统中,默认值是`UTF-8`。 3. `sys.getdefaultencoding()` 文件读写和字符串处理等操作使用的默认编码。 默认值是`ascii`。 ## 字符串拼接 `unicode`和`str`类型通过`+`拼接时,输出结果是`unicode`类型,相当于先将`str`类型的字符串通过`decode()`方法解码成`unicode`,再拼接。此时如果解码时没有明确指明编码类型,可能会出现错误。 >>> a = '啊哈哈' >>> b = u'哟呵呵' >>> >>> a + b Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0: ordinal not in range(128) >>> >>> a.decode('utf-8') + b u'\u554a\u54c8\u54c8\u54df\u5475\u5475' 错误提到`'ascii' codec can't decode byte 0xe5`,这是因为自动将`str`类型的变量按照默认的编码格式`sys.getdefaultencoding()`来解码,默认编码即`ascii`,而这个字符不在`ascii`的范围内,就出现了错误。 >>> import sys >>> reload(sys) <module 'sys' (built-in)> >>> sys.setdefaultencoding('utf-8') >>> >>> a = '啊哈哈' >>> b = u'哟呵呵' >>> a + b u'\u554a\u54c8\u54c8\u54df\u5475\u5475' ## 文件读取和json解析 读文件得到的结果是`str`类型,以`\x`开头的十六进制表示。 >>> f = open('t.txt') >>> a = f.read() >>> a '{"hello":"\xe5\x92\xa9"}\n' 而经过json解析后会自动转为`unicode`。 >>> json.loads(a) {u'hello': u'\u54a9'} ## 输出 ### 输出到文件 `str`类型可以输出到文件,而`unicode`类型必须先编码成`str`。 >>> a = '啊哈哈' >>> b = u'哟呵呵' >>> a '\xe5\x95\x8a\xe5\x93\x88\xe5\x93\x88' >>> b u'\u54df\u5475\u5475' >>> >>> f = open('t.txt', 'w') >>> f.write(a) >>> f.write(b) Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128) >>> f.write(b.encode('utf-8')) `unicode`输出到文件时的错误是由于默认编码为`ascii`,无法自动完成编码过程。如果将`sys.getdefaultencoding()`编码设置成了`utf-8`就可以自动完成转换过程了。 >>> import sys >>> reload(sys) <module 'sys' (built-in)> >>> sys.setdefaultencoding('utf-8') >>> >>> f.write(b) ### 计算md5 同样,md5计算也要求输入的`unicode`先编码。 >>> a = '啊哈哈' >>> b = u'哟呵呵' >>> import hashlib >>> hashlib.md5(a).hexdigest() 'f38b302e2993ec3fdad79c4d76074b21' >>> hashlib.md5(b).hexdigest() Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-2: ordinal not in range(128) >>> hashlib.md5(b.encode('utf-8')).hexdigest() 'c02dc06719bafeaf60505b11d3c0c90a' ### 输出到stdout 输出到`stdout`时,默认编码是`sys.stdout.encoding`,默认值取决于系统环境变量,所以`print`输出汉字时才可以不用指定`utf-8`。 >>> import sys >>> sys.stdout.encoding 'UTF-8' >>> print u'\u54a9' 咩 而在`zh_CN.GB2312`的环境中,默认值不是`utf-8`,就不能正常输出了。 >>> import sys >>> sys.stdout.encoding 'ANSI_X3.4-1968' >>> print u'\u54a9' Traceback (most recent call last): File "<stdin>", line 1, in <module> UnicodeEncodeError: 'ascii' codec can't encode character u'\u54a9' in position 0: ordinal not in range(128) ## 命令行参数读取 通过`sys.argv`或`argparse`得到的命令行参数都是编码后的`str`类型,以`\x`开头的十六进制表示。可以通过`sys.stdin.encoding`得到命令行传入的编码类型,解码成`unicode`。 #! /usr/bin/env python # coding = utf-8 import sys print repr(sys.argv[1]) print sys.stdin.encoding print repr(sys.argv[1].decode(sys.stdin.encoding)) 输出结果。 ~/workspace % python hello.py "哇嘿嘿" '\xe5\x93\x87\xe5\x98\xbf\xe5\x98\xbf' UTF-8 u'\u54c7\u563f\u563f' 如果命令行环境已经改成`GB2312`等其他编码,python找不到与之匹配的编码类型,就会将默认编码`sys.stdin.encoding`设置成`ascii`,无法通过这种方法正常解码成`unicode`。 ## 带\u的字符串转unicode 可能会遇到汉字被转换成unicode编码的形式表示的情况,即一个汉字被表示成了`\u????`的形式。 >>> a = u'咩' >>> a u'\u54a9' >>> b = '\u54a9' >>> b '\\u54a9' 上述`b`就是这样的情况。此时`b`是一个长度为6的字符串,而不是一个汉字。 要把`b`表示为汉字编码有两种方法。 1. unicode-escape编码。 >>> unicode(b, 'unicode-escape') u'\u54a9' 或 >>> b.decode('unicode-escape') u'\u54a9' 2. eval拼接。 >>> eval('u"' + b.replace('"', r'\"')+'"') u'\u54a9'