名前欄がすべて四角 — 判定ツールを作る
目標
顧客が送ってきたファイルのエンコーディングを推測せずに判定するツールenc_probe.pyを作ります。BOMとUTF-8のデコード可否から始めて、候補エンコーディングとサンプルの照合へ進み、二重エンコーディングを復元し、復元できないファイルを見分け、正規化と目に見えない文字が比較をどう壊したかを、自分のデータで証明します。
なぜ重要なのか
テキストファイルの中には、エンコーディングが書かれていません。そのため、「どのエンコーディングか」という問いには、いつも手順でしか答えられません。エディターのメニューを押してみる方式は、ファイルが3つのときにしか通用せず、何より、当たったかどうかを目だけで判断させます。 判定が終わると、ファイルは3種類に分かれます。正しく読めるもの、一度誤って読まれて固まったが元に戻せるもの、そして、文字が潰れて元のバイトがわからないものです。最後の種類にしがみついて時間を使うのは、努力ではなく無駄です。そのファイルは取り直す必要があります。この区別をコードでできてこそ、顧客に何を依頼するかを言えます。 そして、文字は問題なく見えるのに合わない場所があります。同じハングルが、完成形と字母分解形に分かれて保存されると、画面は同じでバイトは違います。目に見えないNBSPとゼロ幅スペースも同じです。結合結果が0件なのに原因が画面に見えない状況は、ここから生まれます。 採点ツールは、提出された文言を信じません。一時ディレクトリに、採点ツールが作ったファイルを用意し、作成したツールを実際に実行して判定を照合します。名前と行数は、実行ごとに変わります。
ステップ
- /root/enc/gen_inbox.pyを作成して実行し、inboxディレクトリの下に7つのファイルを作成してください(保存先: /root/enc/inbox)。同じ24人の名簿が、7つの状態で保存されます。
- /root/enc/enc_probe.pyの
detectを作成し、BOM・UTF-8のデコード可否・候補エンコーディング・サンプル照合の順序でエンコーディングを判定させてください。 decodeを追加し、判定したエンコーディングで読んでUTF-8で標準出力に出させ、受信箱全体を同じ名前で統一して保存してください(保存先: /root/enc/norm)。repairを追加して二重エンコーディングを復元させ、detectがそうしたファイルに対してmojibakeをtrue、verdictをmojibakeと答えるようにしてください。detectにlossyを追加して、U+FFFDが残ったファイルのverdictをlostにし、受信箱の判定を書いてください(保存先: /root/enc/lost.json)。keysを追加して、名前の列をNFCに正規化した比較用のキーとして出力させ、NFDバージョンとUTF-8バージョンの名前が、正規化の前後で何件合うかを書いてください(保存先: /root/enc/join_report.json)。scanを追加して目に見えない文字をコードポイントごとに数えさせ、keysがそれらを取り除くようにしてから書いてください(保存先: /root/enc/invisible_report.json)。- 受信箱の7つのファイルを一度に処理して、/root/enc/enc_report.jsonと、/root/enc/enc_report.mdを作成してください。
参考
- 実行契約:
python3 /root/enc/enc_probe.py <명령> <파일>(プレースホルダーは順に、コマンドとファイルです)。コマンドはdetect・decode・repair・keys・scanの5つです。成功すれば終了コード0、復元できないか読めなければ3、使い方が間違っていれば2です。 detectの応答:{"path": 문자열, "bom": true|false, "utf8_ok": true|false, "encoding": 문자열|null, "verdict": "ok"|"mojibake"|"lost"|"unknown"}(プレースホルダーは文字列です)。ステップ4からmojibake、ステップ5からlossyが一緒に出ます。decodeとrepairは、直した本文を標準出力にそのまま出します。ファイルに保存するのは、呼び出す側の仕事です。keysは、2番目の列(名前)だけを整えて、JSON配列として出力します。scanは、{"U+00A0": 개수, ...}の形で出力します(プレースホルダーは個数です)。- 復元する1行:
깨진문자열.encode("latin-1").decode("utf-8")(プレースホルダーは壊れた文字列です)。latin-1は0から255までのすべてのバイトに文字があるので、元に戻す道がふさがれません。 - サンプル照合の基準として、完成形のハングル音節の範囲
U+AC00からU+D7A3までを使い、割合0.9を基準線にします。この数字はこのラボの前提です。韓国語の名簿に合わせた値であり、資料が変われば決め直す必要があります。 - CP949とEUC-KRを区別せず、CP949に統一して読みます。CP949がEUC-KRの拡張なので、こうしても損はありません。
- 公式ドキュメント: RFC 3629(UTF-8)・UnicodeのUTF-8・BOM案内・UAX #15の正規化・python codecs・python unicodedata
- よくあるミス: BOMを取り除かずにヘッダーを比較すること、U+FFFDが残ったファイルを直し続けようとすること、正規化した値で原本を上書きすること、ゼロ幅文字が
strip()で消えると信じること。 - バイトを直接見たいときは、
od -An -tx1 -N 16 <파일>とfile <파일>を使ってください(プレースホルダーはファイルです)。
7つの状態の同じ名簿を作る
/root/enc/gen_inbox.pyを作成して実行し、inboxディレクトリの下に7つのファイルを作成してください(保存先: /root/enc/inbox)。内容は同じ24人で、保存されたバイトだけが違います。
まず/root/encを作成し、その中でpython3を使ってファイルを書きます。テキストを作ったあと、encodeを変えてバイトとして保存するだけです。ファイルを開くときは、テキストモードではなくバイナリモード("wb")で開く必要があります。そうしないと、エンコーディングが二重にかかります。
推測せずに判定する
/root/enc/enc_probe.pyを作成し、detect <파일>(プレースホルダーはファイルです)が、BOM・UTF-8のデコード可否・候補エンコーディング・サンプル照合の順序で判定した結果を、JSONの1つの塊として出力させてください。応答には、path・bom・utf8_ok・encoding・verdictが必要です。
順序がそのまま判定です。先頭3バイトがEF BB BFかをまず見て、そうでなければ厳密なUTF-8デコードを試し、それも失敗したらCP949で読んでみます。最後に、読み出した文字が意味をなしているかを、完成形のハングルの割合で確認します。エラーなしにデコードできたというだけでは足りません。
判定したとおりに読んでUTF-8に統一する
decode <파일>を追加し(プレースホルダーはファイルです)、判定したエンコーディングで読んだ本文をUTF-8で標準出力に出させてください(BOMは取り除いて出力します)。その結果を、/root/enc/normの下に同じ名前で統一版として保存してください。
判定結果をそのまま再利用すれば足ります。BOMが付いたファイルはutf-8-sigで読まないと、最初のカラム名に目に見えない文字が残ります。原本はそのままにして、統一版は別のディレクトリに作ります。判定が間違っていたときに戻る場所が必要だからです。
一度誤って読まれて固まった文字を復元する
repair <파일>を追加して(プレースホルダーはファイルです)二重エンコーディングを復元し、標準出力に出させ、detectの応答にmojibakeを加えて、そうしたファイルのverdictをmojibakeにしてください。復元できなければ、終了コードは3です。
UTF-8のバイトをlatin-1で読むと、ハングル1文字がアルファベット3文字になります。バイトは1つも失われていないので、同じ道を逆にたどれば元に戻ります。復元したと判定する根拠は、復元したほうの完成形のハングルの割合が高くなったことです。この検査を省くと、問題のないファイルまで触ってしまいます。
復元できるものとすでに失ったものを分ける
detectの応答にlossyを追加して、U+FFFDが残ったファイルのverdictをlostにしてください。そして、受信箱をたどり、lostとrecoverableの2つの一覧とreasonを書いてください(保存先: /root/enc/lost.json)。
誤って読む側が寛容だと、読めないバイトが置換文字1つに潰れます。異なるバイトが複数、同じ文字になるので、元に戻す情報が残りません。これは努力の問題ではなく判定です。そのファイルは取り直す必要があります。lostとrecoverableは、ファイル名だけを入れた一覧です。
同じに見えるのに合わない名前
keys <파일>を追加し(プレースホルダーはファイルです)、2番目の列(名前)をNFCに正規化した比較用のキー配列として出力させてください。そして、NFDバージョンとUTF-8バージョンの名前が、正規化の前に何件、正規化のあとに何件合うかを、rows・matched_raw・matched_nfcとして書いてください(保存先: /root/enc/join_report.json)。
ハングル1文字は、完成した音節1つでも、字母3つでも保存されます。画面には同じに見えますが、別の文字列です。正規化は比較のためのもので、原本を直すものではありません。原本ファイルはそのままにします。matched_rawは、正規化していないNFDの名前が、UTF-8バージョンの名前の集合にそのまま入っている件数です。
目に見えない文字がキーに混ざった
scan <파일>を追加して(プレースホルダーはファイルです)目に見えない文字をコードポイントごとに数えさせ、keysがNBSPを普通の空白に変え、ゼロ幅文字を取り除くようにしてください。結果は、counts・rows_affected・keys_fixedとして書きます(保存先: /root/enc/invisible_report.json)。
ゼロ幅文字はstrip()では消えません。消すものをコードポイントのリストで決めておき、1文字ずつたどって取り除く必要があります。rows_affectedは、目に見えない文字が1つでも入っていた行の数で、keys_fixedは、整えたあとにUTF-8バージョンの名前と合うようになったキーの数です。
受信箱1枚で報告する
受信箱の7つのファイルを一度に処理して、files・ok・mojibake・lostを書き(保存先: /root/enc/enc_report.json)、/root/enc/enc_report.mdに、## 무엇을 받았나、## 어떻게 판정했나、## 되살린 것과 잃은 것、## 보내는 쪽에 요청할 것の4つの節で書いてください(韓国語の見出しは順に「受け取ったもの」「どう判定したか」「復元できたものと失われたもの」「送る側に依頼すること」という意味です)。
filesはファイル名をキーにして、encodingとverdictを入れたオブジェクトです。レポートには、復元したファイル数と失ったファイル数を数字で書いてください。顧客に再送してもらう必要があるものが何かが、この文書の目的です。前のステップで作った判定関数を、そのまま呼べば足ります。