Python2字符编码

字符编码

我们通常见到的字符串编码主要是三种GB2312/GBK、Unicode、UTF-8。GB2312/GBK是多字节(multibytes)编码的一种,属于“ASCII的加强版”,与之平行的由Big5、ShiftJIS之类的编码各自为政,所有这些用两个字节表示汉字的多字节编码标准统称为ANSI编码,同样的汉字在不同的ASNI编码中的表示是不同的。为了避免这个问题,Unicode应运而生,将全世界所有的字符统一编码到一个定长的结构中。Unicode解决了统一编码的问题,但带来了新的问题。第一点,Unicode和ASCII不兼容了,这是因为ASCII只有一个字节,而这一个字节肯定装不下Unicode。第二点,用Unicode传输开销变大了,这是因为很多文档二十六个字母(1个字节)就能解决了,用Unicode多了很多冗余的字节。因此UTF-8应运而生。UTF-8对Unicode进行变长编码(我们可以想象下Huffman树),通常长度在1-4字节。目前Linux系统使用的是UTF-8编码,而Windows内部则是UTF-16LE/GBK编码。

Python2的字符串表示

Python2中有表示字符串有str和Unicode两种。其中一个str字面量由""表示,我们也可以用''或者"""这类括号括起,一个Unicode字面量由u""括起。

1
2
3
4
5
6
7
8
>>> "你好"
'\xc4\xe3\xba\xc3'
>>> u"你好"
u'\u4f60\u597d'
>>> type("你好")
<type 'str'>
>>> type(u"你好")
<type 'unicode'>

其中Unicode得益于ucs2/ucs4标准,在不同系统上都是固定的表示的。其中ucs2即Unicode16,比较常见,用2个字节(65536)来索引,一般表示是u"\uxxxx",ucs4即Unicode32,一般表示是u"\Uxxxxyyyy"在一些Python中也能见到。我们可以通过下面的代码来检测Python是哪一个

1
2
3
4
5
6
7
8
9
--enable-unicode=ucs4
>>> import sys
>>> print sys.maxunicode
1114111

--enable-unicode=ucs2
>>> import sys
>>> print sys.maxunicode
65535

str的表示取决于所在的系统,例如Linux是默认UTF8,上面的“你好”就会变为'/xe4/xbd/xa0/xe5/xa5/xbd',我们这里看到UTF8确实是一种字符的表示。

1
2
3
4
5
6
7
8
9
10
11
>>> "hello".encode("utf-8")
'hello'
>>> "hello".decode("gbk").encode("utf-8")
'hello'

>>> "你好".encode("utf-8")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128)
>>> "你好".decode("gbk").encode("utf-8")
'\xe4\xbd\xa0\xe5\xa5\xbd'

Python2中字符串问题实录

reload

在Python2中出现编码问题时,很多人喜欢使用下面的语句来改变系统的默认编码

1
2
3
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

这种策略通常用来解决下面这样的错误提示

UnicodeEncodeError: 'ascii' codec can't encode byte

现在ASCII码不能encode是吧,那我默认都用utf-8来encode总行了吧?但这样可能存在问题,博文中就举出了一个实例。对于多字节字符串str,我们之前默认用ASCII解码,要是解不开,就RE了。现在我们默认用utf-8解,utf-8阈值广,基本都能解开,不RE了,可是就不WA了么?样例中举出一个例子,一个latin-1编码的字符串,用utf-8解码不会报错,但解出来的结果并不对。因此在多字节编码规则不统一这个客观问题存在的情况下,不存在银弹。我们需要的是用chardet.detect来猜测编码。当猜测不出时我们只能不停地try,直到找到一个解码不报错的编码,虽然可能解出来还是乱码,因为可能一段byte串同时用utf-8、gbk、Big5等都能解码而不报错。
这里提供一个工具类,能够尽可能地猜测字节串所使用的编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def ultimate_decode(doc):
def try_decode(s, try_decoding):
try:
res = s.decode(try_decoding)
return True, res
except UnicodeDecodeError:
return False, ""
if isinstance(doc, str):
predicted = chardet.detect(doc)
print predicted
if predicted['encoding'] and predicted['confidence'] > 0.5:
doc = doc.decode(predicted['encoding'])
else:
encodeing_list = ["utf-8", "gbk", "Big5", "EUC-JP"]
for e in encodeing_list:
state, res = try_decode(doc, e)
if state:
doc = res
break
if not isinstance(doc, unicode):
return None

return doc

coding

当我们在Python代码文件中需要加入中文时,我们需要在文件开头加上这两行中的一行,不然即使用上前面的reload大法都不行。

1
2
# -*- coding:utf-8 -*-
# coding: utf8

这是用来指定Python代码文件所使用的编码方式。在Python2.1时,这个机制还没有引入,我们只能通过unicode-escape的方式输入。一个类似的做法是很多json库dump的结果中常出现\u打头的unicode-escape字符串,这是非常正常的现象。这样json库可以省事地避免编码问题,因为这样json文件现在都是ASCII码了。

伪装成Unicode的多字节

有时候我们会看到这种东西u'\xe3\x80\x8a\xe5',首先这外面套了个u,应该是Unicode,但是里面却是\x打头的multi-bytes的形式,这往往是由于错误调用unicode()函数所致的。对此,python提供了raw_unicode_escape来解决这个问题

正则匹配

由于存在特殊符号的原因,使用正则匹配时宜使用Unicode而不是多字节匹配。但是以下的编码在Win10和某些Linux发行版上都跑不了,但在MacOS和Ubuntu上能够正常运行,去StackOverflow问了,他们认为这是一个Bug,所以暂时还是先用上面的方法。