文字は、人が見る形のままファイルに入っているわけではない。番号に置き換え、その番号をバイトに詰めて保存している。
| 層 | 「あ」の場合 | 決めているもの |
|---|---|---|
| ① 文字 | あ | 人が見る形 |
| ② コードポイント | U+3042 | Unicode(文字に振った番号の表) |
| ③ バイト列 | UTF-8:E3 81 82CP932: 82 A0 | 符号化方式(番号をバイトに詰める規則) |
① ㈱ 髙 などを足した拡張版| 事故 | 症状 | どの層のずれか |
|---|---|---|
| 読めない | 文字化け・読み込みエラー | ③→②の読み方の規則が、書いた側と違う |
| 一致しない | JOIN・名寄せ・列名の照合から漏れる | ①は同じなのに、②の並びが違う |
文字化けしたとき、ファイルのバイト列は壊れていない。書いた側と同じ規則で読み直せば戻る。 文字が壊れたと勘違いすることが多いが、使っているレンズが違うだけ。
| 状態 | 戻せるか |
|---|---|
| 元のファイルが残っている | 戻せる。正しい方式で読み直す |
読めないバイトが置換文字 U+FFFD(�)に置き換わった値 | 戻せない。どの � も EF BF BD で、元の情報が残っていない |
shift_jis は ㈱ を扱えないが、Go の japanese.ShiftJIS は中身が CP932 相当で扱える英数字は UTF-8 でも CP932 でも同じバイトになる。文字コードを取り違えていてもテストは通る。テストデータには日本語を入れる。
見た目が同じなのに一致しないときは、推測で直さない。数えて、番号を出して、正体を特定してから直す。
| 原因 | 何が起きるか | 対処 |
|---|---|---|
| BOM(Byte Order Mark) | ファイル先頭の見えない1文字(UTF-8 では EF BB BF)。先頭の列名だけ照合が外れる | 先頭の U+FEFF を落とす |
CRLF(\r\n) | \n だけで区切ると \r が残り、各行の最後の列だけ "shipped\r" になる | 区切る前に \r\n を \n に置き換える |
| 全角半角 | 「ア」と「ア」、「1」と「1」は別のコードポイント | NFKC で正規化する |
| 見えない文字 | ゼロ幅スペースや BOM は、TrimSpace(BigQuery の TRIM も)で落ちない | コードポイントを出して、その文字を指定して落とす |
| 大文字小文字 | "ABC" == "abc" は false | 比較の前に小文字にそろえる |
正規化は、見た目は同じなのに中身が違う文字列を、1つの形にそろえること。JOIN・名寄せ・重複チェックのように、2つの文字列が同じかを機械が判定するときに必要になる。表示するだけなら要らない。
TRIM):前後の空白を削る。全角スペースや改行も消えるが、真ん中のスペースと、ゼロ幅スペース・BOM は残る㈱ は (株) に、① は 1 になる。長音「ー」とハイフン、大文字小文字、ゼロ幅スペースはそろわないToLower / LOWER):大文字小文字をそろえる。NFKC はここをやらないので別に足す" ABCストア " → TrimSpace → "ABCストア" → NFKC → "ABCストア" → 小文字化 → "abcストア"
正規化は情報を捨てる(㈱ は (株) に戻らないし、①番棚 と 1番棚 が同じになる)。元の値はそのまま残し、比べるための列を別に作って、そっちだけ正規化する。
decoded := transform.NewReader(r, japanese.ShiftJIS.NewDecoder()) // CP932 → UTF-8
...
if strings.ContainsRune(f, utf8.RuneError) { // utf8.RuneError は U+FFFD
return nil, fmt.Errorf("... invalid Shift-JIS sequence at col %d", ...)
}
RuneError の検査:デコーダは読めないバイトに出会っても、エラーを返さず黙って U+FFFD に置き換える。だから置換文字が1つでも混ざった値は、テーブルに入れずに止める文字の事故は「読めない」と「一致しない」の2つだけ。どちらも、3層のどこでずれたかで説明できる。