障害の一次切り分けが終わり、あとは業務システム側のログを読むだけ、というところまで来た。ファイルを開いた。画面に並んでいるのは ?~?A???[ のような何かです。
日本語圏のログ調査には、英語圏の記事にほとんど書かれていない工程がひとつ余分にあります。そのバイト列を、どの文字コードとして解釈するかを決める工程です。ここで詰まると、検索も、grep も、正規表現も、すべてが「動いているのに当たらない」という最悪の形で失敗します。エラーが出れば気づけますが、文字コードの取り違えはたいていゼロ件という静かな嘘で返ってきます。
この記事では、日本語ログでよく踏む罠を4つ並べます。化ける・判定が外れる・先頭3バイトで挙動が変わる・検索が当たらない。どれも手持ちのコマンドで切り分ける手順を書き、そのうえで、その手がどこで止まるかまで踏み込みます。
1. cat した瞬間に画面が壊れる — Shift_JISのログとUTF-8ターミナル
状況
Windows で動いている業務システムのログを受け取った。手元の macOS / Linux のターミナルで cat した。文字が化けただけならまだよくて、ターミナルそのものの表示が崩れて reset を打つはめになった。
なぜ起きるか
Shift_JIS(実務上は CP932 / Windows-31J)の2バイト文字は、2バイト目が ASCII の可視文字と重なる設計です。有名なのが 2バイト目に 0x5C(バックスラッシュ)が来る文字群 —— 「ソ」「表」「能」「噂」「ダメ」あたりが常連です。これを UTF-8 前提のツールに食わせると、バックスラッシュとしてエスケープ解釈され、正規表現もパスも壊れます。
UTF-8 として読もうとした場合はさらに情報が失われます。不正なバイト列は多くの実装で U+FFFD(�)に置換されるため、化けた表示から元のバイトを復元できません。画面が崩れるのは、置換の過程で制御文字域のバイトがそのまま端末に届き、エスケープシーケンスとして解釈されるからです。
汎用ツールでの対処と限界
パイプで変換すれば読めます。
iconv -f CP932 -t UTF-8 app.log | less
nkf --guess app.log # 判定だけ見る
ここで -f Shift_JIS ではなく -f CP932 を指定するのが実務上のコツです。NEC特殊文字(①、㈱)やIBM拡張漢字(﨑、髙)は素の Shift_JIS の規格外なので、Shift_JIS 指定だと変換がそこで止まります。
限界は3つあります。ひとつめ、変換した先はもう原本ではないこと。iconv は不正バイトに当たると停止し、//IGNORE で握りつぶせば、その不正バイトこそが手がかりだった場合に証拠が消えます。ふたつめ、全量変換のコスト。数GBのログを毎回パイプに流すと、読みたいのが末尾の1分間でも先頭から変換が走ります。みっつめ、変換済みファイルを作って解決した場合、容量が二重になり、原本との行番号の対応を人間が管理することになります。
2. 「自動判定」を信じきれない — 判定が外れる5つの条件
状況
文字コード自動判定つきのビューアで開いた。それでも化けた。手動で切り替えたら直った。では次のファイルはどうなのか——自動判定を信じてよいのか分からなくなった。
なぜ起きるか
文字コードの判定は、原理的にヒューリスティックです。ファイルの中にエンコーディング名は書かれていないので、バイトの分布と「その符号化として妥当か」の検査から推測するしかありません。外れる条件はだいたい決まっています。
先頭がASCIIだけ。 タイムスタンプと英語のINFO行が延々と続き、日本語は数万行先のエラーメッセージにだけ出る——これは日本の業務システムのログでは典型です。先頭数KBしか見ないツールは、迷わず「UTF-8」と断定します。
EUC-JP と CP932 が似ている。 バイト分布が近く、日本語の量が少ないと取り違えます。
混在している。 複数ホストのログを cat で連結した、あるいは移行前後で出力が変わった。ファイル単位で1つに決める判定は、混在ファイルでは必ず一方を壊します。
妥当性検査を通ってしまう。 あるバイト列が UTF-8 として妥当で、かつ CP932 としても妥当、ということは起こり得ます。検査は候補を減らせても、決めきれません。
汎用ツールでの対処と限界
疑わしいときのチェックリストはこうなります。
file・nkf --guess・chardetectの3つに聞いて一致するかを見る(割れたら混在を疑う)- その判定がファイルのどこを見た結果かを疑う(先頭だけか、全量か)
grep -n -P '[^\x00-\x7F]' app.log | head -1で日本語が最初に出る位置を探し、そこを見る- 連結・追記の履歴があるなら混在を前提にする
- 妥当性で候補を潰す:
iconv -f utf-8 -t utf-8 app.log > /dev/nullが通るか
限界は、この5番目が全量スキャン × 候補数の時間を要求することです。そして、より本質的な限界があります。ここで本当にやりたいのは判定の断定ではなく、解釈を切り替えて、化けが直るかを目で見ることです。切り替えるたびにファイルを開き直し、索引を作り直す道具では、この往復が1回あたり数分になります。数分かかる確認を4候補ぶん回すかと言われたら、たいていの人は諦めます。
3. 先頭3バイトで挙動が変わる — BOMという見えない文字
状況
CSV のパーサが1行目だけ落ちる。head -1 の表示は正しく見える。grep '^timestamp' が当たらない。目視では、どう見ても timestamp から始まっています。
なぜ起きるか
UTF-8 BOM(EF BB BF)の3バイトが先頭に付いているからです。表示上は見えません。日本の現場では Excel を経由したファイルで頻繁に混入します(Excel は BOM 無し UTF-8 の CSV を CP932 と誤認して化けさせるため、BOM を付けて保存する運用のほうが正しい場面もある)。
つまり BOM は「付いているのが正しい」道具と「付いていないのが正しい」道具が両方あるという、立場が割れている先頭3バイトです。シェルスクリプトは #! の前に BOM があると実行できません。多くの JSON パーサは弾きます。awk の1列目は BOM ごと1トークンになります。
UTF-16 はさらに厄介です。BOM は FF FE / FE FF。ASCII 文字の間に NUL バイトが入るため grep はバイナリと判断して黙り(grep -a が要る)、行分割を \n(0x0A)1バイト基準で行うツールは、0A 00 を正しく行末と扱えません。Windows のイベントログを PowerShell でエクスポートすると既定で UTF-16LE——という一点だけでも、覚えておく価値があります。
汎用ツールでの対処と限界
先頭を見る癖をつけるのが唯一の防御です。
head -c 16 access.csv | xxd # efbbbf で始まっていないか
file access.csv # "with BOM" と出ることがある
sed '1s/^\xEF\xBB\xBF//' access.csv > clean.csv # 除去
限界は、除去が原本の改変になることです。証跡として扱うログでハッシュ値が変わるのは避けたい。かといって除去済みコピーを作れば、また容量が二重になり、どちらが原本かを管理する負債が増えます。
そして何より、BOM は先頭3バイトを見れば1秒で分かる情報です。それを確かめるために、毎回ターミナルに戻って xxd を叩いている。ビューアで開いているファイルの先頭バイトが見えていれば済む話が、道具の外に追い出されています。
4. 日本語が検索でヒットしない — 正規化・全半角・エンコーディングの三重罠
状況
「エラー」で検索した。0件。画面には確かに「エラー」と表示されている。目の前にある文字列が、検索で当たらない。
なぜ起きるか
日本語の検索は、3つの層のどこでも外れます。そして、どの層で外れたのかを検索結果は教えてくれません。
第1層・エンコーディング。 UTF-8 のターミナルから入力した「エラー」のバイト列と、CP932 で書かれたファイル内の「エラー」のバイト列は別物です。grep はバイト列を比較しているので、当たらないのが正しい動作です。
第2層・表記ゆれと正規化。 半角カナ(エラー)と全角、ABC と ABC。加えて Unicode 正規化の NFC / NFD があります。macOS 由来のパス名がログに混ざると、「が」が「か + 濁点」の2コードポイントで書かれていることがある。見た目は完全に同一で、バイト列は別物という、いちばん気づきにくい形です。
第3層・異体字と互換文字。 髙/高、﨑/崎。CP932 の NEC 特殊文字 ① は UTF-8 変換の経路によって U+2460 にも (1) にもなります。ハイフン類(- ‐ – — − ー)の取り違えも定番で、極めつけが波ダッシュ問題 —— 全角チルダ U+FF5E と波ダッシュ U+301C は、CP932 と UTF-8 の変換テーブルの実装差でどちらにも転びます。
汎用ツールでの対処と限界
順番に潰していきます。
grep "$(echo 'エラー' | iconv -f UTF-8 -t CP932)" app.log # 第1層:検索語のほうを変換
uconv -x nfc -f UTF-8 -t UTF-8 app.log > nfc.log # 第2層:正規化してから検索
grep -E 'エラ|エラ' app.log # 第3層:短く・両表記で
検索語を漢字1〜2文字まで短くするのは、実務でよく効きます。ゆれが入り込む余地を減らせるからです(grep -i は日本語の全半角には効きません)。
限界は2つです。ひとつは、正規化がまたファイルを書き換えること。数十GBのログに uconv をかけてコピーを作る判断は、容量的にも時間的にも簡単ではありません。
もうひとつが本質的です。ゼロ件という結果は、3つの層のどれで外れたのかを一切語りません。 切り分けるには結局、日本語が出ている場所を1か所開いて、そこに何が書かれているかを目で見るしかない。ところが grep は当たらなかったのだから、その「場所」を教えてくれません。検索が失敗したときに、検索の道具は次の一手を出せない——これが日本語ログ調査でいちばん時間を溶かす瞬間です。
4つに共通していたもの
| 罠 | 現れ方 | 止まる場所 |
|---|---|---|
| SJIS × UTF-8端末 | 画面が壊れる | 変換した先は原本ではない・全量変換の待ち |
| 自動判定の外れ | 開いたら化ける | 切り替えて見る往復のコストが高い |
| BOM・UTF-16 | 先頭行だけ挙動が違う | 見れば分かる情報が道具の外にある |
| 検索ゼロ件 | 当たらない | ゼロ件の理由が分からず、次の一手が出ない |
4つとも、変換ではなく確認で決着する問題でした。判定を切り替えて化けが直るかを見る。先頭バイトを見る。当たらなかった語の周辺を見る。どれも「別ファイルを作って変換する」必要はなく、原本を開いたまま、解釈だけを切り替えられれば一往復で済みます。
にもかかわらず現場でこれが重い作業になるのは、道具の側の事情です。iconv も uconv も変換して別の出力を作る道具で、確認のために使うと必ず中間ファイルか全量パイプが挟まります。ビューア側も、文字コードを切り替えるたびに読み直しと索引の作り直しが走るなら、数GBのファイルでは1回の確認が数分になる。第1回で見たツールの限界と、第2回で見た「一覧と現場の往復」が、日本語ログではもう一段きつい形で現れる、ということです。
なお本記事では現役の CP932 を中心に扱いました。EUC-JP・UTF-16・サロゲートペアによる行ズレといった、より古い層・より新しい層の話は続編で扱います。
使っている道具
私が開発している UwView(無料)は、この確認の往復のために作ったビューアです。UTF-8 / Shift-JIS(CP932) / EUC-JP / UTF-16 を自動判定し、手動での切り替えは索引の作り直しなしで即座に反映されます(判定が外れたときに、開き直さずその場で候補を試せます)。巨大なログでも開いた瞬間から全体を表示・スクロール・検索でき、索引はバックグラウンドで作られて、完成すると行番号が付きます。変換も除去もしないので、原本は1本のまま・無改変のまま扱えます。正直に書いておくと、UTF-16 は BOM で認識しますが行分割は \n 基準のため、主対象は UTF-8 / Shift-JIS / EUC-JP です。そして、ストレージを専有している巨大ログを圧縮して保管し、さらに高速に検索したいなら UwView Pro をどうぞ。永続索引・圧縮キャッシュ検索・約1/9保管で、開き直しも検索も一段速くなります(2回目以降は行番号付きで瞬時に開けます。全OS対応・買い切り/月額プランあり)。
リンク
- 第1回: 巨大ファイルに沈む4つの定番ツールと、その先: https://uvp.y42u.net/blog/uwview-ps01-huge-file-tool-limits/
- 第2回: ログの因果を遡る4つの技法: https://uvp.y42u.net/blog/uwview-ps02-log-causality-tracing/
- 巨大ログが開けないときの手順: https://uvp.y42u.net/blog/uwview-huge-log-cannot-open/
- 検索一覧と現場を行き来する仕組み: https://uvp.y42u.net/blog/uwview-filter-popup-jump-save-context/
- ソースコード(GitHub): https://github.com/amru195704/UwView
開発者より: アプリ・Kindle本・オープンソースの一覧は GitHub: amru195704 にまとめています。
お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。文字コードの判定挙動・変換テーブルの差異は、ツールの実装やバージョン、ロケール設定により異なります。コマンド例は環境(GNU/BSD、iconvの実装差、uconvの有無等)により調整が必要な場合があります。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。

