你的位置: 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类编码方式:

  1. 包含字节顺序标记(BOM)的UTF-n。它包括UTF-8,大尾端和小尾端的UTF-16,还有所有4字节顺序的UTF-32的变体。
  2. 转义编码,它们与7字节的ASCII编码兼容,非ASCII编码的字符会以一个转义序列打头。比如:ISO-2022-JP(日文)和HZ-GB-2312(中文).
  3. 多字节编码,在这种编码方式中,每个字符使用可变长度的字节表示。比如:Big5(中文),SHIFT_JIS(日文),EUC-KR(韩文)和缺少BOM标记的UTF-8
  4. 单字节编码,这种编码方式中,每个字符使用一个字节编码。例如:KOI8-R(俄语),windows-1255(希伯来语)和TIS-620(泰国语)。
  5. windows-1252,它主要被根本不知道字符编码的中层管理人员(middle manager)在Microsoft Windows上使用。

BOM标记的UTF-n

如果文本以BOM标记打头,我们可以合理地假设它使用了UTF-8UTF-16或者UTF-32编码。(BOM会告诉我们是其中哪一种,这就是它的功能。)这个过程在UniversalDetector中完成,并且不需要深入处理,会非常快地返回其结果。

转义编码

如果文本包含有可识别的能指示出某种转义编码的转义序列,UniversalDetector会创建一个EscCharSetProber对象(在escprober.py中定义),然后以该文本调用它。

EscCharSetProber会根据HZ-GB-2312ISO-2022-CNISO-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,它用来处理每种多字节编码:Big5GB2312EUC-TWEUC-KREUC-JPSHIFT_JISUTF-8MBCSGroupProber将文本作为每一个特定编码探测器的输入,并且检测其结果。如果某个探测器报告说它发现了一个非法的字节序列,那么该探测器则会被放弃,不再进一步处理(因此,换句话说就是,任何对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-JPSHIFT_JIS,所以SJISProber(在sjisprober.py中定义)也使用双字符的分布分析。SJISContextAnalysisEUCJPContextAnalysis(都定义在jpcntx.py中,并且都从类JapaneseContextAnalysis中继承)检测文本中的平假名音节字符(Hiragana syllabary characher)的出现次数。一旦处理了足够量的文本,它会返回一个置信度给SJISProberSJISProber检查两个分析器的结果,然后将置信度高的那个返回给MBCSGroupProber

单字节编码

单字节编码的探测器,即SBCSGroupProber(定义在sbcsgroupprober.py中),也是一个管理一组其他探测器的shell,它会尝试单字节编码和语言的每种组合:windows-1251KOI8-RISO-8859-5MacCyrillicIBM855,and IBM866(俄语);ISO-8859-7windows-1253(希腊语);ISO-8859-5windows-1251(保加利亚语);ISO-8859-2windows-1250(匈牙利语);TIS-620(泰国语);windows-1255ISO-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'>
  1. 除了常见的类属性,在chardet模块中只多了一个detect()函数。
  2. 这是我们发觉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
  1. __init__.py文件定义了detect()函数,它是chardet库的主入口点。
  2. 但是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类型。这段代码检测内置的TrueFalse常量是否缺失,如果必要的话则定义它们。

但是,Python 3总是有bool类型的,所以整个这片代码都没有必要。最简单的方法是将所有的constants.Trueconstants.False都分别替换成TrueFalse,然后将这段死代码从constants.py中移除。

所以universaldetector.py中的以下行:

self.done = constants.False

变成了

self.done = False

啊哈,是不是很有满足感?代码不仅更短了,而且更具可读性。

No module named 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”。修改的方法是一样的:手工地将其分割为两条语句,一条为相对导入准备,另一条用于绝对导入。

前进!

Name 'file' is not defined

再来一次,运行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'):

这即是我关于这个问题想要说的。

Can’t use a string pattern on a bytes-like object

现在事情开始变得有趣了。对于“有趣,”我的意思是“跟地狱一样让人迷茫。”

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范围内查找非ASCIIbytes

问题就出在这儿了。

在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

Can't convert '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]

Unsupported operand type(s) for +: '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'
  1. 定义一个长度为3的字节数组。
  2. 字节数组的最后一个元素为191。
  3. 它是一个整数。
  4. 连接整数和字节数组的操作是不允许的。我们重复了在universaldetector.py中发现的那个错误。
  5. 啊,这就是解决办法了。使用列表分片从数组的最后一个元素中创建一个新的字节数组,而不是直接获取这个元素。即,从最后一个元素开始切割,直到到达数组的末尾。当前mLastChar是一个长度为1的字节数组。
  6. 连接长度分别为1和3的字节数组,则会返回一个新的长度为4的字节数组。

所以,为了保证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,因为cint类型的,但是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)]

caBuf中遍历,这就意味着它是一个整数,而非字符串。解决方案是相同的:把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]

Unorderable types: 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,这是可以的,因为aStraBuf都是字符串,所以aStr[0]也是字符串,并且我们允许比较两个字符串的是否相等。但是在Python 3中,aStraBuf都是字节数组,而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中也发现了同样的问题(更确切地说,在以下这些类中,EUCTWDistributionAnalysisEUCKRDistributionAnalysisGB2312DistributionAnalysisBig5DistributionAnalysisSJISDistributionAnalysisEUCJPDistributionAnalysis)。对于它们存在的问题,解决办法与我们对jpcntx.py中的类EUCJPContextAnalysisSJISContextAnalysis的做法相似。

Global name '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

总结

我们学到了什么?

  1. 尝试大批量地把代码从Python 2移植到Python 3上是一件让人头疼的工作。没有捷径。它确实很困难。
  2. 自动化的2to3脚本确实有用,但是它只能做一些简单的辅助工作 — 函数重命名,模块重命名,语法修改等。之前,它被认为是一项会让人印象深刻的大工程,但是最后,实际上它只是一个能智能地执行查找替换机器人。
  3. 在移植chardet库的时候遇到的头号问题就是:字符串和字节对象之间的差异。在我们这个情况中,这种问题比较明显,因为整个chardet库就是一直在执行从字节流到字符串的转换。但是“字节流”出现的方式会远超出你的想象。以“二进制”模式读取文件?我们会获得字节流。获取一份web页面?调用web API?这也会返回字节流。
  4. 需要彻底地了解所面对的程序。如果那段程序是自己写自然非常好,但是至少,我们需要够理解所有晦涩难懂的细节。因为bug可能埋伏在任何地方。
  5. 测试样例是必要的。没有它们的话不要尝试着移植代码。我自信移植后的chardet模块能在Python 3中工作的唯一理由是,我一开始就使用了测试集合来检验所有主要的代码路径。如果你还没有任何测试集,在移植代码之前自己写一些吧。如果你的测试集合太小,那么请写全。如果测试集够了,那么,我们就又可以开始历险了。

© 2001–9 Mark Pilgrim