我們通常見到的字符串編碼主要是三種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"你好"
'xc4xe3xbaxc3'
u"你好"
u'u4f60u597d'
type("你好")
type(u"你好")
其中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 "", line 1, in
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc4 in position 0: ordinal not in range(128)
>>> "你好".decode("gbk").encode("utf-8")
'xe4xbdxa0xe5xa5xbd'
Python2中字符串問題實錄
reload
在Python2中出現編碼問題時,很多人喜歡使用下面的語句來改變系統的默認編碼1
2
3import sys
(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
23def 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'xe3x80x8axe5',首先這外面套了個u,應該是Unicode,但是里面卻是x打頭的multi-bytes的形式,這往往是由于錯誤調用unicode()函數所致的。對此,python提供了raw_unicode_escape來解決這個問題
正則匹配
由于存在特殊符號的原因,使用正則匹配時宜使用Unicode而不是多字節匹配。但是以下的編碼在Win10和某些Linux發行版上都跑不了,但在MacOS和Ubuntu上能夠正常運行,去StackOverflow問了,他們認為這是一個Bug,所以暫時還是先用上面的方法。