1998年のログが届きました。
grep ERROR が0件で返ってきます。ファイルは12GBあって、ERROR の文字は目で見えている。動いているのに当たらない検索は、何を疑えばいいのでしょうか。
答えは、あなたが打ったコマンドの中にはありません。
第3回で現役の Shift_JIS(CP932) を扱ったとき、EUC-JP・UTF-16・サロゲートペアの行ズレは続編で、と書きました。これがその続編です。扱うのはもっと古い層と、もっと新しい層。どちらも「2026年の手元のツールが素直に読んでくれない」という一点で共通しています。
4つの場面を並べます。EUC-JPの基幹システムログ・UTF-16のWindowsイベントログ・メインフレームから来た正体不明のファイル・絵文字で崩れる行。4つとも、詰まっているのは同じ1点です。
- 先に結論: UwView は UTF-8 / Shift-JIS(CP932) / EUC-JP / UTF-16 を自動判定し、外れたときは索引の作り直しなしでその場で切り替えて見比べられます。Pro なら一度開いた47.73GBのファイルは2回目以降0.02〜0.07秒で行番号付きのまま開き直せます(特定環境での実測。環境により異なります。詳細は記事末尾)
- 1. EUC-JPの基幹システムログを、2026年に読む
- 2. UTF-16のWindowsログを、Unixのツールが読めない
- 3. EBCDICを疑い、SJISだと分かって安堵し、その後で困る
- 4. 絵文字が混ざった行で、行番号と桁がズレる
- 4つに共通していたもの
- 使っている道具
- リンク
先に結論: UwView は UTF-8 / Shift-JIS(CP932) / EUC-JP / UTF-16 を自動判定し、外れたときは索引の作り直しなしでその場で切り替えて見比べられます。Pro なら一度開いた47.73GBのファイルは2回目以降0.02〜0.07秒で行番号付きのまま開き直せます(特定環境での実測。環境により異なります。詳細は記事末尾)
1. EUC-JPの基幹システムログを、2026年に読む
状況
20年動いている基幹システムのログを受け取りました。UTF-8 のターミナルで cat すると化けます。
第3回を読んでいるので、まず CP932 を疑って iconv -f CP932 を通します。化けたまま、化け方だけが変わりました。 記号の羅列が別の記号の羅列になっただけで、日本語には見えません。
なぜ起きるか
EUC-JPとCP932は、どちらも「2バイトで、それらしく」化けるからです。
EUC-JP の漢字は先頭・2バイト目とも 0xA1〜0xFE の範囲に収まります。CP932 の先頭バイトは 0x81〜0x9F と 0xE0〜0xEF。範囲は完全には重なりませんが、重なる部分が十分に広い。だから間違ったほうで変換しても、多くのバイトが「変換可能な別の文字」として通ってしまいます。エラーで止まってくれれば気づけるところを、それらしい別の文字が出てくるので気づけません。
さらに EUC-JP には半角カナが 0x8E + 1バイトという特殊な形(SS2)で入ります。ここが CP932 の解釈では別の文字に化けるので、半角カナが多いログほど誤変換が「もっともらしく」なります。
そして 2026年に受け取るこの種のログは、たいてい移行期をまたいでいます。 サーバのロケールを ja_JP.eucJP から ja_JP.UTF-8 に切り替えた日を境に、同じファイルの途中でエンコーディングが変わっていることがあります。
汎用ツールでの対処と限界
判定して変換する、が定石です。
nkf --guess app.log # 判定だけ見る
file -i app.log # libmagic の判定
iconv -f EUC-JP -t UTF-8 app.log | less # 通れば EUC-JP
head -c 4000 app.log | iconv -f EUC-JP -t UTF-8 >/dev/null && echo ok
4本目は実用的です。先頭だけ食わせて、通るかどうかで当たりを付けられます。
限界は3つ。ひとつめ、判定は「先頭の一部」を見て決まることです。移行期のファイルでは先頭が EUC-JP で末尾が UTF-8 という状態が普通に起こり、先頭を見た判定は後半で必ず外れます。iconv は1ファイルにひとつの -f しか取りません。
ふたつめ、確認のたびに全量が流れることです。12GBのログで「EUC-JPかも」「やっぱりCP932かも」を往復すると、1回の思いつきごとに12GBを読みます。第3回で書いた「変換した先はもう原本ではない」に、待ち時間の問題が上乗せされます。
みっつめ、判定の根拠が返ってこないことです。nkf --guess は答えだけを返し、どのバイトが決め手だったかは分かりません。外れたとき、次に何を試すべきかの手がかりがないまま候補を総当たりすることになります。
2. UTF-16のWindowsログを、Unixのツールが読めない
状況
Windowsサーバのイベントログをエクスポートしたテキストを受け取りました。grep -c Error が 0件。
wc -l は妙に小さい数を返します。grep は Binary file app.txt matches とだけ言って中身を見せません。file に聞くと Little-endian UTF-16 Unicode text と返ってきます。
なぜ起きるか
UTF-16では、ASCIIの1文字が2バイトになるからです。
E は 0x45 ではなく 0x45 0x00。だから grep Error が探している 45 72 72 6F 72 というバイト列は、ファイルの中に一度も現れません。 検索が0件で返るのは正しい動作で、しかも何も警告してくれません。
wc -l が狂うのも同じ理由です。改行は 0x0D 0x00 0x0A 0x00。0x0A 自体は含まれるので数えられますが、テキスト中に 0x0A 0x00 の並びが偶然できるとそこも行末として数えられます。そして NUL バイトが大量に混ざるので、多くのツールはこのファイルをバイナリだと判断します。 これが Binary file matches の正体です。
厄介なのは、BOMがない UTF-16 が実在することです。BOMがないと file の判定も不安定になり、data や ASCII text とだけ返ってくることがあります。
汎用ツールでの対処と限界
変換してから扱うのが基本です。
xxd app.txt | head -3 # 先頭バイトを目で見る(FF FE なら UTF-16LE)
iconv -f UTF-16LE -t UTF-8 app.txt > app_u8.txt
grep -a Error app.txt # バイナリ判定を無視して走らせる
tr -d '\000' < app.txt | grep Error # NULを落として無理やり読む
1本目を先にやるべきです。先頭2バイトを見ればだいたい決まります。
限界は3つ。ひとつめ、出し直せない場合があることです。手元で Get-WinEvent | Export-Csv -Encoding UTF8 を打てるなら話は早いのですが、受け取ったのが調査対象の証拠ファイルだったり、退職者のPCから吸い出した1回きりのエクスポートだったりすると、同じものをもう一度は作れません。 原本を変換せずに読む必要があります。
ふたつめ、tr -d '\000' は乱暴すぎることです。ASCII範囲は読めるようになりますが、日本語部分は壊れます。しかも「壊れた」ことが表示から分かりません。ASCIIだけ読めているので、一見うまくいったように見えるのが最悪です。
みっつめ、変換するとバイトオフセットの対応が消えることです。第9回で書いたとおり、生のバイト位置が意味を持つ場面があります。「原本の何バイト目から壊れているか」を報告に書く必要があるとき、変換後のファイルの位置は使えません。
3. EBCDICを疑い、SJISだと分かって安堵し、その後で困る
状況
メインフレームから来たデータを受け取りました。cat してもテキストに見えません。
EBCDIC を疑います。dd conv=ascii を通してみます。やはり読めない。1時間ほど格闘したあと、ただの CP932 だったと分かります。 安堵します。
そして次の日、別の問題で止まります。このファイルには改行がありません。
なぜ起きるか
汎用機まわりのファイルは、文字コード以外の次元でもテキストではないからです。
いくつか同時に起きます。固定長レコード——1レコード250バイトで、区切りは改行ではなく位置です。スペースパディング——短いフィールドは後ろが空白で埋まっているので、grep の結果に大量の空白が付いてきます。シフトアウト/シフトイン——1バイト文字と2バイト文字の切り替えを 0x0E / 0x0F で表す方式が残っている場合、同じファイルの中で領域ごとに符号化が違うことになります。
EBCDIC を疑ったこと自体は正しい初動です。ただ、dd conv=ascii を通してしまうと二重に壊れます。 元が CP932 なら、EBCDIC→ASCII の変換テーブルを当てた時点で元のバイトは失われます。安堵まで1時間かかったのは、壊した後のファイルを見ていたからかもしれません。
汎用ツールでの対処と限界
まずバイトを見る、が唯一の正解です。
xxd data.dat | head -20 # 何はともあれ生バイト
od -c data.dat | head -20
fold -w 250 data.dat | head # 固定長を折り返して見る
tr -d '\016\017' < data.dat | iconv -f CP932 -t UTF-8 | less # SO/SI を落とす
xxd を最初に打つかどうかで、この日の残り時間が決まります。
限界は3つ。ひとつめ、改行のないファイルは行指向ツールの前提を壊すことです。grep も awk も less も「1行」を単位に動きます。12GBが1行のファイルを渡すと、実装によっては1行をまるごとメモリに載せようとします。fold で折り返せますが、折り返した先の「12,043行目」は原本の座標ではありません。
ふたつめ、SO/SI の入れ子は単一のエンコーディング指定で表現できないことです。iconv -f CP932 は「ファイル全体がCP932」という前提で動きます。1バイト領域と2バイト領域が制御文字で切り替わる構造は、この前提の外にあります。落とせば読めますが、落とした瞬間に「どこが2バイト領域だったか」が消えます。
みっつめ、変換を1回でも確定させると引き返せないことです。EBCDIC疑惑のときに dd conv=ascii > data2.dat としてしまい、data.dat を上書きしていたら、そこで調査は終わります。第1回で見た「巨大ファイルを開くために加工する」の一番危ない形が、ここに出ます。
4. 絵文字が混ざった行で、行番号と桁がズレる
状況
問い合わせフォームの本文がそのままログに載る構成です。2026年のユーザー入力には、当然のように絵文字が入ります。
障害報告書に「12,043行目」と書きました。同僚が開くと、そこには別の行があります。あるいは awk '{print $5}' の結果が、特定の行だけ1フィールドずれています。
なぜ起きるか
「1文字」の定義が、道具ごとに4つあるからです。
バイト数・コードポイント数・UTF-16コードユニット数・書記素クラスタ数。絵文字はこの4つがすべて違う値になります。😀 は UTF-8 で4バイト、コードポイントは1個、UTF-16ではサロゲートペアで2ユニット、書記素は1個。👨👩👧 に至ってはコードポイント5個(ZWJ含む)で書記素1個です。アプリケーションが「何文字目」と言うとき、どの数え方かは書かれていません。
行番号がズレる主犯は、実は絵文字そのものではありません。ユーザー入力に含まれる生の改行です。エスケープせずにログへ書くと、1レコードが2行3行に分かれます。もうひとつが片割れサロゲート——アプリが文字列を「16ビット単位で」切り詰めると、サロゲートペアの上位だけが残ることがあります。正当なUTF-8にならないバイト列なので、後段のパーサやビューアはそこで止まるか U+FFFD に置換します。置換した瞬間、バイト数が変わり、以降の桁が全部ずれます。
汎用ツールでの対処と限界
数え方を固定するのが定石です。
LC_ALL=C grep -n 'ERROR' app.log # バイト単位で速く数える
grep -naP '[\x{1F300}-\x{1FAFF}]' app.log # 絵文字を含む行を探す(PCRE)
python3 -c "import sys;[print(i+1,len(l),len(l.encode())) for i,l in enumerate(sys.stdin)]" < app.log
1本目は速いです。LC_ALL=C はマルチバイト解釈をやめるので、大きなファイルでは体感が変わります。
限界は3つ。ひとつめ、LC_ALL=C にすると日本語で検索できなくなることです。速さと可読性のトレードオフで、片方を選ぶと片方を失います。同じファイルに対して両方やりたいのに、環境変数を切り替えて2回走らせることになります。
ふたつめ、端末の表示幅が信用できないことです。絵文字が1桁で表示されるか2桁で表示されるかは端末とフォント次第で、less で見た列位置と awk のフィールド位置は一致しません。目で見て数えた桁を、そのまま報告書に書けません。
みっつめ、片割れサロゲートを含む行は、変換ツールが「なかったこと」にすることです。iconv は停止するか //IGNORE で捨て、多くのビューアは U+FFFD に置換します。しかしその壊れたバイトこそが、アプリが文字列を不正に切り詰めた証拠です。読みやすくする処理が、調査対象を消しています。
4つに共通していたもの
| 場面 | 最初に疑うもの | 実際の詰まりどころ | 汎用ツールが止まる場所 |
|---|---|---|---|
| EUC-JPの基幹ログ | 文字コードの取り違え | ファイルの途中で変わる | -f は1ファイルに1つだけ |
| UTF-16のWindowsログ | 検索の書き方 | ASCIIが2バイトになる | 変換なしでは grep が0件 |
| メインフレームのデータ | EBCDICかどうか | 改行がない・SO/SIがある | 行指向ツールの前提が崩れる |
| 絵文字と行ズレ | 絵文字そのもの | 「1文字」の定義が4つある | 数え方を固定すると別を失う |
3列目を縦に読むと、正体が見えます。どれも「バイト列をどう解釈するか」を、道具の側が先に決めてしまう話です。 iconv は変換を確定させ、grep はバイナリと判断して黙り、tr はNULを捨て、ビューアは壊れたバイトを U+FFFD に置き換える。どの処理も親切で、どれも引き返せません。
そして4列目。汎用ツールが止まるのは、いつも「もう一度、別の解釈で見たい」と思った瞬間です。この往復が、12GBのファイルでは1回ずつ全量走査になります。
必要なものは3つです。
- 解釈を切り替えて、見比べられること — 別ファイルを作るのではなく、同じ原本を別の解釈で表示し直せること。切り替えのたびに読み直しと索引の作り直しが走るなら、この往復は現実的に回せません。
- 行と位置の定義が、ひとつに決まっていること — 「12,043行目」が誰の環境でも同じ行を指すこと。バイト位置で飛ぶと行番号が消える問題は第10回でも出ましたが、ここにはさらに「そもそも行とは何か」が加わります。
- 読めないものが、読めないまま見えること — 片割れサロゲートも、SO/SIの制御文字も、NUL埋めも、整形して隠さないこと。壊れているという事実が手がかりです。
冒頭の「1998年のログ、grep ERROR が0件」に戻ります。0件は、検索が失敗した合図ではありません。 バイト列が想定と違うという、いちばん静かな形の報告です。0件が返ってきたときに真っ先に xxd を打てるかどうかで、その日の残り時間が決まります。
使っている道具
私が開発している UwView(無料)は、巨大なテキストを開いた瞬間から全体を表示・スクロール・検索できるビューアです。索引はバックグラウンドで作られ、完成すると行番号が付きます(他のビューアの多くは索引が終わるまで先頭しか見えません)。変換も分割もしないので、原本は1本のまま・無改変のまま扱えます。編集機能を持たないので、受領した証拠ファイルを開くこと自体が安全な操作です。
- 解釈を切り替えて見比べる: UTF-8 / Shift-JIS(CP932) / EUC-JP / UTF-16 を自動判定し、手動で切り替えたときは索引の作り直しなしで即座に反映されます。第1章の「EUC-JPかCP932かを往復する」が、12GBの全量走査を伴わなくなります。
- 開き直しを軽くする: UwView Pro は索引と圧縮を保存するので、2回目以降は行番号付きのまま開き直せます(47.73GBのテキストで実測0.02〜0.07秒。環境により異なります)。判定を試すたびに待つ構図が消えます。
- 行番号を、原本の座標として持つ: 抜き出しも折り返しもせずに原本を開いたまま読むので、「12,043行目」が原本の12,043行目のままです。第4章の桁ズレのうち、少なくとも行の同定は共通の座標に戻せます。
- 壊れたバイトを整形しない: NUL埋めはNUL埋めのまま、制御文字は制御文字のまま見えます。第3章・第4章で「消したくない手がかり」として挙げたものが、そのまま画面に残ります。
正直に書いておくと、UwView は EBCDIC を扱いません。第3章の初動でEBCDICを疑うところは xxd と dd の仕事で、この道具の出番はCP932だと分かってからです。また、UTF-16 は BOM で認識しますが、行分割は \n 基準です。BOMなしのUTF-16や、0x0D 0x00 0x0A 0x00 の行区切りが素直に効かない場合があり、主対象は UTF-8 / Shift-JIS / EUC-JP だと考えてください。SO/SIによる領域ごとの符号化切り替えにも対応しません。加えて、文字コードを変換して保存する機能はありません。読むための道具であって、直すための道具ではありません。
そして、ストレージを専有している巨大ログを圧縮して保管し、さらに高速に検索したいなら UwView Pro をどうぞ。永続索引・圧縮キャッシュ検索・約1/9保管で、開き直しも検索も一段速くなります(全OS対応・買い切り/月額プランあり)。レガシー文字コードの調査のように、同じファイルを解釈を変えて何度も開き直す使い方では、2回目以降が軽いことがそのまま調査の回数になります。
リンク
- 第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/
- 第3回: 文字コード4つの罠と切り分け手順(本記事の前編): https://uvp.y42u.net/blog/uwview-ps03-japanese-encoding-traps/
- 第4回: 消す・残す・圧縮するの判断基準: https://uvp.y42u.net/blog/uwview-ps04-log-retention-decision/
- 第5回: 開発ログとの正しい付き合い方4選: https://uvp.y42u.net/blog/uwview-ps05-debug-log-practices/
- 第6回: 認証ログ数千万行から痕跡を拾う4手順: https://uvp.y42u.net/blog/uwview-ps06-intrusion-triage-auth-logs/
- 第7回: タイムスタンプを武器にする4つの読み方: https://uvp.y42u.net/blog/uwview-ps07-timestamp-driven-triage/
- 第8回: 圧縮保管と検索の両立: https://uvp.y42u.net/blog/uwview-ps08-compressed-archive-search/
- 第9回: 構造化データを生のまま読む4場面: https://uvp.y42u.net/blog/uwview-ps09-read-raw-structured-data/
- 第10回: 深夜2時の自分を助ける4つの仕込み: https://uvp.y42u.net/blog/uwview-ps10-oncall-night-preparation/
- 巨大ログが開けないときの手順: 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 にまとめています。
お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。文字コードの判定挙動・変換テーブルの差異・制御文字の扱いは、ツールの実装やバージョン、ロケール設定により異なります。メインフレーム由来ファイルのレコード形式やシフトコードの有無は、送出側のシステム構成によって大きく変わります。記載した実測値は特定の1環境での測定例であり、同じ結果を保証するものではありません。コマンド例は環境(GNU/BSD、iconv・nkfの実装差、grepのPCRE対応の有無等)により調整が必要です。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。

