テキストエディタで開いても原因が分からないファイルに、ときどき出会います。
見た目は普通なのに、プログラムに渡すと落ちる。
そういうときは、中身をバイト単位で覗いてしまうのが早道です。
今回は od と xxd を使って、ファイルの中身を16進数で確認する手順をまとめました。
検証は GNU coreutils 8.32 の od と、xxd 2021-10-22 版で行っています。
od と xxd はどう違うのか
どちらもファイルを16進数などで表示するコマンドですが、出自は別もの。
od は GNU coreutils に含まれていて、たいていの環境に最初から入っています。
一方の xxd は vim に付属していて、ディストリによっては vim 関連パッケージを入れて初めて使えるツールです。
使い分けは単純で、読みやすさなら xxd、表示形式の細かい切り替えなら od。
さらに xxd には16進ダンプから元のバイト列へ戻す機能があり、od にはこれがありません。
$ od --version | head -1
od (GNU coreutils) 8.32
$ xxd -v
xxd 2021-10-22 by Juergen Weigert et al.
まずは od でバイト列を見る
サンプルとして、日本語を含む小さな CSV を作ります。
printf なら改行の有無まで自分で決められるので、この手の検証にはうってつけ。
$ printf 'ID,名前\n1,かもめ\n' > utf8.csv
$ od -c utf8.csv
0000000 I D , 345 220 215 345 211 215 \n 1 , 343 201 213 343
0000020 202 202 343 202 201 \n
0000026
左端の 0000000 はファイル先頭からの位置で、既定では8進数で表示されます。
ASCII に収まる文字はそのまま出て、それ以外は数値に置き換わる仕組み。
「名」が 345 220 215 の3バイトになっているのは、od -c が数値を8進数で書くためです。
16進で読みたいときは -tx1 を足します。
$ od -An -tx1 -c bom.csv | head -3
ef bb bf 49 44 2c 4e 61 6d 65 0a 31 2c 67 75 6c
357 273 277 I D , N a m e \n 1 , g u l
6c 0a
-An はオフセット列を消すオプションで、出力を貼り付けて比較したいときに効きます。
文字化けそのものの仕組みは、別の記事で整理しました。
xxd なら16進とASCIIを同時に読める
同じ要領で、CRLF 改行の CSV を xxd に通してみます。
$ printf 'ID,Name\r\n1,gull\r\n' > crlf.csv
$ xxd crlf.csv
00000000: 4944 2c4e 616d 650d 0a31 2c67 756c 6c0d ID,Name..1,gull.
00000010: 0a .
左に16進、右に ASCII が並ぶのが xxd の標準の出力です。
ただし右側では表示できない文字がすべてピリオドに潰れるので、改行とタブの区別は付きません。
つまり 判断はいつも左の16進で行うのが鉄則。
この例では 0d 0a が2か所に並んでいるため、CRLF 改行だと確定できます。
改行コードの背景については、こちらにまとめました。
BOM と末尾の改行を見つける
Excel が書き出した CSV でよく引っかかるのが BOM です。
$ xxd bom.csv | head -2
00000000: efbb bf49 442c 4e61 6d65 0a31 2c67 756c ...ID,Name.1,gul
00000010: 6c0a l.
先頭の ef bb bf が UTF-8 の BOM で、1列目のヘッダ名が ID と一致しないという定番の事故は、たいていこの3バイトが原因。
当たりを付けるだけなら file コマンドでも足ります。
$ file utf8.csv crlf.csv bom.csv
utf8.csv: Unicode text, UTF-8 text
crlf.csv: ASCII text, with CRLF line terminators
bom.csv: Unicode text, UTF-8 (with BOM) text
file で当たりを付けて、od か xxd で裏を取る。
この順番だと調査がいちばん速く終わります。
末尾に改行があるかどうかの確認も、終わりの数バイトを切り出すだけ。
$ printf 'no newline at end' > tail1.txt
$ tail -c 20 tail1.txt | xxd | tail -1
00000010: 64 d
$ printf 'has newline at end\n' > tail2.txt
$ tail -c 20 tail2.txt | xxd | tail -1
00000010: 6e64 0a nd.
0a で終わっていなければ、そのファイルには末尾の改行がありません。
全角スペースのような「見えない差」も見える
目視ではまず見つからないのが、半角スペースと全角スペースの混在です。
$ printf 'a\xe3\x80\x80b' > zenkaku.txt
$ xxd zenkaku.txt
00000000: 61e3 8080 620a a...b.
e3 80 80 が全角スペース(U+3000)の UTF-8 表現。
設定ファイルのインデントに紛れ込むと、パーサが理由も言わずに落ちます。
エディタで空白の可視化を切っていると、何時間でも溶かせる種類のバグ。
範囲を絞る、そして元に戻す
ファイルが大きいときは、見たい場所だけ切り出します。
$ xxd -s 3 -l 7 bom.csv
00000003: 4944 2c4e 616d 65 ID,Name
-s が開始位置、-l が長さです。
BOM の3バイトを飛ばしてやると、ちゃんと ID,Name が読めました。
xxd のもう一つの武器が、16進ダンプから元のバイト列へ戻す -r。
$ xxd -p crlf.csv
49442c4e616d650d0a312c67756c6c0d0a
$ xxd -p crlf.csv | xxd -r -p | od -c | head -2
0000000 I D , N a m e \r \n 1 , g u l l \r
0000020 \n
-p はオフセットも ASCII 欄も無いプレーンな16進出力です。
これを -r -p に流すと、元のバイト列がそのまま復元されます。
壊れた1バイトだけを書き換えて戻す、といった荒業もこの組み合わせで通るのです。
最後に
od と xxd は、ファイルを「文字の列」ではなく「バイトの列」として見せてくれる道具です。
文字化け、改行コード、BOM、全角スペース。
どれもエディタの画面では見分けが付かないのに、16進で開けば一目で違います。
原因の分からないファイルに当たったら、まず xxd に通してみてください。
以上です。











コメントを残す