你的位置: Home ‣ Dive Into Python 3 ‣
難度等級: ♦♦♦♦♦
chardet移植到Python 3❝ Words, words. They’re all we have to go on. ❞
— Rosencrantz and Guildenstern are Dead
未知的或者不正確的字符編碼是因特網上無效數據(gibberish text)的頭號起因。在第3章,我們討論過字符編碼的歷史,還有Unicode的產生,“一個能處理所有情況的大塊頭。”如果在網絡上不再存在亂碼這回事,我會愛上她的…因為所有的編輯系統(authoring system)保存有精確的編碼信息,所有的傳輸協議都支持Unicode,所有處理文本的系統在執行編碼間轉換的時候都可以保持高度精確。
我也會喜歡pony。
Unicode pony。
Unipony也行。
這一章我會處理編碼的自動檢測。
⁂
它是指當面對一串不知道編碼信息的字節流的時候,嘗試著確定一種編碼方式以使我們能夠讀懂其中的文本內容。它就像我們沒有解密鑰匙的時候,嘗試破解出編碼。
通常來說,是的,不可能。但是,有一些編碼方式為特定的語言做了優化,而語言并非隨機存在的。有一些字符序列在某種語言中總是會出現,而其他一些序列對該語言來說則毫無意義。一個熟練掌握英語的人翻開報紙,然后發現“txzqJv 2!dasd0a QqdKjvz”這樣一些序列,他會馬上意識到這不是英語(即使它完全由英語中的字母組成)。通過研究許多具有“代表性(typical)”的文本,計算機算法可以模擬人的這種對語言的感知,并且對一段文本的語言做出啟發性的猜測。
換句話說就是,檢測編碼信息就是檢測語言的類型,并輔之一些額外信息,比如每種語言通常會使用哪些編碼方式。
結果證明,是的,它存在。所有主流的瀏覽器都有字符編碼自動檢測的功能,因為因特網上總是充斥著大量缺乏編碼信息的頁面。Mozilla Firefox包含有一個自動檢測字符編碼的庫,它是開源的。我將它導入到了Python 2,并且取綽號為chardet模塊。這一章中,我會帶領你一步一步地將chardet模塊從Python 2移植到Python 3。
⁂
chardet模塊在開始代碼移植之前,如果我們能理解代碼是如何工作的這將非常有幫助!以下是一個簡明地關于chardet模塊代碼結構的手冊。chardet庫太大,不可能都放在這兒,但是你可以從chardet.feedparser.org下載它。
universaldetector.py是檢測算法的主入口點,它包含一個類,即UniversalDetector。(可能你會認為入口點是chardet/__init__.py中的detect函數,但是它只是一個便捷的包裝方法,它會創建UniversalDetector對象,調用對象的方法,然后返回其結果。)
UniversalDetector共處理5類編碼方式:
如果文本以BOM標記打頭,我們可以合理地假設它使用了UTF-8,UTF-16或者UTF-32編碼。(BOM會告訴我們是其中哪一種,這就是它的功能。)這個過程在UniversalDetector中完成,并且不需要深入處理,會非常快地返回其結果。
如果文本包含有可識別的能指示出某種轉義編碼的轉義序列,UniversalDetector會創建一個EscCharSetProber對象(在escprober.py中定義),然后以該文本調用它。
EscCharSetProber會根據HZ-GB-2312,ISO-2022-CN,ISO-2022-JP,和ISO-2022-KR(在escsm.py中定義)來創建一系列的狀態機(state machine)。EscCharSetProber將文本一次一個字節地輸入到這些狀態機中。如果某一個狀態機最終唯一地確定了字符編碼,EscCharSetProber迅速地將該有效結果返回給UniversalDetector,然后UniversalDetector將其返回給調用者。如果某一狀態機進入了非法序列,它會被放棄,然后使用其他的狀態機繼續處理。
假設沒有BOM標記,UniversalDetector會檢測該文本是否包含任何高位字符(high-bit character)。如果有的話,它會創建一系列的“探測器(probers)”,檢測這段廣西是否使用多字節編碼,單字節編碼,或者作為最后的手段,是否為windows-1252編碼。
這里的多字節編碼探測器,即MBCSGroupProber(在mbcsgroupprober.py中定義),實際上是一個管理一組其他探測器的shell,它用來處理每種多字節編碼:Big5,GB2312,EUC-TW,EUC-KR,EUC-JP,SHIFT_JIS和UTF-8。MBCSGroupProber將文本作為每一個特定編碼探測器的輸入,并且檢測其結果。如果某個探測器報告說它發現了一個非法的字節序列,那么該探測器則會被放棄,不再進一步處理(因此,換句話說就是,任何對UniversalDetector.feed()的子調用都會忽略那個探測器)。如果某一探測器報告說它有足夠理由確信找到了正確的字符編碼,那么MBCSGroupProber會將這個好消息傳遞給UniversalDetector,然后UniversalDetector將結果返回給調用者。
大多數的多字節編碼探測器從類MultiByteCharSetProber(定義在mbcharsetprober.py中)繼承而來,簡單地掛上合適的狀態機和分布分析器(distribution analyzer),然后讓MultiByteCharSetProber做剩余的工作。MultiByteCharSetProber將文本作為特定編碼狀態機的輸入,每次一個字節,尋找能夠指示出一個確定的正面或者負面結果的字節序列。同時,MultiByteCharSetProber會將文本作為特定編碼分布分析機的輸入。
分布分析機(在chardistribution.py中定義)使用特定語言的模型,此模型中的字符在該語言被使用得最頻繁。一旦MultiByteCharSetProber把足夠的文本給了分布分析機,它會根據其中頻繁使用字符的數目,字符的總數和特定語言的分配比(distribution ratio),來計算置信度(confidence rating)。如果置信度足夠高,MultiByteCharSetProber會將結果返回給MBCSGroupProber,然后由MBCSGroupProber返回給UniversalDetector,最后UniversalDetector將其返回給調用者。
對于日語來說檢測會更加困難。單字符的分布分析并不總能區別出EUC-JP和SHIFT_JIS,所以SJISProber(在sjisprober.py中定義)也使用雙字符的分布分析。SJISContextAnalysis和EUCJPContextAnalysis(都定義在jpcntx.py中,并且都從類JapaneseContextAnalysis中繼承)檢測文本中的平假名音節字符(Hiragana syllabary characher)的出現次數。一旦處理了足夠量的文本,它會返回一個置信度給SJISProber,SJISProber檢查兩個分析器的結果,然后將置信度高的那個返回給MBCSGroupProber。
單字節編碼的探測器,即SBCSGroupProber(定義在sbcsgroupprober.py中),也是一個管理一組其他探測器的shell,它會嘗試單字節編碼和語言的每種組合:windows-1251,KOI8-R,ISO-8859-5,MacCyrillic,IBM855,and IBM866(俄語);ISO-8859-7和windows-1253(希臘語);ISO-8859-5和windows-1251(保加利亞語);ISO-8859-2和windows-1250(匈牙利語);TIS-620(泰國語);windows-1255和ISO-8859-8(希伯來語)。
SBCSGroupProber將文本輸入給這些特定編碼+語言的探測器,然后檢測它們的返回值。這些探測器的實現為某一個類,即SingleByteCharSetProber(在sbcharsetprober.py中定義),它使用語言模型(language model)作為其參數。語言模型定義了典型文本中不同雙字符序列出現的頻度。SingleByteCharSetProber處理文本,統計出使用得最頻繁的雙字符序列。一旦處理了足夠多的文本,它會根據頻繁使用的序列的數目,字符總數和特定語言的分布系數來計算其置信度。
希伯來語被作為一種特殊的情況處理。如果在雙字符分布分析中,文本被認定為是希伯來語,HebrewProber(在hebrewprober.py中定義)會嘗試將其從Visual Hebrew(源文本一行一行地被“反向”存儲,然后一字不差地顯示出來,這樣就能從右到左的閱讀)和Logical Hebrew(源文本以閱讀的順序保存,在客戶端從右到左進行渲染)區別開來。因為有一些字符在兩種希伯來語中會以不同的方式編碼,這依賴于它們是出現在單詞的中間或者末尾,這樣我們可以合理的猜測源文本的存儲方向,然后返回合適的編碼方式(windows-1255對應Logical Hebrew,或者ISO-8859-8對應Visual Hebrew)。
windows-1252如果UniversalDetector在文本中檢測到一個高位字符,但是其他的多字節編碼探測器或者單字節編碼探測器都沒有返回一個足夠可靠的結果,它就會創建一個Latin1Prober對象(在latin1prober.py中定義),嘗試從中檢測以windows-1252方式編碼的英文文本。這種檢測存在其固有的不可靠性,因為在不同的編碼中,英文字符通常使用了相同的編碼方式。唯一一種區別能出windows-1252的方法是通過檢測常用的符號,比如彎引號(smart quotes),撇號(curly apostrophes),版權符號(copyright symbol)等這一類的符號。如果可能Latin1Prober會自動降低其置信度以使其他更精確的探測器檢出結果。
⁂
2to3我們將要開始移植chardet模塊到Python 3了。Python 3自帶了一個叫做2to3的實用腳本,它使用Python 2的源代碼作為輸入,然后盡其可能地將其轉換到Python 3的規范。某些情況下這很簡單 — 一個被重命名或者被移動到其他模塊中的函數 — 但是有些情況下,這個過程會變得非常復雜。想要了解所有它能做的事情,請參考附錄,使用2to3將代碼移植到Python 3。接下來,我們會首先運行一次2to3,將它作用在chardet模塊上,但是就如你即將看到的,在該自動化工具完成它的魔法表演后,仍然存在許多工作需要我們來收拾。
chardet包被分割為一些不同的文件,它們都放在同一個目錄下。2to3能夠立即處理多個文件:只需要將目錄名作為命令行參數傳遞給2to3,然后它會輪流處理每個文件。
C:\home\chardet> python c:\Python30\Tools\Scripts\2to3.py -w chardet\ RefactoringTool: Skipping implicit fixer: buffer RefactoringTool: Skipping implicit fixer: idioms RefactoringTool: Skipping implicit fixer: set_literal RefactoringTool: Skipping implicit fixer: ws_comma --- chardet\__init__.py (original) +++ chardet\__init__.py (refactored) @@ -18,7 +18,7 @@ __version__ = "1.0.1" def detect(aBuf):- import universaldetector+ from . import universaldetector u = universaldetector.UniversalDetector() u.reset() u.feed(aBuf) --- chardet\big5prober.py (original) +++ chardet\big5prober.py (refactored) @@ -25,10 +25,10 @@ # 02110-1301 USA ######################### END LICENSE BLOCK #########################-from mbcharsetprober import MultiByteCharSetProber-from codingstatemachine import CodingStateMachine-from chardistribution import Big5DistributionAnalysis-from mbcssm import Big5SMModel+from .mbcharsetprober import MultiByteCharSetProber +from .codingstatemachine import CodingStateMachine +from .chardistribution import Big5DistributionAnalysis +from .mbcssm import Big5SMModel class Big5Prober(MultiByteCharSetProber): def __init__(self): --- chardet\chardistribution.py (original) +++ chardet\chardistribution.py (refactored) @@ -25,12 +25,12 @@ # 02110-1301 USA ######################### END LICENSE BLOCK #########################-import constants-from euctwfreq import EUCTWCharToFreqOrder, EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO-from euckrfreq import EUCKRCharToFreqOrder, EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO-from gb2312freq import GB2312CharToFreqOrder, GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO-from big5freq import Big5CharToFreqOrder, BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO-from jisfreq import JISCharToFreqOrder, JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO+from . import constants +from .euctwfreq import EUCTWCharToFreqOrder, EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO +from .euckrfreq import EUCKRCharToFreqOrder, EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO +from .gb2312freq import GB2312CharToFreqOrder, GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO +from .big5freq import Big5CharToFreqOrder, BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO +from .jisfreq import JISCharToFreqOrder, JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO ENOUGH_DATA_THRESHOLD = 1024 SURE_YES = 0.99 . . . (it goes on like this for a while) . . RefactoringTool: Files that were modified: RefactoringTool: chardet\__init__.py RefactoringTool: chardet\big5prober.py RefactoringTool: chardet\chardistribution.py RefactoringTool: chardet\charsetgroupprober.py RefactoringTool: chardet\codingstatemachine.py RefactoringTool: chardet\constants.py RefactoringTool: chardet\escprober.py RefactoringTool: chardet\escsm.py RefactoringTool: chardet\eucjpprober.py RefactoringTool: chardet\euckrprober.py RefactoringTool: chardet\euctwprober.py RefactoringTool: chardet\gb2312prober.py RefactoringTool: chardet\hebrewprober.py RefactoringTool: chardet\jpcntx.py RefactoringTool: chardet\langbulgarianmodel.py RefactoringTool: chardet\langcyrillicmodel.py RefactoringTool: chardet\langgreekmodel.py RefactoringTool: chardet\langhebrewmodel.py RefactoringTool: chardet\langhungarianmodel.py RefactoringTool: chardet\langthaimodel.py RefactoringTool: chardet\latin1prober.py RefactoringTool: chardet\mbcharsetprober.py RefactoringTool: chardet\mbcsgroupprober.py RefactoringTool: chardet\mbcssm.py RefactoringTool: chardet\sbcharsetprober.py RefactoringTool: chardet\sbcsgroupprober.py RefactoringTool: chardet\sjisprober.py RefactoringTool: chardet\universaldetector.py RefactoringTool: chardet\utf8prober.py
現在我們對測試工具 — test.py — 應用2to3腳本。
C:\home\chardet> python c:\Python30\Tools\Scripts\2to3.py -w test.py RefactoringTool: Skipping implicit fixer: buffer RefactoringTool: Skipping implicit fixer: idioms RefactoringTool: Skipping implicit fixer: set_literal RefactoringTool: Skipping implicit fixer: ws_comma --- test.py (original) +++ test.py (refactored) @@ -4,7 +4,7 @@ count = 0 u = UniversalDetector() for f in glob.glob(sys.argv[1]):- print f.ljust(60),+ print(f.ljust(60), end=' ') u.reset() for line in file(f, 'rb'): u.feed(line) @@ -12,8 +12,8 @@ u.close() result = u.result if result['encoding']:- print result['encoding'], 'with confidence', result['confidence']+ print(result['encoding'], 'with confidence', result['confidence']) else:- print '******** no result'+ print('******** no result') count += 1-print count, 'tests'+print(count, 'tests') RefactoringTool: Files that were modified: RefactoringTool: test.py
看吧,還不算太難。只是轉換了一些impor和print語句。說到這兒,那些import語句原來到底存在什么問題呢?為了回答這個問題,你需要知道chardet是如果被分割到多個文件的。
⁂
chardet是一個多文件模塊。我也可以將所有的代碼都放在一個文件里(并命名為chardet.py),但是我沒有。我創建了一個目錄(叫做chardet),然后我在那個目錄里創建了一個__init__.py文件。如果Python看到目錄里有一個__init__.py文件,它會假設該目錄里的所有文件都是同一個模塊的某部分。模塊名為目錄的名字。目錄中的文件可以引用目錄中的其他文件,甚至子目錄中的也行。(再講一分鐘這個。)但是整個文件集合被作為一個單獨的模塊呈現給其他的Python代碼 — 就好像所有的函數和類都在一個.py文件里。
在__init__.py中到底有些什么?什么也沒有。一切。界于兩者之間。__init__.py文件不需要定義任何東西;它確實可以是一個空文件。或者也可以使用它來定義我們的主入口函數。或者把我們所有的函數都放進去。或者其他函數都放,單單不放某一個函數…
☞包含有
__init__.py文件的目錄總是被看作一個多文件的模塊。沒有__init__.py文件的目錄中,那些.py文件是不相關的。
我們來看看它實際上是怎樣工作的。
>>> import chardet >>> dir(chardet) ① ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', '__version__', 'detect'] >>> chardet ② <module 'chardet' from 'C:\Python31\lib\site-packages\chardet\__init__.py'>
chardet模塊中只多了一個detect()函數。
chardet模塊不只是一個文件的第一個線索:“module”被當作文件chardet/目錄中的__init__.py文件列出來。
我們再來瞟一眼__init__.py文件。
def detect(aBuf): ①
from . import universaldetector ②
u = universaldetector.UniversalDetector()
u.reset()
u.feed(aBuf)
u.close()
return u.result
__init__.py文件定義了detect()函數,它是chardet庫的主入口點。
detect()函數沒有任何實際的代碼!事實上,它所做的事情只是導入了universaldetector模塊然后開始調用它。但是universaldetector定義在哪兒?
答案就在那行古怪的import語句中:
from . import universaldetector
翻譯成中文就是,“導入universaldetector模塊;它跟我在同一目錄,”這里的我即指文件chardet/__init__.py。這是一種提供給多文件模塊中文件之間互相引用的方法,不需要擔心它會與已經安裝的搜索路徑中的模塊發生命名沖突。該條import語句只會在chardet/目錄中查找universaldetector模塊。
這兩條概念 — __init__.py和相對導入 — 意味著我們可以將模塊分割為任意多個塊。chardet模塊由36個.py文件組成 — 36!但我們所需要做的只是使用chardet/__init__.py文件中定義的某個函數。還有一件事情沒有告訴你,detect()使用了相對導入來引用了chardet/universaldetector.py中定義的一個類,然后這個類又使用了相對導入引用了其他5個文件的內容,它們都在chardet/目錄中。
☞如果你發現自己正在用Python寫一個大型的庫(或者更可能的情況是,當你意識到你的小模塊已經變得很大的時候),最好花一些時間將它重構為一個多文件模塊。這是Python所擅長的許多事情之一,那就利用一下這個優勢吧。
⁂
2to3腳本所不能做的False is invalid syntax現在開始真正的測試:使用測試集運行測試工具。由于測試集被設計成可以覆蓋所有可能的代碼路徑,它是用來測試移植后的代碼,保證bug不會埋伏在某個地方的一種不錯的辦法。
C:\home\chardet> python test.py tests\*\*
Traceback (most recent call last):
File "test.py", line 1, in <module>
from chardet.universaldetector import UniversalDetector
File "C:\home\chardet\chardet\universaldetector.py", line 51
self.done = constants.False
^
SyntaxError: invalid syntax
唔,一個小麻煩。在Python 3中,False是一個保留字,所以不能把它用作變量名。我們來看一看constants.py來確定這是在哪兒定義的。以下是constants.py在執行2to3腳本之前原來的版本。
import __builtin__
if not hasattr(__builtin__, 'False'):
False = 0
True = 1
else:
False = __builtin__.False
True = __builtin__.True
這一段代碼用來允許庫在低版本的Python 2中運行,在Python 2.3以前,Python沒有內置的bool類型。這段代碼檢測內置的True和False常量是否缺失,如果必要的話則定義它們。
但是,Python 3總是有bool類型的,所以整個這片代碼都沒有必要。最簡單的方法是將所有的constants.True和constants.False都分別替換成True和False,然后將這段死代碼從constants.py中移除。
所以universaldetector.py中的以下行:
self.done = constants.False
變成了
self.done = False
啊哈,是不是很有滿足感?代碼不僅更短了,而且更具可讀性。
constants是時候再運行一次test.py了,看看它能走多遠。
C:\home\chardet> python test.py tests\*\*
Traceback (most recent call last):
File "test.py", line 1, in <module>
from chardet.universaldetector import UniversalDetector
File "C:\home\chardet\chardet\universaldetector.py", line 29, in <module>
import constants, sys
ImportError: No module named constants
說什么了?不存在叫做constants的模塊?可是當然有constants這個模塊了。它就在chardet/constants.py中。
還記得什么時候2to3腳本會修復所有那些導入語句嗎?這個包內有許多的相對導入 — 即,在同一個庫中,導入其他模塊的模塊 — 但是在Python 3中相對導入的邏輯已經變了。在Python 2中,我們只需要import constants,然后它就會首先在chardet/目錄中查找。在Python 3中,所有的導入語句默認使用絕對路徑。如果想要在Python 3中使用相對導入,你需要顯式地說明:
from . import constants
但是。2to3腳本難道不是要自動修復這些的嗎?好吧,它確實這樣做了,但是該條導入語句在同一行組合了兩種不同的導入類型:庫內部對constants的相對導入,還有就是對sys模塊的絕對導入,sys模塊已經預裝在了Python的標準庫里。在Python 2里,我們可以將其組合到一條導入語句中。在Python 3中,我們不能這樣做,并且2to3腳本也不是那樣聰明,它不能把這條導入語句分成兩條。
解決的辦法是把這條導入語句手動的分成兩條。所以這條二合一的導入語句:
import constants, sys
需要變成兩條分享的導入語句:
from . import constants
import sys
在chardet庫中還分散著許多這類問題的變體。某些地方它是“import constants, sys”;其他一些地方則是“import constants, re”。修改的方法是一樣的:手工地將其分割為兩條語句,一條為相對導入準備,另一條用于絕對導入。
前進!
再來一次,運行test.py來執行我們的測試樣例…
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
File "test.py", line 9, in <module>
for line in file(f, 'rb'):
NameError: name 'file' is not defined
這一條也出乎我的意外,因為在記憶中我一直都在使用這種風格的代碼。在Python 2里,全局的file()函數是open()函數的一個別名,open()函數是打開文件用于讀取的標準方法。在Python 3中,全局的file()函數不再存在了,但是open()還保留著。
這樣的話,最簡單的解決辦法就是將file()調用替換為對open()的調用:
for line in open(f, 'rb'):
這即是我關于這個問題想要說的。
現在事情開始變得有趣了。對于“有趣,”我的意思是“跟地獄一樣讓人迷茫。”
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 98, in feed
if self._highBitDetector.search(aBuf):
TypeError: can't use a string pattern on a bytes-like object
我們先來看看self._highBitDetector是什么,然后再來調試這個錯誤。它被定義在UniversalDetector類的__init__方法中。
class UniversalDetector:
def __init__(self):
self._highBitDetector = re.compile(r'[\x80-\xFF]')
這段代碼預編譯一條正則表達式,它用來查找在128–255 (0x80–0xFF)范圍內的非ASCII字符。等一下,這似乎不太準確;我需要對更精確的術語來描述它。這個模式用來在128-255范圍內查找非ASCII的bytes。
問題就出在這兒了。
在Python 2中,字符串是一個字節數組,它的字符編碼信息被分開記錄著。如果想要Python 2跟蹤字符編碼,你得使用Unicode編碼的字符串(u'')。但是在Python 3中,字符串永遠都是Python 2中所謂的Unicode編碼的字符串 — 即,Unicode字符數組(可能存在可變長字節)。由于這條正則表達式是使用字符串模式定義的,所以它只能用來搜索字符串 — 再強調一次,字符數組。但是我們所搜索的并非字符串,它是一個字節數組。看一看traceback,該錯誤發生在universaldetector.py:
def feed(self, aBuf):
.
.
.
if self._mInputState == ePureAscii:
if self._highBitDetector.search(aBuf):
aBuf是什么?讓我們原路回到調用UniversalDetector.feed()的地方。有一處地方調用了它,是測試工具,test.py。
u = UniversalDetector()
.
.
.
for line in open(f, 'rb'):
u.feed(line)
在此處我們找到了答案:UniversalDetector.feed()方法中,aBuf是從磁盤文件中讀到的一行。仔細看一看用來打開文件的參數:'rb'。'r'是用來讀取的;OK,沒什么了不起的,我們在讀取文件。啊,但是'b'是用以讀取“二進制”數據的。如果沒有標記'b',for循環會一行一行地讀取文件,然后將其轉換為一個字符串 — Unicode編碼的字符數組 — 根據系統默認的編碼方式。但是使用'b'標記后,for循環一行一行地讀取文件,然后將其按原樣存儲為字節數組。該字節數組被傳遞給了 UniversalDetector.feed()方法,最后給了預編譯好的正則表達式,self._highBitDetector,用來搜索高位…字符。但是沒有字符;有的只是字節。蒼天哪。
我們需要該正則表達式搜索的并不是字符數組,而是一個字節數組。
只要我們認識到了這一點,解決辦法就有了。使用字符串定義的正則表達式可以搜索字符串。使用字節數組定義的正則表達式可以搜索字節數組。我們只需要改變用來定義正則表達式的參數的類型為字節數組,就可以定義一個字節數組模式。(還有另外一個該問題的實例,在下一行。)
class UniversalDetector:
def __init__(self):
- self._highBitDetector = re.compile(r'[\x80-\xFF]')
- self._escDetector = re.compile(r'(\033|~{)')
+ self._highBitDetector = re.compile(b'[\x80-\xFF]')
+ self._escDetector = re.compile(b'(\033|~{)')
self._mEscCharSetProber = None
self._mCharSetProbers = []
self.reset()
在整個代碼庫內搜索對re模塊的使用發現了另外兩個該類型問題的實例,出現在charsetprober.py文件中。再次,以上代碼將正則表達式定義為字符串,但是卻將它們作用在aBuf上,而aBuf是一個字節數組。解決方案還是一樣的:將正則表達式模式定義為字節數組。
class CharSetProber:
.
.
.
def filter_high_bit_only(self, aBuf):
- aBuf = re.sub(r'([\x00-\x7F])+', ' ', aBuf)
+ aBuf = re.sub(b'([\x00-\x7F])+', b' ', aBuf)
return aBuf
def filter_without_english_letters(self, aBuf):
- aBuf = re.sub(r'([A-Za-z])+', ' ', aBuf)
+ aBuf = re.sub(b'([A-Za-z])+', b' ', aBuf)
return aBuf
'bytes' object to str implicitly奇怪,越來越不尋常了…
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 100, in feed
elif (self._mInputState == ePureAscii) and self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly
在此存在一個Python解釋器與代碼風格之間的不協調。TypeError可以出現在那一行的任意地方,但是traceback不能明確定地指出錯誤的位置。可能是第一個或者第二個條件語句(conditional),對traceback來說,它們是一樣的。為了縮小調試的范圍,我們需要把這條代碼分割成兩行,像這樣:
elif (self._mInputState == ePureAscii) and \
self._escDetector.search(self._mLastChar + aBuf):
然后再運行測試工具:
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 101, in feed
self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly
啊哈!錯誤不在第一個條件語句上(self._mInputState == ePureAscii),是第二個的問題。但是,是什么引發了TypeError錯誤呢?也許你會想search()方法需要另外一種類型的參數,但是那樣的話,就不會產生當前這種traceback了。Python函數可以使用任何類型參數;只要傳遞了正確數目的參數,函數就可以執行。如果我們給函數傳遞了類型不匹配的參數,代碼可能就會崩潰,但是這樣一來,traceback就會指向函數內部的某一代碼塊了。但是當前得到的traceback告訴我們,錯誤就出現在開始調用search()函數那兒。所以錯誤肯定就出在+操作符上,該操作用于構建最終會傳遞給search()方法的參數。
從前一次調試的過程中,我們已經知道aBuf是一個字節數組。那么self._mLastChar又是什么呢?它是一個在reset()中定義的實例變量,而reset()方法剛好就是被__init__()調用的。
class UniversalDetector:
def __init__(self):
self._highBitDetector = re.compile(b'[\x80-\xFF]')
self._escDetector = re.compile(b'(\033|~{)')
self._mEscCharSetProber = None
self._mCharSetProbers = []
self.reset()
def reset(self):
self.result = {'encoding': None, 'confidence': 0.0}
self.done = False
self._mStart = True
self._mGotData = False
self._mInputState = ePureAscii
self._mLastChar = ''
現在我們找到問題的癥結所在了。你發現了嗎?self._mLastChar是一個字符串,而aBuf是一個字節數組。而我們不允許對字符串和字節數組做連接操作 — 即使是空串也不行。
那么,self._mLastChar到底是什么呢?在feed()方法中,在traceback報告的位置以下幾行就是了。
if self._mInputState == ePureAscii:
if self._highBitDetector.search(aBuf):
self._mInputState = eHighbyte
elif (self._mInputState == ePureAscii) and \
self._escDetector.search(self._mLastChar + aBuf):
self._mInputState = eEscAscii
self._mLastChar = aBuf[-1]
feed()方法被一次一次地調用,每次都傳遞給它幾個字節。該方法處理好它收到的字節(以aBuf傳遞進去的),然后將最后一個字節保存在self._mLastChar中,以便下次調用時還會用到。(在多字節編碼中,feed()在調用的時候可能只收到了某個字符的一半,然后下次調用時另一半才被傳到。)但是因為aBuf已經變成了一個字節數組,所以self._mLastChar也需要與其匹配。可以這樣做:
def reset(self):
.
.
.
- self._mLastChar = ''
+ self._mLastChar = b''
在代碼庫中搜索“mLastChar”,mbcharsetprober.py中也發現一個相似的問題,與之前不同的是,它記錄的是最后2個字符。MultiByteCharSetProber類使用一個單字符列表來記錄末尾的兩個字符。在Python 3中,這需要使用一個整數列表,因為實際上它記錄的并不是是字符,而是字節對象。(字節對象即范圍在0-255內的整數。)
class MultiByteCharSetProber(CharSetProber):
def __init__(self):
CharSetProber.__init__(self)
self._mDistributionAnalyzer = None
self._mCodingSM = None
- self._mLastChar = ['\x00', '\x00']
+ self._mLastChar = [0, 0]
def reset(self):
CharSetProber.reset(self)
if self._mCodingSM:
self._mCodingSM.reset()
if self._mDistributionAnalyzer:
self._mDistributionAnalyzer.reset()
- self._mLastChar = ['\x00', '\x00']
+ self._mLastChar = [0, 0]
'int' and 'bytes'有好消息,也有壞消息。好消息是我們一直在前進著…
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 101, in feed
self._escDetector.search(self._mLastChar + aBuf):
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'
…壞消息是,我們好像一直都在原地踏步。
但我們確實一直在取得進展!真的!即使traceback在相同的地方再次出現,這一次的錯誤畢竟與上次不同。前進!那么,這次又是什么錯誤呢?上一次我們確認過了,這一行代碼不應該會再做連接int型和字節數組(bytes)的操作。事實上,我們剛剛花了相當長一段時間來保證self._mLastChar是一個字節數組。它怎么會變成int呢?
答案不在上幾行代碼中,而在以下幾行。
if self._mInputState == ePureAscii:
if self._highBitDetector.search(aBuf):
self._mInputState = eHighbyte
elif (self._mInputState == ePureAscii) and \
self._escDetector.search(self._mLastChar + aBuf):
self._mInputState = eEscAscii
self._mLastChar = aBuf[-1]
該錯誤沒有發生在feed()方法第一次被調用的時候;而是在第二次調用的過程中,在self._mLastChar被賦值為aBuf末尾的那個字節之后。好吧,這又會有什么問題呢?因為獲取字節數組中的單個元素會產生一個整數,而不是字節數組。它們之間的區別,請看以下在交互式shell中的操作:
>>> aBuf = b'\xEF\xBB\xBF' ① >>> len(aBuf) 3 >>> mLastChar = aBuf[-1] >>> mLastChar ② 191 >>> type(mLastChar) ③ <class 'int'> >>> mLastChar + aBuf ④ Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: unsupported operand type(s) for +: 'int' and 'bytes' >>> mLastChar = aBuf[-1:] ⑤ >>> mLastChar b'\xbf' >>> mLastChar + aBuf ⑥ b'\xbf\xef\xbb\xbf'
universaldetector.py中發現的那個錯誤。
所以,為了保證universaldetector.py中的feed()方法不管被調用多少次都能夠正常運行,我們需要將self._mLastChar實例化為一個長度為0的字節數組,并且保證它一直是一個字節數組。
self._escDetector.search(self._mLastChar + aBuf):
self._mInputState = eEscAscii
- self._mLastChar = aBuf[-1]
+ self._mLastChar = aBuf[-1:]
ord() expected string of length 1, but int found困了嗎?就要完成了…
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 116, in feed
if prober.feed(aBuf) == constants.eFoundIt:
File "C:\home\chardet\chardet\charsetgroupprober.py", line 60, in feed
st = prober.feed(aBuf)
File "C:\home\chardet\chardet\utf8prober.py", line 53, in feed
codingState = self._mCodingSM.next_state(c)
File "C:\home\chardet\chardet\codingstatemachine.py", line 43, in next_state
byteCls = self._mModel['classTable'][ord(c)]
TypeError: ord() expected string of length 1, but int found
OK,因為c是int類型的,但是ord()需要一個長度為1的字符串。就是這樣了。c在哪兒定義的?
# codingstatemachine.py
def next_state(self, c):
# for each byte we get its class
# if it is first byte, we also get byte length
byteCls = self._mModel['classTable'][ord(c)]
不是這兒; 此處c只是被傳遞給了next_state()函數。我們再上一級看看。
# utf8prober.py
def feed(self, aBuf):
for c in aBuf:
codingState = self._mCodingSM.next_state(c)
看到了嗎?在Python 2中,aBuf是一個字符串,所以c就是一個長度為1的字符串。(那就是我們通過遍歷字符串所得到的 — 所有的字符,一次一個。)因為現在aBuf是一個字節數組,所以c變成了int類型的,而不再是長度為1的字符串。也就是說,沒有必要再調用ord()函數了,因為c已經是int了!
這樣修改:
def next_state(self, c):
# for each byte we get its class
# if it is first byte, we also get byte length
- byteCls = self._mModel['classTable'][ord(c)]
+ byteCls = self._mModel['classTable'][c]
在代碼庫中搜索“ord(c)”后,發現sbcharsetprober.py中也有相似的問題…
# sbcharsetprober.py
def feed(self, aBuf):
if not self._mModel['keepEnglishLetter']:
aBuf = self.filter_without_english_letters(aBuf)
aLen = len(aBuf)
if not aLen:
return self.get_state()
for c in aBuf:
order = self._mModel['charToOrderMap'][ord(c)]
…還有latin1prober.py…
# latin1prober.py
def feed(self, aBuf):
aBuf = self.filter_with_english_letters(aBuf)
for c in aBuf:
charClass = Latin1_CharToClass[ord(c)]
c在aBuf中遍歷,這就意味著它是一個整數,而非字符串。解決方案是相同的:把ord(c)就替換成c。
# sbcharsetprober.py
def feed(self, aBuf):
if not self._mModel['keepEnglishLetter']:
aBuf = self.filter_without_english_letters(aBuf)
aLen = len(aBuf)
if not aLen:
return self.get_state()
for c in aBuf:
- order = self._mModel['charToOrderMap'][ord(c)]
+ order = self._mModel['charToOrderMap'][c]
# latin1prober.py
def feed(self, aBuf):
aBuf = self.filter_with_english_letters(aBuf)
for c in aBuf:
- charClass = Latin1_CharToClass[ord(c)]
+ charClass = Latin1_CharToClass[c]
int() >= str()繼續我們的路吧。
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
File "test.py", line 10, in <module>
u.feed(line)
File "C:\home\chardet\chardet\universaldetector.py", line 116, in feed
if prober.feed(aBuf) == constants.eFoundIt:
File "C:\home\chardet\chardet\charsetgroupprober.py", line 60, in feed
st = prober.feed(aBuf)
File "C:\home\chardet\chardet\sjisprober.py", line 68, in feed
self._mContextAnalyzer.feed(self._mLastChar[2 - charLen :], charLen)
File "C:\home\chardet\chardet\jpcntx.py", line 145, in feed
order, charLen = self.get_order(aBuf[i:i+2])
File "C:\home\chardet\chardet\jpcntx.py", line 176, in get_order
if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
TypeError: unorderable types: int() >= str()
這都是些什么?“Unorderable types”?字節數組與字符串之間的差異引起的問題再一次出現了。看一看以下代碼:
class SJISContextAnalysis(JapaneseContextAnalysis):
def get_order(self, aStr):
if not aStr: return -1, 1
# find out current char's byte length
if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
((aStr[0] >= '\xE0') and (aStr[0] <= '\xFC')):
charLen = 2
else:
charLen = 1
aStr從何而來?再深入棧內看一看:
def feed(self, aBuf, aLen):
.
.
.
i = self._mNeedToSkipCharNum
while i < aLen:
order, charLen = self.get_order(aBuf[i:i+2])
看,是aBuf,我們的老戰友。從我們在這一章中所遇到的問題你也可以猜到了問題的關鍵了,因為aBuf是一個字節數組。此處feed()方法并不是整個地將它傳遞出去;而是先對它執行分片操作。就如你在這章前面看到的,對字節數組執行分片操作的返回值仍然為字節數組,所以傳遞給get_order()方法的aStr仍然是字節數組。
那么以下代碼是怎樣處理aStr的呢?它將該字節第一個元素與長度為1的字符串進行比較操作。在Python 2,這是可以的,因為aStr和aBuf都是字符串,所以aStr[0]也是字符串,并且我們允許比較兩個字符串的是否相等。但是在Python 3中,aStr和aBuf都是字節數組,而aStr[0]就成了一個整數,沒有執行顯式地強制轉換的話,是不能對整數和字符串執行相等性比較的。
在當前情況下,沒有必要添加強制轉換,這會讓代碼變得更加復雜。aStr[0]產生一個整數;而我們所比較的對象都是常量(constant)。那就把長度為1的字符串換成整數吧。我們也順便把aStr換成aBuf吧,因為aStr本來也不是一個字符串。
class SJISContextAnalysis(JapaneseContextAnalysis):
- def get_order(self, aStr):
- if not aStr: return -1, 1
+ def get_order(self, aBuf):
+ if not aBuf: return -1, 1
# find out current char's byte length
- if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
- ((aBuf[0] >= '\xE0') and (aBuf[0] <= '\xFC')):
+ if ((aBuf[0] >= 0x81) and (aBuf[0] <= 0x9F)) or \
+ ((aBuf[0] >= 0xE0) and (aBuf[0] <= 0xFC)):
charLen = 2
else:
charLen = 1
# return its order if it is hiragana
- if len(aStr) > 1:
- if (aStr[0] == '\202') and \
- (aStr[1] >= '\x9F') and \
- (aStr[1] <= '\xF1'):
- return ord(aStr[1]) - 0x9F, charLen
+ if len(aBuf) > 1:
+ if (aBuf[0] == 0x202) and \
+ (aBuf[1] >= 0x9F) and \
+ (aBuf[1] <= 0xF1):
+ return aBuf[1] - 0x9F, charLen
return -1, charLen
class EUCJPContextAnalysis(JapaneseContextAnalysis):
- def get_order(self, aStr):
- if not aStr: return -1, 1
+ def get_order(self, aBuf):
+ if not aBuf: return -1, 1
# find out current char's byte length
- if (aStr[0] == '\x8E') or \
- ((aStr[0] >= '\xA1') and (aStr[0] <= '\xFE')):
+ if (aBuf[0] == 0x8E) or \
+ ((aBuf[0] >= 0xA1) and (aBuf[0] <= 0xFE)):
charLen = 2
- elif aStr[0] == '\x8F':
+ elif aBuf[0] == 0x8F:
charLen = 3
else:
charLen = 1
# return its order if it is hiragana
- if len(aStr) > 1:
- if (aStr[0] == '\xA4') and \
- (aStr[1] >= '\xA1') and \
- (aStr[1] <= '\xF3'):
- return ord(aStr[1]) - 0xA1, charLen
+ if len(aBuf) > 1:
+ if (aBuf[0] == 0xA4) and \
+ (aBuf[1] >= 0xA1) and \
+ (aBuf[1] <= 0xF3):
+ return aBuf[1] - 0xA1, charLen
return -1, charLen
在代碼庫中查找ord()函數,我們在chardistribution.py中也發現了同樣的問題(更確切地說,在以下這些類中,EUCTWDistributionAnalysis,EUCKRDistributionAnalysis,GB2312DistributionAnalysis,Big5DistributionAnalysis,SJISDistributionAnalysis和EUCJPDistributionAnalysis)。對于它們存在的問題,解決辦法與我們對jpcntx.py中的類EUCJPContextAnalysis和SJISContextAnalysis的做法相似。
'reduce' is not defined再次陷入中斷…
C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
File "test.py", line 12, in <module>
u.close()
File "C:\home\chardet\chardet\universaldetector.py", line 141, in close
proberConfidence = prober.get_confidence()
File "C:\home\chardet\chardet\latin1prober.py", line 126, in get_confidence
total = reduce(operator.add, self._mFreqCounter)
NameError: global name 'reduce' is not defined
根據官方手冊:What’s New In Python 3.0,函數reduce()已經從全局名字空間中移出,放到了functools模塊中。引用手冊中的內容:“如果需要,請使用functools.reduce(),99%的情況下,顯式的for循環使代碼更有可讀性。”你可以從Guido van Rossum的一篇日志中看到關于這項決策的更多細節:The fate of reduce() in Python 3000。
def get_confidence(self):
if self.get_state() == constants.eNotMe:
return 0.01
total = reduce(operator.add, self._mFreqCounter)
reduce()函數使用兩個參數 — 一個函數,一個列表(更嚴格地說,可迭代的對象就行了) — 然后將函數增量式地作用在列表的每個元素上。換句話說,這是一種良好而高效的用于綜合(add up)列表所有元素并返回其結果的方法。
這種強大的技術使用如此頻繁,所以Python就添加了一個全局的sum()函數。
def get_confidence(self):
if self.get_state() == constants.eNotMe:
return 0.01
- total = reduce(operator.add, self._mFreqCounter)
+ total = sum(self._mFreqCounter)
由于我們不再使用operator模塊,所以可以在文件最上方移除那條import語句。
from .charsetprober import CharSetProber
from . import constants
- import operator
可以開始測試了吧?(快要吐血的樣子…)
C:\home\chardet> python test.py tests\*\* tests\ascii\howto.diveintomark.org.xml ascii with confidence 1.0 tests\Big5\0804.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\blog.worren.net.xml Big5 with confidence 0.99 tests\Big5\carbonxiv.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\catshadow.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\coolloud.org.tw.xml Big5 with confidence 0.99 tests\Big5\digitalwall.com.xml Big5 with confidence 0.99 tests\Big5\ebao.us.xml Big5 with confidence 0.99 tests\Big5\fudesign.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\kafkatseng.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\ke207.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\leavesth.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\letterlego.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\linyijen.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\marilynwu.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\myblog.pchome.com.tw.xml Big5 with confidence 0.99 tests\Big5\oui-design.com.xml Big5 with confidence 0.99 tests\Big5\sanwenji.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\sinica.edu.tw.xml Big5 with confidence 0.99 tests\Big5\sylvia1976.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\tlkkuo.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\tw.blog.xubg.com.xml Big5 with confidence 0.99 tests\Big5\unoriginalblog.com.xml Big5 with confidence 0.99 tests\Big5\upsaid.com.xml Big5 with confidence 0.99 tests\Big5\willythecop.blogspot.com.xml Big5 with confidence 0.99 tests\Big5\ytc.blogspot.com.xml Big5 with confidence 0.99 tests\EUC-JP\aivy.co.jp.xml EUC-JP with confidence 0.99 tests\EUC-JP\akaname.main.jp.xml EUC-JP with confidence 0.99 tests\EUC-JP\arclamp.jp.xml EUC-JP with confidence 0.99 . . . 316 tests
天哪,伙計,她真的歡快地跑起來了!/me does a little dance
⁂
我們學到了什么?
2to3腳本確實有用,但是它只能做一些簡單的輔助工作 — 函數重命名,模塊重命名,語法修改等。之前,它被認為是一項會讓人印象深刻的大工程,但是最后,實際上它只是一個能智能地執行查找替換機器人。
chardet庫的時候遇到的頭號問題就是:字符串和字節對象之間的差異。在我們這個情況中,這種問題比較明顯,因為整個chardet庫就是一直在執行從字節流到字符串的轉換。但是“字節流”出現的方式會遠超出你的想象。以“二進制”模式讀取文件?我們會獲得字節流。獲取一份web頁面?調用web API?這也會返回字節流。
chardet模塊能在Python 3中工作的唯一理由是,我一開始就使用了測試集合來檢驗所有主要的代碼路徑。如果你還沒有任何測試集,在移植代碼之前自己寫一些吧。如果你的測試集合太小,那么請寫全。如果測試集夠了,那么,我們就又可以開始歷險了。
© 2001–9 Mark Pilgrim