15.1. 没頭しよう
質問: web上で、メールボックスで、これまでに書かれたコンピュータシステムで、テキストが文字化けする理由の第一位は何でしょうか?文字エンコーディングです。文字列の章で、文字エンコーディングの歴史と"1つのエンコーディングですべてを支配する" Unicodeの誕生について話をしました。すべてのオーサリングシステムが正確なエンコーディング情報を持っていて、すべての転送プロトコルがUnicodeを認識し、テキストを扱うすべてのシステムが完璧にメンテナンスされていて、webページで2度と汚い文字を見なくなれば、それでよかったのですが。
私はポニー(※)も好きなのです。
Unicodeのポニーです。
言うなれば、ユニポニーです。
文字エンコーディングを自動で検出してみましょう。
(※訳注 ポニー: 言葉を1対1で訳していくこと。スラング)
15.2.文字エンコーディングの自動検出とは?
何かわからない文字エンコーディングのバイトを受け取っても読めるように、エンコーディングを決めるということです。解読キーを持っていないコードのクラッキングに似ています。15.2.1.そんなことは不可能なのでは?
一般的には、不可能です。しかし、特定の言語に対して最適化されたエンコーディングというものがありますし、言語というものはランダムではありません。ある文字の並びが頻繁に使われる一方で、他の組合せは意味をなさないのです。英語が流暢な人が新聞を広げて"txzqJv 2!dasd0a QqdKjvz"という文字を見つけたら、直ぐにそれは英語ではないことに気がつくでしょう(アルファベットで書かれているとしても)。 "よくある"テキストを数多く学ぶことで、コンピュータアルゴリズムはこのような流暢さを真似することができ、テキストの言語を教えられたとおりに推定できるようになります。
言い換えるならば、エンコーディング検出が実際にやっているのは、どの言語がどのエンコーディングを使っているか、という知識を組み合わせた言語の検出なのです。
15.2.2. そのようなアルゴリズムはあるのだろうか?
このあと出てくるように、あります。すべてのメジャなブラウザはエンコーディングの自動検出を備えています。なぜなら、webはエンコーディング情報の無いページで満ち溢れているからです。Mozilla Firefoxはオープンソースのエンコーディング自動検出ライブラリを実装しています。私はそのライブラリをPython2にポートしてchardetモジュールに書き込みました。この章ではchardetモジュールをPython2からPython3にポートする過程を1つずつ見ていきます。15.3. chardetモジュールの紹介
コードをポートする前に、コードがどんな風に動いているかを理解しておくとよいでしょう!こちらはコードを紹介するガイドです。chardetライブラリは大き過ぎるためここに書けませんが、chardet.feedparser.orgからダウンロードすることができます。検出アルゴリズムの開始場所はuniversaldetector.pyで、ここにはUniversalDetectorというクラスがあります。(chardet/_ init.pyが開始場所だと思ったかもしれませんが、そこは単にUniversaiDetectorオブジェクトを作成するための簡易の関数で、呼び出すとその結果を返します。 )
UniversalDetectorは5つのエンコーディングのカテゴリーを扱います。
1. UTF-Nでバイトオーダーマーク(BOM)つきのもの。これはUTF-8、ビッグエンディアン・リトルエンディアンのUTF-16、UTF-32のすべての4バイト変数を含んでいます。
2. エスケープエンコーディング。7bit ASCIIに互換性のあるもので、非ASCII文字はエスケープシーケンスで始まります。
3. マルチバイトエンコーディング。各文字は可変数のバイトで表されます。例:big5 (中国語)l shift_jis (日本語), eucーkr (韓国語), and UTF-8 BOMなし.
4. シングルバイトエンコーディング。文字が1バイトで表されているもの。例:koi8-r (Russian), windows-1255 (Hebrew), and tisー620 (Thai)
5. Windows-1252。主にMicrosoft Windowsで、文字エンコーディングのことなど全く意に介さない中間管理職によって使われています。
15.3.1. BOMつきのUTF-N
BOMで始まるテキストであれば、テキストエンコーディングがUTF-8、uft-16、UTF-32のいずれかだと理由付きで推定することができます。(BOMはどのエンコーディングであるかを教えてくれます。そのためにあるのですから。) UniversalDetectorの行内で扱われて、他のプロセスなしに即座に結果を返します。15.3.2. エスケープド・エンコーディング
テキストがエスケープド・エンコーディングであると示すエスケープシーケンスを持っていると認識されたとき、UniversalDetectorはEscCharSetProberを作成して(escprober.py内で定義される)テキストの中にフィードします。EscCharSetProberはHZ-GB-2312、ISO-2022-CN、ISO-2022-JP、ISO-2022-KR (escsm.py内で定義される)のモデルに基づいてステートマシンの列を作成します。EscCharSetProberは各ステートマシンにテキストを1度に1バイトずつフィードします。ステートマシンがエンコーディングを特定して終了したときは、EscCharSetProberは即座にUniversalDetectorにプラスの結果を返し、UniversalDetectorはその結果を呼び出し側に返します。ステートマシンで違反したシーケンスが見つかったときは無視されて、他のステートマシンへと進みます。
15.3.3. マルチバイト・エンコーディング
BOMがないときは、UniversalDetectorはテキストに高ビットの文字が含まれていないかを確認します。もし含まれているならば、"probers"の列を作ってマルチバイトエンコーディング、シングルバイトエンコーディング、そして最後の手段であるWindowsー1252を検出します。MBCSGroupProber (マルチバイト・エンコーディング検出器、 mbcsgroupprober.py内で定義)は、実は単なるshellなのですが、他の検出器を管理しています。検出器は各マルチバイトエンコーディングに対応しています。対応しているのは、big5、gb2312、euc-tw、euc-kr、euc-jp、shift_jis、UTFー8です。MBCSGroupProberはテキストを各エンコーディング専用の検出器に送って結果を確認します。違反したバイトシーケンスを見つけたと検出器が報告してきたら、それ以降のプロセスには進みません。(つまり、例えばUniversalDetector.feed()を呼び出すものは検出器をスキップします。)もし検出器がエンコーディングを発見したと理由付きで自信を持って報告してきたならば、MBCSGroupProberはこのポジティブな結果をUniversalDetectorに送り、UniversalDetectorは呼び出し側に結果を報告します。
マルチバイトエンコーディング検出器のほとんどはMultiByteCharSetProber (mbcharsetprober.py内で定義される)から派生していて、適切なステートマシンと分布アナライザを中継し、残りの仕事をMultiByteCharSetProberにさせるのです。MultiByteCharSetProberは、テキストをエンコーディング特有のステートマシンの中で1バイトずつ確認して、結果の真偽を決定づけるバイトシーケンスを探します。同時に、MultiByteCharSetProberはエンコーディング特有の分布アナライザにテキストを送ります。各エンコーディングに対する分布アナライザはchardistribution.py内で定義されていて、どの文字が多用されているかという言語特有のモデルを使います。MultiByteCharSetProberが分布に充分なテキストを送ったなら、頻繁に使われる文字、 トータル文字数、言語特有の分布割合に基いて確かさのレーティングを計算します。確かさが高いならば、MultiByteCharSet Proberは結果をMBCSGroupProberに返して、UniversalDetector、呼び出し側という順に返されます。
日本語の場合はもっと難しいです。1文字の分布分析ではEUC-JPとSHIFT-JISを見分けるのには充分ではないので、SJISProber (sjisprober.py内で定義)では2文字分布の分析をしています。SJISContextAnalysisとEUCJPContextAnalysisは、テキスト中のひらがなを確認しています(これらはどちらもjpcntx.py内で定義されていて、共通のJapaneseContextAnalysisクラスから派生しています)。充分な量のテキストを調べたあと、確かさのレベルをSJISProberに返します。SJISProberは両方のアナライザの結果を比較して高い方の確かさのレベルをMBCSGroupProberに送ります。
15.3.4. シングルバイトエンコーディング
シングルバイトエンコーディング検出器SBCSGroupProberはsbcsgroupprober.py内で定義されていて、他の検出器のグループを管理するshellです。その検出器はそれぞれシングルバイトエンコーディングと言語の組合せになっています。windows-1251、KOIS-R、ISO-8859-5、MacCyrillic、IBM855、and IBM866 (ロシア語); lS0-8859-7 and windows-1253 (ギリシア語); IS0-8859-5 and windows-1251 (ブルガリア語); ISO-8859-2、windows-1250 (ハンガリー語); TIS-620 (タイ語); windows-1255 and IS0-8859-8 (ヘブライ語)SBCSGroupProberはこれらの特定のエンコーディングと言語の検出器にテキストを送り、結果を確認します。これらの検出器はすべて1つのクラスとして実装されています。SingleByteCharSetProber (sbcharsetprober.py内で定義される)は、言語モデルを引数にとります。言語モデルは2文字のシーケンスがあるテキストの中にどれだけ現れるかを定義します。SingleByteCharSetProberはテキストを調べて最も多く使われた2文字のシーケンスを数えます。充分にテキストが調べられたら、より多く使われたシーケンスの数、全文字数、言語特有の分布度合いに基いて確実さのレベルを計算します。
ヘブライ語は特殊なケースとして扱われます。2文字の分布分析によってテキストがへブライ語であると判ったら、HebrewProber(hebrewprober.py)が Visual Hebrew(ソーステキストが実際"後ろ向きに"1行ずつ保存されて、文字どおり後ろ向きに表示されて右から左に読める)と、Logical Hebrew (ソーステキストが左から右に保存されていて、クライアントで右から左向きにレンダリングされる)に区別します。ある文字が単語の途中で現れるか最後で現れるかによって区別してエンコーディングされるので、ソーステキストの向きについては理由をつけて推定することができ、適切なエンコーディングを返すことができます(Logical Hebrewlにはwindows-1255、または Visual HebrewにはISO-8859-8)
15.3.5. windows-1252
UniversalDetectorによってテキスト中で高ビット文字が検出されたものの、マルチバイト、シングルバイトエンコーディング検出器がどれも自信のある結果を返さなかった場合、Latinl Prober(latinlprober.py内で定義)を作ってwindows-1252エンコーディングで英語を検出しようとします。本来は、この方法では読むことができません。なぜなら英語の文字は他の多くのエンコーディングと同じ様にエンコードされているからです。windows-1252を見分ける唯一の方法としては、共通して使われる記号であるスマートクオーツ、カーリーアポストロフィ、コピーライトシンボルを使います。Latin1Proberは自動的に確証度を減らして、可能な限り正しい検出器が採用されるようにします。15.4. 2to3を走らせる
chardetモジュールをPython2からPython3へ移植しましょう。Python3には2to3というユーティリティがあって、Python2のソースコードを可能な限り自動でPython3に変換してくれます。簡単なものでは関数の名前を変更したり他のモジュールに移したりしますが、とても複雑なものもあります。2to3で可能なことをすべて知りたい場合は、appendixを参照してください(Porting code to Python3with 2to3)。この章では、chardetパッケージで2to3を走らせてスタートしますが、あとで見ていくように、自動ツールがマジックを披露したあとにもやることが沢山あります。chardetのメインパッケージはいくつかのファイルに分かれていて、すべて同じディレクトリに入っています。
2to3スクリプトでは、複数ファイルを変換することも簡単にできます。コマンドライン引数としてディレクトリを与えるだけで、2to3はファイルを順に変換します。
テストハーネスであるtest.pyで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
さて、そんなに難しくはありませんでした。\home\chardet>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
変換にいくつかimportとprint文を使っただけです。
import宣言といえば、問題が<i>あった</i>のはどこでしょうか?
その問いに答えるためには、chardetモジュールがどのように複数ファイルに分かれているかを理解する必要があります。
15.5. 複数ファイルに分けるモジュールへ少し寄り道
chardetはマルチファイルモジュールです。1つのファイル(chardet.pyという名前)にすべてを入れてもよかったのですが、そうしませんでした。代わりに、ディレクトリ(chardet)を作って、その中に__init__.pyファイルを作りました。Pythonは、ディレクトリ内で__init__.pyファイルを見つけた場合は、そのディレクトリ中のファイルはすべて同じモジュールの一部だと推定します。モジュールの名前はディレクトリ名になります。ディレクトリ中のファイルは同一ディレクトリ内の他のファイルを参照することができ、サブディレクトリの中であっても参照可能です(すぐあとで説明します。)
一方、ファイル全体の集まりは他のPythonコードから見ると1つのモジュールとなります。1つのpyファイルにすべての関数とクラスが入っているようなものです。
__init__.pyファイルはどうなったのでしょうか?何もない。すべてある。その間の何かです。__init__.pyファイルは定義が必要ありません。文字どおり空のファイルなのです。mainエントリポイント関数として定義するために使うこともできます。中にすべての関数を入れることもできます。1つだけを入れることもできます。
👉__init__.pyファイルの入ったディレクトリは常にマルチファイルモジュールとして扱われることになります。__init__.pyファイルがないときは、ディレクトリは単に関係のない.pyファイルのディレクトリとして扱われます。
実際にどう動くのか見てみましょう。
①chardetモジュールには、通常のクラス属性に加えてdetect()関数が入っています。>>> import chardet >>> dir(chardet) # ① ['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', '__version__', 'detect'] >>> chardet # ②
②これがchardetモジュールが単一のファイルではない最初の手がかりになります。"module"はchardet/ディレクトリの__init__.pyファイルの中にあるのです。
__init__.py ファイルの中を見てみましょう。
①__init__.pyファイルはdetect()関数を定義します。これはchardetライブラリのメイン入り口です。def detect(aBuf): # ① from . import universaldetector # ② u = universaldetector.UniversalDetector() u.reset() u.feed(aBuf) u.close() return u.result
②一方、detect()関数にはコードが一切ありません!
実は、この関数がやっているのはuniversaldetectorをimportして使い始めることだけなのです。universaldetectorはどこで定義されていたのでしようか?
答えは、奇妙に見えるimport宣言にあります。
翻訳するならば、「自分と同じディレクトリにあるuniversaldetectorモジュールをimportせよ」となります。"自分"というのはcardet/__init__.pyファイルです 。これは相対importと呼ばれるものです。マルチファイルモジュールにあるファイルをお互いに参照するための方法で、importサーチバスの中でインストールした他のモジュールとの名前の衝突を気にせずに参照できます。このimport文はchardet/ディレクトリにあるuniversaldetector モジュールだけを探します。from . import universaldetector
この2つの考え方 一 __init__.py、相対import 一は、モジュールを好きなように多くのパーツに分割できることを意味しています。chardetモジュールは36の.pyファイルから出きています。36です!使うときにはimport chardetとするだけで、chardet.detect()関数を呼び出すことができます。
あなたのコードが気がつかないうちに、実際にはdetect()関数がchardet/__init__.pyファイルを定義していたのです。また、あなたが気がつかないうちに、detect()関数は相対importを使ってchardet/universaldetector.pyに定義されたクラスを参照し、そのクラスは次に5つの他のファイルを相対importします。これらのファイルはすべてchardet/ディレクトリの中で起こっています。
👉巨大なPythonライブラリを書いていると認識したとき(むしろ、小さいライブラリが大きく育ったと気がついたとき)、時間をとってリファクタリングし、マルチファイルモジュールにするとよいでしょう。Pythonの沢山ある得意なことの1つですので、その恩恵に預かりましょう。
15.6. 2to3にできないことを修正する
15.6.1. Falseはinvalid syntaxである (文法エラー)
ここからは本当のテストです。テストスイートに対するテストハーネスを走らせます。テストスイートはすべての可能性のあるコードの経路をカバーするように設計されているので、ポートされたコードをテストしてバグがどこにもないことを確かめるのがよいでしょう。C:\home\chardet> python test.py tests\*\* Traceback (most recent call last): File "test.py", line 1, in
from chardet.universaldetector import UniversalDetector File "C:\home\chardet\chardet\universaldetector.py", line 51 self.done = constants.False ^ SyntaxError: invalid syntax
おや、小さなつまづきがあります。Python3ではFalseは予約語ですので、変数としては使えません。constants.pyを調べてどこで定義されているかを見てみましょう。
これが2to3が変換する前の元のconstants.pyです。
この部分のコードは、ライブラリが古いバージョンのPython2でも動作するように書かれています。import __builtin__ if not hasattr(__builtin__, 'False'): False = 0 True = 1 else: False = __builtin__.False True = __builtin__.True
Python2.3より前では、Pythonにビルトインのbool型はありませんでした。
このコードがビルトイン定数のTrue、Falseがないことを検知して必要に応じて定義しています。しかし、Python3では必ずbool型がありますので、このコードスニペット全体が不要になります。
一番シンプルな解法としては、constants.True、constants.FalseというインスタンスをすべてTrue、Falseに置き換えます。そしてconstants.pyからこの死んだコードを削除します。
universaldetector.pyにある
はself.done = constants.False
self.done = False
となります。どうですか、満足ではないでしょうか?これでコードは短く、読みやすくなっています。
15.6.2. constantsというモジュールはない
test.pyを再度走らせてどうなるかを調べてみましょう。どう書かれていますか?<code>constants</code>という名前のモジュールはありませんか?もちろん、constantsという名前のモジュールはあります。まさにchardet/constants.pyにあるのです。C:\home\chardet> python test.py tests\*\* Traceback (most recent call last): File "test.py", line 1, in
from chardet.universaldetector import UniversalDetector File "C:\home\chardet\chardet\universaldetector.py", line 29, in import constants, sys ImportError: No module named constants
2to3スクリプトが重要なimport文をすべて変えてしまったことを覚えていますか?
このライブラリはたくさんの相対importを持っています、つまり、同じライブラリ内で、モジュールが別のモジュールをインポートします。しかし相対importのロジックはPython3で変更されました。Python2では、単に<code>constants</code>をインポートすると、Pythonはchardet/ディレクトリを先に訓べていました。Python3では、デフォルトではimport文はすべて絶対指定になります。Python3で相対インポートをしたいときは、明示的にする必要があります。
待ってください。2to3スクリプトはこの場合も対応するはずではなかったでしょうか?そのはずです。しかしこの特別なimport文は、2つの異なるimportを1行に組み合わせています。ライブラリ内の定数モジュールの相対インポート、そしてPython標準ライブラリにプリインストールされたsysモジュールの絶対インポートです。Python2では、これらを1つのimport文に組み合わせることができました。しかし、Python3ではできません。2to3スクリプトはimport文を2つに分けられるほど賢いものではないのです。from . import constants
解決策は、import文を手動で分割することです。この1行に2つあるimportは、
import constants, sys
このように分割します。
from . import constants import sys
chardetライブラリの中にはこの種の問題がいろいろあります。
あるところでは、<code>"import constants, sys"</code>、またあるところでは<code>"import constants, re"</code>というようにです。修正方法は同じです。手動でimport文を2行に分けます。1つは相対importに、もう1つは絶対importにします。
先に進みましょう!
15.6.3. 名前'file'が定義されていない
ここでまた、test.pyを実行してテストケースを実行してみましょう...この結果は驚きです。覚えている限りではずっとこのイディオムを使ってきたのですから。Python2では、グローバルfile()関数はopen()関数のエイリアスで、テキストを読むために開くときの標準的な方法でした。Python3では、グローバルfile()関数は存在していませんが、open()関数は残っています。C:\home\chardet> python test.py tests\*\* tests\ascii\howto.diveintomark.org.xml Traceback (most recent call last): File "test.py", line 9, in
for line in file(f, 'rb'): NameError: name 'file' is not defined
つまり、このfile()がない問題の一番簡単な解法は、代わりにopen()関数を呼べばよいのです:
となります。for line in open (f, 'rb') :
これだけで大丈夫です。
15.6.4. 文字列パターンをbyteライクオブジェクトに使えない
さて面白くなってきました。ここでの"面白い"というのは、"最高に混乱している"という意味です。これをデバッグするために、self._highBitDetectorが何なのかを見てみましょう。UniversalDetectorクラスの__init__メソッドに定義されています。C:\home\chardet> python test.py tests\*\* tests\ascii\howto.diveintomark.org.xml Traceback (most recent call last): File "test.py", line 10, in
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
これはプリンコンパイルされた正規表現で、範囲が128-255(0x80-0xFF)の非ASCII文字を探すものです。待ってください、それは正しくありません。class UniversalDetector: def __init__(self): self._highBitDetector = re.compile(r'[\x80-\xFF]')
使う用語に対して、もっと厳密になる必要があります。
このpatternは範囲が128-255の非ASCII<i>バイト</i>を探します。
そして、問題はそこにあるのです。
Python2では、文字列はバイトのアレイで、文字エンコーディングは別々に記録されていました。Python2に文字エンコーディングを記録させたければ、代わりにUnicode文字列(u'')を使えぱよかったのです。一方、Python3では文字列は常にPython2でいうところのUnicode文字列です。つまり、(バイト長が可変の)Unicode文字のアレイです。この正規表現は1つの文字列パターンで定義されていますが、この文字列一ここでも文字のアレイーを探すのにだけ使えます。しかし、探しているのは文字列ではありません。バイトアレイです。tracebackを見てみると、このエラーはuniversaldetector.pyで起きています。
aBufとは何でしょうか?もう少し逆上ってみて、aBufがUniversalDetector.feed()を呼び出す場所を探してみましょう。呼び出している場所はテストハーネス、test.pyです。def feed(self, aBuf): . . . if self._mInputState == ePureAscii: if self._highBitDetector.search(aBuf):
ここに知りたい答えがあります。UniversalDetectar.feed()メソッドでは、aBufはディスク上のファイルから読んだライン1行です。ファイルを開くためのパラメータを注意深く見てみましょう。'rb'です。'r'は"read"です。0K、これは重要です、ファイルを読んでいるのです。おっと、一方'b'は"binary"の意味です。'b'がなければ、このforループはファイルを1行ずつ読んで、システムのデフォルトの文字エンコーディングに従って各行を文字列、つまりUnicode文字のアレイに変換します。'b'があるときは、このforループはファイルを1行ずつ読んで、ファイルに現れるそのまま、つまりバイトのアレイとして保存します。上位ビット・・・つまり文字を探すために、バイトアレイはUniversalDetector.feed()に渡されて、最終的にプリコンパイルされた正規表現self._highBitDetectorに渡されます。ところが、このとき文字がありません。バイトです。u = UniversalDetector() . . . for line in open(f, 'rb'): u.feed(line)
この正規表現で探したいものは、文字のアレイではなく、バイトのアレイなのです。
そこがわかれば、解決は難しくありません。文字列で定義された正規表現は文字列を探すことができます。バイトアレイで正規表現はバイトアレイを探すことがで
きます。バイトアレイのpatternを定義するためには、単純に正規表現を定義するときに使う引数の型をバイトアレイに変えれぱよいのです。(まさに次の行でも、
同じ問題が別に起きています)
reモジュールの他が使われている場所をコードベース全体を探してみるとcharsetprober.py内であと2つのインスタンスが見つかります。ここでも、コー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()
ドは文字列の正規表現を定義していますが、実行されているのはバイトアレイ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
15.6.5. bytesオブジェクトは自動で文字列に変換されない
ますます興味深いことに・・・ここでは、不運にもコードスタイルとPythonインタプリタが衝突しています。TypeErrorはどこにでも現れますが、tracebackはそれがどこにあるのかを教えてはくれません。最初の条件にあるかもしれないし、2番目かもしれませんが、tracebackは同じように見えます。場所を絞るために、行をこのように半分にします。C:\home\chardet> python test.py tests\*\* tests\ascii\howto.diveintomark.org.xml Traceback (most recent call last): File "test.py", line 10, in
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
testを再実行します。elif (self._mInputState == ePureAscii) and \ self._escDetector.search(self._mLastChar + aBuf):
やりました!問題は最初の条件(self._mInputState == ePureAscii)ではなくて、2番目でした。では何がそこでTypeErrorを起こしたのでしょうか?search()メソッドが違う型の値を期待していたと考えるかもしれませんが、それではこのtracebackは起きません。Python関数はどんな値でも受け取ることができます。つまり正しい数の引数を渡すと、関数は実行されます。想定と異なる型の値を渡すとクラッシュするかもしれませんが、そのときはtracebackはどこか関数の中を指すはずです。しかし、今回のtracebackはsearch()メソッドを呼び出すところまでは到達していないと言っています。つまり問題は、値をコンストラクトして、最終的にその値をsearch()メソッドに渡している操作の部分にあるはずです。C:\home\chardet> python test.py tests\*\* tests\ascii\howto.diveintomark.org.xml Traceback (most recent call last): File "test.py", line 10, in
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
前のデバッグの例<link>15.6.4.</link>からaBufはバイトアレイだとわかっています。
ではself._mLastCharとは何でしょうか?インスタンス変数で、reset()メソッドの中に定義されています。reset()は実は__init__()メソッドから呼ばれるます。
これで答えが出てきました。わかりましたか?self._mLastCharは文字列ですが、aBufはバイトアレイです。そのため文字列とバイトアレイは結合できません。文字列の長さが0であってもできないのです。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とは何なのでしょうか?feed()メソッドでは、tracebackが起きた場所から数行下にあります。
呼び出し関数はfeed()メソッドを1度に数バイトずつ、繰り返し呼び出します。メソッドは与えられたバイト(aBufとして渡される)を処理して、次の呼び出しのときに必要になるかもしれないので最後のバイトをself._mLastCharに保存します。(マルチバイトエンコーディングでは、feed()メソッドは文字の半分で呼び出されることになるかもしれません、そのときは残り半分を呼び出します。)ところが、aBufは今は文字列ではなくバイトアレイですので、self.mLastCharもまたバイトアレイである必要があります。このようになります。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] self._mLastChar = aBuf[-1]
"mLastChar"を全コードベースで探すと、mbcharsetprober.pyでも同様の問題が出てきますが、最後の文字を追跡する代わりに、最後の<i>2文字</i>を追跡します。MultiByteCharSetProberクラスは1文字の文字列のリストを使って、最後の2文字を追跡します。Python3では、整数のリストを使う必要があります。実際に探すのは文字ではなくてバイトだからです。(バイトは単に0-255の整数です。)def reset(self): . . . - self._mLastChar = '' + self._mLastChar = b''
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]
15.6.6. 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
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)を結合しませんでした。実は、<link>self._mLastCharがバイトアレイであると確認するため</link>にこれまで多くの時間を使ってしまったのです。どうやって整数にしたのでしょうか?
答えは前のコード行にはありません。こちらの行にあります。
このエラーは、feed()メソッドがはじめて呼び出されたときには起きませんでした。self.mLastCharがaBufの最後のバイトとしてセットされて<i>2回目</i>に起きます。では、これの何が問題なのでしょうか?バイトアレイから1つの要素を取り出すと、バイトアレイではなく整数になります。違いを比べるために、対話型シェルで見ていきます。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]
①長さ3のバイトアレイを定義します。>>> aBuf = b'\xEF\xBB\xBF' # ① >>> len(aBuf) 3 >>> mLastChar = aBuf[-1] >>> mLastChar # ② 191 >>> type(mLastChar) # ③
>>> mLastChar + aBuf # ④ Traceback (most recent call last): File " ", line 1, in TypeError: unsupported operand type(s) for +: 'int' and 'bytes' >>> mLastChar = aBuf[-1:] # ⑤ >>> mLastChar b'\xbf' >>> mLastChar + aBuf # ⑥ b'\xbf\xef\xbb\xbf'
②バイトアレイの最後の要素は191です。
③これは整数です。
④整数とバイトアレイの結合はできません。universaldetector.pyで出てきたようなエラーが再現しました。
⑤このように修正します。バイトアレイの最後の要素をとるのではなく、 リストをスライスして最後の要素だけを含んだ新しいバイトアレイを作ります。つまり、最後の要素から始まってバイトアレイの最後までスライスを続けるのです。これでmLastCharは長さ1のバイトアレイになりました。
⑥長さ1のバイトアレイと長さ3のバイトアレイを結合すると、長さ4の新しいバイトアレイになります。
では、universaidetector.pyにあるfeed()メソッドがどれだけ頻繁に呼び出されても動作することを確認するためには、<link>self._mLastCharを長さ0のバイトアレイに初期化</link>して、バイトアレイのままであるかを確認することになります。
15.6.7. ord()は長さ1の文字列を期待していたのに、整数が見つかったself._escDetector.search(self._mLastChar + aBuf): self._mInputState = eEscAscii - self._mLastChar = aBuf[-1] + self._mLastChar = aBuf[-1:]
もう疲れてしまいましたか?もう少しです。
そう、cは整数ですが、ord()関数は1文字の文字列を受け取ろうとしていました。そのとおり。cはどこで定義されていますか?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
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
これは関数に渡されただけで、答えの助けになりません。分解してみましょう。# 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)]
わかりましたか?Python2では、aBufは文字列なので、cは1文字の文字列でした。# utf8prober.py def feed(self, aBuf): for c in aBuf: codingState = self._mCodingSM.next_state(c)
(文字列をイテレートすると、1文字ずつ一すべての文字が得られます。)
しかし今は、aBufはバイトアレイですので、cは1文字の文字列ではなく整数です。
言い換えると、cは最初から整数なのでord()関数を呼ぶ必要はないのです!
つまりこうなります。
インスタンスord(c)を全コードベースで探すと、sbcharsetprober.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)] + byteCls = self._mModel['classTable'][c]
# sbcharsetprober.py
latin1prober.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)]
cはaBufをイテレートしているので整数であり、1文字の文字列ではありません。解決法は同じです。ord(c)を単なるcに変更すればよいのです。# latin1prober.py def feed(self, aBuf): aBuf = self.filter_with_english_letters(aBuf) for c in aBuf: charClass = Latin1_CharToClass[ord(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]
15.6.8. Unorderabletypes:intO>=str()
もう1度やってみましょう。これはどういうことでしょうか?"Unorderable types"とは何でしょうか?ここでもまた、バイトアレイと文字列の違いが生まれています。コードを見てみましょう。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
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()
aStrはどこから来たのでしょうか?スタックを抜き出してみましょう。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
見てください、古い友人のaBufです。def feed(self, aBuf, aLen): . . . i = self._mNeedToSkipCharNum while i < aLen: order, charLen = self.get_order(aBuf[i:i+2])
この章で出てきた他の問題から推測したかもしれませんが、aBufはバイトアレイです。
ここでfeed()メソッドはaBufをそのまま受け渡しているのではありません。スライスしています。しかし、15.6.6 で出てきたように、バイトアレイのスライスはバイトアレイを返します。そのためget_order()メソッドへと渡されるaStrパラメータは、やはりバイトアレイです。
このコードはaStrを使って何をしようとしているでしようか?バイトアレイの最初の要素を受け取って、長さ1の文字列と比較しています。Python2では、これは動作します。aStrとaBufは文字列だからです。aStr[0]は文字列となり、文字列と等しいかを比べることができるでしょう。しかしPython3では、aStrとaBufはバイトアレイであり、aStr[0]は整数です。そのため整数と文字列が等しいかは、どちらかを明示的に変換しない限り比較できません。
この場合、明示的な変換を追加してコードをもっと複雑にする必要はありません。
aStr[0]は整数になりますので、比較しようとしているものはすべて定数です。
1文字の文字列から整数に変更してみましょう。合わせて、aStrをaBufに変えましょう。実際は文字列ではありませんから。
ord()関数の存在を全コードベースで探すことで、同じ問題がchardistribution.pyで見つかります。(特に、これらのクラスです。EUCTWDistributionAnalysis、EUCKRDistributionAnalysis、GB2312DistributionAnalysis、Big5DistributionAnalysis、SJISDistributionAnalysis、EUCJPDistributionAnalysis )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
各場合において、修正はjpcntx.pyでEUCJPContextAnaiysisやSJISContextAnalysisクラスにしたものと似ています。
15.6.9. グローバル名'reduce'が定義されていない
もう一度、違反を見ていきましょう。公式ガイドWhat's New In Python 3.0によると、reduce()関数はグローバル名前空間から外されてfunctoolsのモジュールになりました。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
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
このガイドによると「必要であればfunctools.reduce()を使ってください。しかし、99パーセントの場合では判り易いforループの方が読みやすいです」Guido van Rossumのブログでその決定について読むことができます(The fate of reduce() in Python 3000)
reduce()関数は2つの引数、関数とリストをとります(厳密に言うと、どんなイテラブルオブジェクトでもそうです)。そして関数をリストの各項目に対して累積的def get_confidence(self): if self.get_state() == constants.eNotMe: return 0.01 total = reduce(operator.add, self._mFreqCounter)
に適応していきます。言いかえれば、リストの項目をすべて繰り返し足していって結果をだす方法なのです。
Pythonがグローバルsum()関数を追加したのでこのように巨大になるのは普通のことでした。
operatorモジュールをもう使っていませんので、そのimportをファイルの先頭部分から削除することができます。def get_confidence(self): if self.get_state() == constants.eNotMe: return 0.01 - total = reduce(operator.add, self._mFreqCounter) + total = sum(self._mFreqCounter)
I CAN HAZ TESTZ?テストを通過できるでしょうか?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
15.7. まとめ
何を学びましたか?- 大量のコードをPython2からPython3にポートするのは痛みを伴います。逃げ道はありません。大変です。
- 自動化された2to3ツールは役に立つところもありますが、簡単な部分関数、モジュールのリネームや、文法の変更だけに適用できます。素晴らしい技術なのですが、結局のところは賢く検索して置換するボットに過ぎないのです。
- このライブラリのポートにおける1番の問題は、文字列とバイトの違いでした。chardetライブラリにおける問題点はバイトストリームを文字列に変換することだけだったため、明確でした。しかし"バイトストリーム"は想定外のものになったりもします。ファイルを"バイナリモード"で読んだ場合はどうでしょうか?バイトストリームを受け取ります。WEBページのフェッチではどうでしょうか?WEB APIの呼び出しではどうでしょうか?このときもパイトストリームが返ってきます。
- "あなたが"プログラムを理解する必要があります。すみずみまでです。自分が書いたのものが望ましいですが、最低限としてプログラムの癖や、カビ臭い隅っこまで熟知している必要があります。
- テストケースはとても重要です。テストせずにポートしてはいけません。chardetがPython3でも動くという自信があるとすれば、テストスイートから始めてすべての圭要なコードパスを調べたからです。テストがないならぱ、Python3にポートし始める前にテストを書きましょう。いくつかテストがあるならば、もっと書きましょう。たくさんのテストがあれば、本当の楽しみが始まります。
0 件のコメント:
コメントを投稿