TT Lab
はじめる
学ぶ 学習パス コース

顧客データを扱う

名前欄がすべて四角 — 判定ツールを作る

TT Labで続きを見る

目標

顧客が送ってきたファイルのエンコーディングを推測せずに判定するツールenc_probe.pyを作ります。BOMとUTF-8のデコード可否から始めて、候補エンコーディングとサンプルの照合へ進み、二重エンコーディングを復元し、復元できないファイルを見分け、正規化と目に見えない文字が比較をどう壊したかを、自分のデータで証明します。

なぜ重要なのか

テキストファイルの中には、エンコーディングが書かれていません。そのため、「どのエンコーディングか」という問いには、いつも手順でしか答えられません。エディターのメニューを押してみる方式は、ファイルが3つのときにしか通用せず、何より、当たったかどうかを目だけで判断させます。 判定が終わると、ファイルは3種類に分かれます。正しく読めるもの、一度誤って読まれて固まったが元に戻せるもの、そして、文字が潰れて元のバイトがわからないものです。最後の種類にしがみついて時間を使うのは、努力ではなく無駄です。そのファイルは取り直す必要があります。この区別をコードでできてこそ、顧客に何を依頼するかを言えます。 そして、文字は問題なく見えるのに合わない場所があります。同じハングルが、完成形と字母分解形に分かれて保存されると、画面は同じでバイトは違います。目に見えないNBSPとゼロ幅スペースも同じです。結合結果が0件なのに原因が画面に見えない状況は、ここから生まれます。 採点ツールは、提出された文言を信じません。一時ディレクトリに、採点ツールが作ったファイルを用意し、作成したツールを実際に実行して判定を照合します。名前と行数は、実行ごとに変わります。

ステップ

  1. /root/enc/gen_inbox.pyを作成して実行し、inboxディレクトリの下に7つのファイルを作成してください(保存先: /root/enc/inbox)。同じ24人の名簿が、7つの状態で保存されます。
  2. /root/enc/enc_probe.pyのdetectを作成し、BOM・UTF-8のデコード可否・候補エンコーディング・サンプル照合の順序でエンコーディングを判定させてください。
  3. decodeを追加し、判定したエンコーディングで読んでUTF-8で標準出力に出させ、受信箱全体を同じ名前で統一して保存してください(保存先: /root/enc/norm)。
  4. repairを追加して二重エンコーディングを復元させ、detectがそうしたファイルに対してmojibakeをtrue、verdictをmojibakeと答えるようにしてください。
  5. detectにlossyを追加して、U+FFFDが残ったファイルのverdictをlostにし、受信箱の判定を書いてください(保存先: /root/enc/lost.json)。
  6. keysを追加して、名前の列をNFCに正規化した比較用のキーとして出力させ、NFDバージョンとUTF-8バージョンの名前が、正規化の前後で何件合うかを書いてください(保存先: /root/enc/join_report.json)。
  7. scanを追加して目に見えない文字をコードポイントごとに数えさせ、keysがそれらを取り除くようにしてから書いてください(保存先: /root/enc/invisible_report.json)。
  8. 受信箱の7つのファイルを一度に処理して、/root/enc/enc_report.jsonと、/root/enc/enc_report.mdを作成してください。

参考

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を入れたオブジェクトです。レポートには、復元したファイル数と失ったファイル数を数字で書いてください。顧客に再送してもらう必要があるものが何かが、この文書の目的です。前のステップで作った判定関数を、そのまま呼べば足ります。