您当前位置: 首页 ‣ 深入 Python 3 ‣
难度等级: ♦♦♦♦♦
chardet
移植到 Python 3❝ 言语,言语。我们唯一能做的就是说。 ❞
— 《罗森克兰茨与吉尔登斯腾已死》
问题:在网络、收件箱以及所有编写的计算机系统中,导致乱码文本的头号罪魁祸首是什么?答案是字符编码。在 字符串 章节中,我谈到了字符编码的历史以及 Unicode 的诞生,即“统治所有编码的编码”。我希望永远不再看到网页上的乱码字符,因为所有创作系统都存储了准确的编码信息,所有传输协议都支持 Unicode,并且每个处理文本的系统在编码之间转换时都保持了完美的保真度。
我也想要一匹小马。
一匹 Unicode 小马。
一匹 Unipony,如果可以这么说的话。
我会满足于字符编码自动检测。
⁂
这意味着获取未知字符编码的字节序列,并尝试确定编码以便读取文本。就像当你没有解密密钥时破解密码一样。
一般来说,是的。但是,一些编码针对特定语言进行了优化,语言不是随机的。一些字符序列经常出现,而其他序列则毫无意义。一个精通英语的人打开报纸,发现“txzqJv 2!dasd0a QqdKjvz”会立即意识到那不是英语(即使它完全由英语字母组成)。通过研究大量“典型”文本,计算机算法可以模拟这种熟练程度并对文本的语言进行有根据的猜测。
换句话说,编码检测实际上是语言检测,结合了哪些语言倾向于使用哪些字符编码的知识。
事实证明,是的。所有主要的浏览器都具有字符编码自动检测功能,因为网络上充斥着没有编码信息的网页。Mozilla Firefox 包含一个开源的编码自动检测库。我将该库移植到 Python 2,并将其称为 chardet
模块。本章将带您一步一步地了解将 chardet
模块从 Python 2 移植到 Python 3 的过程。
⁂
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
中)。EscCharSetProber
将文本一次一个字节地馈送到这些状态机中。如果任何状态机最终唯一地识别出编码,EscCharSetProber
会立即将肯定结果返回给 UniversalDetector
,后者将其返回给调用者。如果任何状态机遇到非法序列,它将被删除,处理将继续进行其他状态机。
假设没有 BOM,UniversalDetector
检查文本是否包含任何高位字符。如果是,它会为检测多字节编码、单字节编码以及最后手段的 windows-1252
创建一系列“探测器”。
多字节编码探测器,MBCSGroupProber
(定义在 mbcsgroupprober.py
中),实际上只是一个 shell,它管理着一组其他探测器,每个探测器代表一种多字节编码:Big5、GB2312、EUC-TW、EUC-KR、EUC-JP、SHIFT_JIS 和 UTF-8。MBCSGroupProber
将文本馈送到这些特定于编码的探测器中并检查结果。如果探测器报告发现非法字节序列,它将从进一步处理中删除(因此,例如,对 UniversalDetector
.feed()
的任何后续调用将跳过该探测器)。如果探测器报告它有理由相信它已检测到编码,MBCSGroupProber
会将此肯定结果报告给 UniversalDetector
,后者会将结果报告给调用者。
大多数多字节编码探测器都继承自 MultiByteCharSetProber
(定义在 mbcharsetprober.py
中),它们只是连接相应的 state machine 和 distribution analyzer,并让 MultiByteCharSetProber
完成其余的工作。MultiByteCharSetProber
将文本一次一个字节地通过特定于编码的 state machine,查找可能表明确定性肯定或否定结果的字节序列。同时,MultiByteCharSetProber
将文本馈送到特定于编码的 distribution analyzer。
distribution analyzer(每个都定义在 chardistribution.py
中)使用特定于语言的模型,这些模型定义了哪些字符最常使用。一旦 MultiByteCharSetProber
向 distribution analyzer 馈送了足够多的文本,它就会根据常用字符的数量、字符总数以及特定于语言的分布比率计算置信度等级。如果置信度足够高,MultiByteCharSetProber
会将结果返回给 MBCSGroupProber
,后者会将其返回给 UniversalDetector
,后者会将其返回给调用者。
日语的情况比较复杂。单字符分布分析并不总是足以区分 EUC-JP
和 SHIFT_JIS
,因此 SJISProber
(定义在 sjisprober.py
中)还会使用双字符分布分析。SJISContextAnalysis
和 EUCJPContextAnalysis
(两者都定义在 jpcntx.py
中,并且都继承自公共的 JapaneseContextAnalysis
类)检查文本中平假名音节字符的频率。一旦处理了足够的文本,它们就会将置信度级别返回给 SJISProber
,后者会检查两个 analyzer 并将更高的置信度级别返回给 MBCSGroupProber
。
单字节编码探测器,SBCSGroupProber
(定义在 sbcsgroupprober.py
中),实际上也只是一个 shell,它管理着一组其他探测器,每个探测器代表一种单字节编码和语言的组合:windows-1251
、KOI8-R
、ISO-8859-5
、MacCyrillic
、IBM855
和 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
中),它接受语言模型作为参数。语言模型定义了不同双字符序列在典型文本中出现的频率。SingleByteCharSetProber
处理文本并统计最常用的双字符序列。一旦处理了足够的文本,它就会根据常用序列的数量、字符总数以及特定于语言的分布比率计算置信度级别。
希伯来语的处理是一个特殊情况。如果文本根据双字符分布分析似乎是希伯来语,HebrewProber
(定义在 hebrewprober.py
中)会尝试区分视觉希伯来语(其中源文本实际上逐行存储“反向”,然后逐字显示以便可以从右到左阅读)和逻辑希伯来语(其中源文本按阅读顺序存储,然后由客户端从右到左渲染)。由于某些字符的编码方式不同,具体取决于它们是出现在单词中间还是末尾,我们可以对源文本的方向进行合理的猜测,并返回相应的编码(windows-1255
表示逻辑希伯来语,或 ISO-8859-8
表示视觉希伯来语)。
windows-1252
如果 UniversalDetector
在文本中检测到高位字符,但其他多字节或单字节编码探测器都没有返回确定的结果,它会创建一个 Latin1Prober
(定义在 latin1prober.py
中)来尝试检测 windows-1252
编码中的英语文本。这种检测本质上不可靠,因为英语字母在许多不同编码中的编码方式相同。区分 windows-1252
的唯一方法是通过常用符号,例如智能引号、花括号撇号、版权符号等。Latin1Prober
会自动降低其置信度等级,以使更准确的探测器尽可能获胜。
⁂
2to3
我们将从 Python 2 迁移 chardet
模块到 Python 3。Python 3 附带一个名为 2to3
的实用程序脚本,该脚本将您的实际 Python 2 源代码作为输入,并尽可能自动转换为 Python 3。在某些情况下,这很容易——函数已重命名或移动到不同的模块——但在其他情况下,它可能会变得非常复杂。要了解它可以执行的所有操作,请参考附录,使用 2to3
将代码移植到 Python 3。在本章中,我们将从在 chardet
包上运行 2to3
开始,但正如您将看到的那样,在自动工具发挥作用后,我们仍然需要做很多工作。
主要的 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
好吧,这并不难。只需转换几个导入和打印语句。说到这里,所有这些导入语句到底有什么问题?要回答这个问题,您需要了解 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
模块不仅仅是一个文件的线索:“模块” 被列为 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 个!但是,要开始使用它,你只需要 import chardet
,然后就可以调用主要的 chardet.detect()
函数。你的代码并不知道 detect()
函数实际上是在 chardet/__init__.py
文件中定义的。同样,你也不知道 detect()
函数使用相对导入来引用 chardet/universaldetector.py
中定义的类,而该类又使用相对导入引用了其他五个文件,所有这些文件都包含在 chardet/
目录中。
☞如果你发现自己正在用 Python 编写一个大型库(或者更可能的是,当你意识到你的小型库已经发展成一个大型库时),花点时间把它重构为一个多文件模块。这是 Python 的众多优势之一,所以要充分利用它。
⁂
2to3
不能修复的问题False
是无效语法现在进行真正的测试:针对测试套件运行测试工具。由于测试套件旨在涵盖所有可能的代码路径,所以它是一种测试我们的移植代码的好方法,以确保没有隐藏任何错误。
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
模块的相对导入,以及预装在 Python 标准库中的 sys
模块的绝对导入。在 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()
函数的别名,它是 打开文本文件以进行读取 的标准方法。在 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 字节。
问题就在这里。
在 Python 2 中,字符串是字节数组,其字符编码是单独跟踪的。如果你希望 Python 2 跟踪字符编码,你必须使用 Unicode 字符串(u''
)。但在 Python 3 中,字符串始终是 Python 2 所谓的 Unicode 字符串——也就是说,Unicode 字符数组(可能具有不同的字节长度)。由于这个正则表达式是由字符串模式定义的,因此它只能用于搜索字符串——再次强调,字符数组。但我们搜索的不是字符串,而是字节数组。查看回溯,这个错误发生在 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'
表示“读取”;好吧,没什么大不了的,我们正在读取文件。啊,但 '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'
对象转换为 str
越来越奇怪了……
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
可能出现在该行的任何地方,但回溯没有告诉你确切的位置。它可能出现在第一个条件或第二个条件中,回溯看起来会一样。为了缩小范围,你应该将该行分成两半,如下所示
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()
方法期望一个不同类型的值,但那不会产生这个回溯。Python 函数可以接收任何值;如果你传递了正确数量的参数,该函数就会执行。如果你传递了它期望的类型以外的值,它可能会崩溃,但如果发生了这种情况,回溯会指向函数内部的某个地方。但这个回溯说它根本没有执行到调用search()
方法的地方。所以问题一定是在那个+
操作中,因为它正在尝试构造一个值,它最终会传递给search()
方法。
我们从之前的调试知道,aBuf 是一个字节数组。那么,self._mLastChar
是什么呢?它是一个实例变量,定义在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()
方法中,距离回溯发生的位置只有几行代码。
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
中找到类似的问题,但它不是跟踪最后一个字符,而是跟踪最后两个字符。MultiByteCharSetProber
类使用一个包含 1 个字符的字符串列表来跟踪最后两个字符。在 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'
和 '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'
……坏消息是,它并不总是感觉像在进步。
但这是进步!真的!虽然回溯调用了相同的代码行,但它是一个与以前不同的错误。进步!那么现在问题是什么?上次我检查的时候,这行代码没有尝试将一个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()
期望长度为 1 的字符串,但找到了 int
累了吗?你快到了……
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
好的,所以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)]
这没有帮助;它只是被传递到函数中。让我们弹出堆栈。
# 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,这意味着它是一个整数,而不是一个 1 个字符的字符串。解决方案是相同的:将 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()
那么这一切都是什么呢?“不可比较的类型”?又一次,字节数组和字符串之间的差异正在显现。看一下代码
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] 会产生一个整数;你正在比较的都是常量。让我们将它们从 1 个字符的字符串更改为整数。同时,让我们将 aStr 更改为 aBuf,因为它实际上不是一个字符串。
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] == 202) 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'
未定义再一次冲锋陷阵……
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
根据官方的 Python 3.0 中的新功能指南,reduce()
函数已经从全局命名空间中移出,并移入了 functools
模块。引用该指南:“如果你真的需要它,可以使用 functools.reduce()
;但是,99% 的情况下,显式的 for
循环更易读。” 你可以从 Guido van Rossum 的博客中阅读更多关于这个决定的内容:Python 3000 中 reduce() 的命运。
def get_confidence(self):
if self.get_state() == constants.eNotMe:
return 0.01
total = reduce(operator.add, self._mFreqCounter)
reduce()
函数接受两个参数——一个函数和一个列表(严格来说,任何可迭代对象都可以)——并将该函数累积地应用于列表的每个项目。换句话说,这是一种奇特而迂回的方式,可以将列表中的所有项目加起来并返回结果。
这个怪兽如此常见,以至于 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 跳了一支小舞
⁂
我们学到了什么?
2to3
工具 在其能力范围内很有帮助,但它只会完成简单的事情——函数重命名、模块重命名、语法更改。这是一个令人印象深刻的工程,但最终它只是一个智能的搜索和替换机器人。chardet
库的全部目的是将字节流转换为字符串。但“字节流”的出现频率可能比你想象的要高。以“二进制”模式读取文件?你会得到一个字节流。获取网页?调用 web API?它们也会返回一个字节流。chardet
在 Python 3 中工作有信心的唯一原因是,我从一个测试套件开始,它测试了所有主要的代码路径。如果你没有测试用例,在开始移植到 Python 3 之前写一些测试用例。如果你有一些测试用例,写更多。如果你有很多测试用例,那么真正的乐趣就开始了。© 2001–11 Mark Pilgrim