我們在使用其他語言的庫做編碼轉換時,對于無法理解的字符,通常的處理也只有兩種(或三種):
- 拋異常
- 替換成替代字符
- 跳過
但是在復雜的現實世界中,由于各種不靠譜,我們處理的文本總會出現那么些不和諧因素,比如混合編碼。在這種情況下,又回到了上面的處理辦法。
那么問題來了,python有沒有更好地辦法呢?
答案是,有!
python的編碼轉換流程實際上是兩段式轉換:
1
|
source - > unicode - > dest |
首先將字符串從原始編碼轉換成unicode。再將unicode轉換成目標編碼。
第一步我們一般采用decode()或者 unicode() 這兩個函數完成。
第二步我們使用encode()函數完成。
在這里我們說的黑魔法就是在第一步實現。
decode和unicode函數都有一個叫做errors的可選參數。看看官方的描述:
- errors may be given to set a different error
- handling scheme. Default is 'strict' meaning that encoding errors raise
- a UnicodeDecodeError. Other possible values are 'ignore' and 'replace'
- as well as any other name registered with codecs. register_error that is
- able to handle UnicodeDecodeErrors.
這個參數通常有三種值:
- strict 默認值。如果出現編碼錯誤,則會拋出UnicodeDecodeError。
- ignore 跳過。
- replace 用?替換。
好了,看到最后一句話了嗎?好戲上演了!
模塊codec有一個函數叫做register_error。他的作用讓用戶可以注冊自定義的errors處理方法。
用來處理UnicodeDecodeError。
我們看看函數原型:
1
|
codecs.register_error(name, error_handler) |
name: 錯誤處理的名稱。用以填寫在decode函數的error參數中。
error_handler: 處理函數。該函數接受一個異常參數。
返回一個tuple,該tuple有2個元素,第一個是糾錯后的字符串,第二個是繼續decode的起始位置
有了上面的基本概念。我們看下具體實現:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
def cjk_error(e): if not isinstance (e, UnicodeDecodeError): raise TypeError( "don't know how to handle %r" % exc) if exc.end + 1 > len (exc. object ): raise TypeError( 'unknown codec ,the object too short!' ) ch1 = ord (exc. object [exc.start:exc.end]) newpos = exc.end + 1 ch2 = ord (exc. object [exc.start + 1 :newpos]) sk = exc. object [exc.start:newpos] if 0x81 < = ch1< = 0xFE and ( 0x40 < = ch2< = 0x7E or 0x7E < = ch2< = 0xFE ): # GBK return ( unicode (sk, 'cp936' ), newpos) if 0x81 < = ch1< = 0xFE and ( 0x40 < = ch2< = 0x7E or 0xA1 < = ch2< = 0xFE ): # BIG5 return ( unicode (sk, 'big5' ), newpos) raise TypeError( 'unknown codec !' ) codecs.register_error( "cjk_replace" , cjk_replace) |
上面這個是我從網上copy的。開始我覺得很不錯,但是后來發現是個很不經推敲的算法。
比如utf8和gbk在前兩個字節就有交集的部分。當一個utf8的字符串以gbk編碼decode的時候,出現錯誤是從第三個字節開始(前兩個字節也能夠在gbk編碼范圍中對應到一個漢字)。
如:
1
2
3
|
a = "你" # utf8編碼:'\xe4\xbd\xa0' c = unicode (a[: 2 ], 'gbk' ) # 正常返回 c = unicode (a, 'gbk' ) # UnicodeDecodeError 。錯誤發生在第三個字節 |
所以針對這種情況,做了下改進:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
import codec def cjk_replace(e): if not isinstance (e, UnicodeDecodeError): raise TypeError( "invalid exception type %s" e) src = e.encoding if src in ( 'gbk' , 'gb18030' , 'big5' ): beg = e.start - 2 if beg > = 0 : try : return unicode (e. object [beg:e.end], 'utf8' ), e.end + 1 except : pass if exc.end + 1 > len (exc. object ): raise TypeError( 'unknown codec ,the object too short!' ) ch1 = ord (exc. object [exc.start:exc.end]) newpos = exc.end + 1 ch2 = ord (exc. object [exc.start + 1 :newpos]) sk = exc. object [exc.start:newpos] if src ! = 'gbk' and 0x81 < = ch1< = 0xFE and ( 0x40 < = ch2< = 0x7E or 0x7E < = ch2< = 0xFE ): # GBK return ( unicode (sk, 'cp936' ), newpos) if src ! = 'big5' and 0x81 < = ch1< = 0xFE and ( 0x40 < = ch2< = 0x7E or 0xA1 < = ch2< = 0xFE ): # BIG5 return ( unicode (sk, 'big5' ), newpos) raise TypeError( 'unknown codec !' ) codecs.register_error( "cjk_replace" , cjk_replace) |
當然,這個邏輯其實還是不夠嚴謹的。雖然對于這種混合編碼這種畸形活處理有點較真兒。
不過既然python提供這樣的能力,大家可以一起來討論下,我們怎么可以做的更好?