現在地: ホーム ‣ Dive Into Python 3 ‣
難易度: ♦♦♦♦♦
chardet
をPython 3に移植する❝ 言葉だよ言葉。俺たちはこいつらを口に出してりゃいいんだ。❞
— Rosencrantz and Guildenstern are Dead
問題: ウェブ、メール、そして今までに作られたありとあらゆるコンピュータシステムにおける、文字化けの原因ナンバーワンは何でしょう? 答えは文字コードだ。文字列の章では、文字コードの歴史と、「すべてを支配する1つの文字コード」であるUnicodeの誕生について話した。しウェブページ上で二度と文字化けを見ずに済むようになるなら、私はUnicodeを愛すのだろうけれど、それには、すべてのオーサリングシステムが正確な文字コード情報を格納し、すべての転送プロトコルがUnicodeに対応し、テキストを扱うすべてのシステムが文字コードの変換時に完全な忠実さを持つ必要がある。
私はポニーも欲しいんだ。
ユニコードのポニー。
言ってみれば、ユニポニー。
私は文字コードの自動検出で我慢しておくよ。
⁂
要するに、どの文字コードで符号化されたのか分からないバイト列を受け取って、その文字コードを推測することでテキストを読み込めるようにしようというわけだ。これは、復号鍵を知らない状態で暗号を解読しようとするのに似ている。
一般論としてはイエスだ。とはいえ、一部のエンコーディングは特定の言語に最適化されていて、そして言語というものは何らかの規則性を持っているものだ。文章のあちこちに現れる文字列もあれば、言葉として意味をなさない文字列もある。英語に堪能な人が新聞を開いて “txzqJv 2!dasd0a QqdKjvz” という文章を見つけたら、(文章自体はすべて英語の文字で構成されているけれども)これは英語の文章ではないと即座に認識できるだろう。この種の言語認識は、大量の「典型的」な文章の解析を行えば、統計的なアルゴリズムによってシミュレートすることができるので、文章が用いている言語をある程度推測することができる。
言い換えれば、文字コードの検出というのは、実際には言語の検出に、どの言語がどの文字コードを使う傾向にあるかという知識を組み合わせたものと言える。
実際のところ、イエスだ。主要なウェブブラウザはどれも文字コードの自動検出機能を持っている。というのも、ウェブは文字コードの情報を一切宣言していないページで満ちあふれているからだ。Mozilla Firefoxには、文字コードの自動検出を行うオープンソースのライブラリが組み込まれている。私はこのライブラリをPython 2に移植し、それにchardet
モジュールという名前を付けた。この章は、chardet
モジュールをPython 2からPython 3へ移植する過程をステップバイステップで説明する。
⁂
chardet
モジュールのご紹介コードの移植に取りかかるまえに、それがどのように動作しているのかを理解してもらうのがいいだろう。この節は、コード自体を案内する短いガイドになっている。サイズが大きすぎるため、chardet
ライブラリをここに載せることはできないが、pypi.python.org/pypi/chardet
からダウンロードすることができる。
検出アルゴリズムのメインエントリポイントはuniversaldetector.py
であり、そこにはUniversalDetector
というクラスが1つだけ定義さている(メインエントリポイントはchardet/__init__.py
のdetect
関数だと思ったかもしれないが、実はそれはユーザの便宜のために用意された関数にすぎない。この関数は、UniversalDetector
オブジェクトを作成した上で呼び出し、その結果を返すという処理だけを行う)。
UniversalDetector
が扱う文字コードは5つのカテゴリに分けられる:
テキストがBOMで始まる場合は、そのテキストがUTF-8、UTF-16、UTF-32のどれかでエンコードされていると合理的に推測できる(さらにBOMはこれらのうちのどれであるかを教えてくれる。そもそもBOMはそのためのものだ)。この識別はUniversalDetector
の中でインラインで行われ、UniversalDetector
は、それ以降の処理を行わずに即座に結果を返す。
エスケープを利用した文字コードであることを示すようなエスケープシーケンスがテキストの中に見つかった場合には、UniversalDetector
がEscCharSetProber
(escprober.py
で定義されている)を作成し、そこにテキストを流し込む。
EscCharSetProber
は、HZ-GB-2312、ISO-2022-CN、ISO-2022-JP、ISO-2022-KRのモデル(escsm.py
で定義されている)に基づいた、一連の状態機械 (state machine) を作成する。EscCharSetProber
は、これらの状態機械にテキストを1バイトずつ流し込んでいく。状態機械のどれかが文字コードを一意に特定する結果になったときは、EscCharSetProber
は「文字コードを特定した」という結果を即座にUniversalDetector
に返し、UniversalDetector
はその結果を呼び出し元に返す。各々の状態機械は、その文字コードで解釈できないシーケンスに突き当たった時点で脱落させられ、以後の処理は残りの状態機械だけで続けられる。
BOMがないときは、UniversalDetector
はテキストに0x80〜0xFFのバイトが含まれていないかをチェックする。含まれている場合は、UniversalDetector
はマルチバイト文字列と、シングルバイト文字列と、最後の手段であるwindows-1252
を検出するための一連の「調査器 (prober)」を作成する。
マルチバイトエンコーディングの調査器MBCSGroupProber
(mbcsgroupprober.py
で定義されている)は、実際には各々のマルチバイト文字コード(Big5・GB2312・EUC-TW・EUC-KR・EUC-JP・SHIFT_JIS・UTF-8)の調査器からなるグループを管理するための単なるまとめ役に過ぎない。MBCSGroupProber
は、これら文字コード固有の調査器それぞれにテキストを流し込み、その結果をチェックする。調査器が、解釈できないバイト列を見つけたと報告したら、その調査器は以後の処理から脱落させられる(つまり、例えば、以後のUniversalDetector
.feed()
の呼び出しはその調査器をスキップする)。調査器の一つが、これでまず間違いないという文字コードを検出したら、MBCSGroupProber
はその結果をUniversalDetector
に報告し、さらにUniversalDetector
はこれを呼び出し元に報告する。
マルチバイト文字コードの調査器のほとんどはMultiByteCharSetProber
(mbcharsetprober.py
で定義されている)を継承している。これらの調査器は適切な状態機械と分布解析器をセットしているだけで、残りの仕事はMultiByteCharSetProber
がやっている。MultiByteCharSetProber
は、テキストを文字コード固有の状態機械に1文字ずつ流し込んでいき、「確実にこの文字コードが使われている」ないし「この文字コードでは絶対にない」ということを指し示すバイトシーケンスを探す。それと同時にMultiByteCharSetProber
は、テキストを文字コード固有の分布解析器にも流し込んでいく。
分布解析器(各々はchardistribution.py
で定義されている)は、どの文字が頻繁に使われるのかについての言語固有のモデルを持っている。MultiByteCharSetProber
が分布解析器に十分なテキストを流し込むと、「よく使われる文字」の出現回数、テキストの総文字数、言語固有の分布比に基づいて信頼度を計算する。信頼度が十分に高い場合、MultiByteCharSetProber
はその結果をMBCSGroupProber
に返し、MBCSGroupProber
はそれをUniversalDetector
に返し、UniversalDetector
はそれを呼び出し元に返す。
日本語の場合はもっと難しい。1文字の分布解析はEUC-JP
とSHIFT_JIS
を区別するのには不十分なことがあるので、SJISProber
(sjisprober.py
で定義されている)は2文字ごとの分布解析も行っている。SJISContextAnalysis
とEUCJPContextAnalysis
(両者はjpcntx.py
で定義されていて共通のJapaneseContextAnalysis
クラスを継承している)は、テキストに含まれているひらがなの頻度をチェックする。十分な量のテキストが処理されると、その信頼度がSJISProber
に返され、SJISProber
は両方の分析器をチェックして、高いほうの信頼度をMBCSGroupProber
に返す。
シングルバイトの文字コード調査器SBCSGroupProber
(sbcsgroupprober.py
で定義されている)も、他の調査器のまとめ役にすぎない。束ねられている各々の調査器は、特定の言語・文字コードの組み合わせに対応している: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
で定義されている)として実装されていて、このクラスは引数として言語モデルを受け取る。この言語モデルは、異なる2文字の組み合わせが典型的なテキストにおいてどのくらいの頻度で出現するのかを定義している。SingleByteCharSetProber
はテキストを処理し、「よく使われる2文字の組み合わせ」が文章にいくつ現れているかを数える。十分なテキストが処理されると、SingleByteCharSetProber
は、よく使われる2文字組の出現回数と、テキストの総文字数と、言語固有の分布比に基づいて信頼度の計算を行う。
ヘブライ語は特別なケースとして扱われる。2文字組の分布解析に基づいてテキストがヘブライ語だと思われる場合は、HebrewProber
(hebrewprober.py
で定義されている)を使って、ビジュアルヘブライ(ソーステキストが行ごとに「逆向き」に格納されていて、そのまま表示すれば右から左に読めるようになっている)とロジカルヘブライ(ソーステキストは読む順番で格納され、クライアントによって右から左に表示される)の判別を行う。いくつかの文字は単語の中間に現れるのか単語の末尾に現れるのかによって異なる符号化処理がなされるので、これを使えば元のテキストの方向をほぼ確実に識別することができるのだ。その上で、適切な文字コード(ロジカルヘブライ用のwindows-1255
またはビジュアルヘブライ用のISO-8859-8
)を返す。
windows-1252
UniversalDetector
が0x80〜0xFFのバイトをテキストから検出したにもかかわらず、他のマルチバイトやシングルバイトの調査器が一つも信頼できる結果を返さなかった場合は、windows-1252
でエンコードされた英文テキストかどうかを判別するためにLatin1Prober
(latin1prober.py
で定義されている)を作成する。この検出は本質的に信頼できないものでしかない。というのも、英語の文字をエンコードする方法は大抵の文字コードで同じだからだ。windows-1252
を識別するには、スマートクォート、カールしたアポストロフィ、コピーライト記号、などなどの一般的に使われる記号に頼るしかない。Latin1Prober
は、もっと正確な調査器にできるだけ勝たせてあげるために、自分の信頼度を自動的に減少させるようになっている。
⁂
2to3
を実行するこれより、chardet
モジュールをPython 2からPython 3に移植する。python 3には2to3
と呼ばれるユーティリティスクリプトが付属している。このスクリプトは、実際のPython 2のソースコードを受け取って、Python 3で動くように可能な限り自動変換してくれるものだ。変換が容易であるケース(関数の名前が変更されたとか、他のモジュールに移動されたなど)もあれば、かなり複雑なものになるケースもある。このツールがどこまでできるのかを知るには、Appendixの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
それほど大変なものではなかった。いくつかのインポート文とprint文を変換しただけだ。ところで、そもそもこのインポート文の何がまずかったのだろう。それに答えるには、chardet
モジュールがどのように複数のファイルに分割されているのかを知らなければならない。
⁂
chardet
は複数のファイルで構成されるモジュールだ。これをchardet.py
という一つのファイルにまとめることもできたのだが、そうしなかった。その代わりに、私はchardet
という名前のディレクトリを作成し、そのディレクトリの中に__init__.py
というファイルを置いた。ディレクトリの中に__init__.py
というファイルを見つけると、Pythonはそのディレクトリの中にあるファイル全体が1つのモジュールを構成しているものと解釈する。そのディレクトリ名がモジュールの名前になる。ディレクトリの中にあるファイルは、同じディレクトリにある他のファイルを参照できるし、さらにサブディレクトリの中にあるファイルを参照することもできる(詳細はすぐに述べる)。しかしそのファイルの集まり全体は、ほかの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
モジュールが単一のファイルではないことを示す1つ目の証拠だ: この「モジュール」は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検索パスにインストールされているかもしれない他のモジュールとの名前の衝突を心配しなくてもよくなる。このimport
文は、chardet/
ディレクトリの中にあるuniversaldetector
モジュールだけを探す。
これらの2つの概念、つまり__init__.py
と相対インポートは、自分のモジュールを望む限りいくつのピースにでも分解できることを意味している。chardet
モジュールは36個の.py
ファイルから構成されている。36個だ! とはいえ、このモジュールを使うのに必要な処理はimport chardet
だけであり、それだけでメインのchardet.detect()
関数を呼び出すことができるようになる。モジュールを使う側からは分からないことだが、このdetect()
関数は実際にはchardet/__init__.py
ファイルで定義されている。また、これも気がつかないことだが、detect()
関数は相対インポートを使ってchardet/universaldetector.py
で定義されているクラスを参照している。このファイルはさらに別の5つのファイルを相対インポートしていて、これらのファイルはすべてchardet/
ディレクトリにおさまっている。
☞Pythonで大きなライブラリを書いているなら(というよりも書いているライブラリが大きくなってきたら)、時間を割いて、そのライブラリをマルチファイルモジュールへとリファクタリングするといい。これはPythonが得意なことの1つなので、ぜひ活用しよう。
⁂
2to3
にはできないことを修正するFalse
is invalid syntaxさて実際のテストを行おう。自動テストフレームワークを実行してテストスイートを検証するのだ。このテストスイートは考え得るすべてのコードパスを網羅するように設計されているので、これは私たちが移植したコードのどこにもバグが潜んでいないことを確認するための良い方法だ。
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
を見てみよう。以下は2to3
によって変更される前の、元のバージョンのconstants.py
だ。
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で変更されている。Python2では、import constants
と書けば、まずはchardet/
ディレクトリからconstants
モジュールの探索が開始された。しかし、Python 3では、すべてのインポート文はデフォルトでは絶対インポートだと解釈される(従って、sys.path
上にあるモジュールしか検索されない)。Python 3で相対インポートを使いたければ、そのことを明示しなければならない。
from . import constants
しかし待って欲しい。これらは2to3
スクリプトがやってくれるはずではなかったのか? そう、2to3
はこれをやってくれるのだが、このインポート文では2種類のインポートを1行で行っている。ライブラリ内のconstants
モジュールを相対インポートしている一方で、標準モジュールとしてインストールさているsys
を絶対インポートしているのだ。Python 2ではこの2つのインポート文を1行にまとめて書くことができたのだが、Python 3ではそれが許されていない。そして、2to3
スクリプトはこのインポート文を2行に分割できるほど賢くないのだ。
解決方法はこのインポート文を手作業で分割することだ。したがって、次の2つが1つになっているインポート文は:
import constants, sys
2つの別々のインポート文にしなくてはいけない:
from . import constants
import sys
これと同じ問題は、chardet
ライブラリのあらゆる部分に見られる。ある部分では “import constants, sys
” としているし、他の部分では “import constants, re
” としている。修正方法はすべて同じだ。手作業でインポート文を2行に分割し、1つは相対インポート、もう一つは絶対インポートにすればいい。
次へ進もう!
もう一度、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()
関数が見つからないという問題の最も簡単な解決方法は、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()
を呼び出しているところまでバックトラックしてみよう。これを呼び出している場所の1つは自動テストスクリプトのtest.py
だ。
u = UniversalDetector()
.
.
.
for line in open(f, 'rb'):
u.feed(line)
ここで答えを見つけた。UniversalDetector.feed()
メソッドでは、aBufはディスクから読み込んだファイルの1つの行だ。ファイルを開くときのパラメータを注意深く見てほしい: 'rb'
だ。'r'
は「読み込み」を表す。オーケー、ここではファイルを読み込もうとしているわけだ。あっ、でも'b'
は「バイナリ」を表すものだ。もし'b'
フラグが無かったら、このfor
ループはファイルを1行ずつ読み込んで、各々の行をシステムのデフォルト文字コードに従って文字列(Unicode文字の配列)に変換することになる。しかし、'b'
フラグがあるときは、このfor
ループはファイルを1行ずつ読み込んで、各々の行を、ファイルに現れる通りにバイトの配列として格納する。そのバイト列はUniversalDetector.feed()
に渡され、最終的には0x80-0xFFの「文字」を探すためのプリコンパイルされた正規表現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
の中に2ヶ所あることが分かった。ここでもまた、文字列の正規表現を定義しながら、それをバイト列である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
はこの行のどの部分にでも起こりうるのだが、トレースバックはその正確な場所を教えてはくれない。例外が発生しているのは1つ目の条件文かもしれないし、2つ目の条件文かもしれないのだが、どちらにしても同じトレースバックが出力される。原因を絞り込むために、この行を次のように途中で分けるのがよいだろう:
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
なるほど! 問題は1つ目の条件 (self._mInputState == ePureAscii
) ではなく、2つ目の条件にあったのだ。だとすると、何が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はバイト列だ。そして文字列をバイト列と連結することはできない — たとえそれが長さ0の文字列であったとしてもだ。
それで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
に現れるが、ここでは最後の文字だけではなく、最後の2文字を追跡している。そして、このMultiByteCharSetProber
クラスは最後の2文字を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'
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'
……悪い知らせは、これがちっとも進んでいるとは感じられないことだ。
しかし、ちゃんと前に進んでいるのだ! 本当だよ! トレースバックは同じコード行を指し示しているけれども、これは前のエラーとは違うものだ。だから一歩前進だ! それで、今度の問題は何なのだろうか? さっき見たときは、このコードは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()
メソッドが呼び出されたときには発生しない。エラーは、aBufの最後のバイトがself._mLastCharに代入されたあとの2回目の呼び出しで発生するのだ。そこにどんな問題があるのだろう? 実は、バイト列から要素を一つだけ取り出すと、バイト列ではなく整数が得られるのだ。その違いを見るために、対話シェルで私についてきてほしい。
>>> 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
オーケー、つまり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は関数に渡されているだけだ。呼び出しスタックをさかのぼろう。
# utf8prober.py
def feed(self, aBuf):
for c in aBuf:
codingState = self._mCodingSM.next_state(c)
分かったかな? Python 2では、aBufは文字列だったので、cは1文字の文字列だった(文字列をイテレートすると文字が一つずつ取り出されるからだ)。しかし、ここではaBufはバイト列になっているので、cは1文字の文字列ではなくint
だ。言い換えると、cはすでにint
なので、ord()
関数を呼び出す必要はないということだ!
したがってこうすればいい:
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)]
ここではaBufをイテレートしているので、cは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()
一体全体どういうことだ? 「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()
メソッドは、aBufをそのまま渡しているわけではなく、これをスライスしている。しかし、この章の前の方で見たように、バイト列のスライスはバイト列を返すので、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'
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()
関数は2つの引数 — 関数とリスト(厳密に言うとイテレート可能なオブジェクトなら何でもいい) — を受け取り、その関数をリストの各々の要素に累積的に適用する。言い換えると、これはリストの要素を足し合わせた結果を返すための派手で遠回しな方法だ。
この奇怪な手法はとても一般的だったので、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
モジュールはバイトのストリームを文字に変換するためのプログラムだからだ。しかし、「バイトのストリーム」というものは思いのほか色々なところに現れるものだ。ファイルを「バイナリ」モードで読み込むって? それで手に入るのはバイトのストリームだ。ウェブページを取得する? ウェブ上のAPIを呼び出す? これはどれもバイトのストリームを返すのだ。
chardet
がPython 3で動作することに自信を持っている唯一の理由は、私がすべての主要なコードパスを網羅するテストスイートを作ることから始めたからだ。もしテストを一切持っていないのであれば、Python 3への移植を始める前にいくらかのテストを書こう。テストを少し持っているのであれば、もっとたくさん書こう。たくさんのテストを持っているのであれば、本当に楽しいことが始められる。
© 2001– Mark Pilgrim
© Fukada, Fujimoto(日本語版)