BOM(バイト順マーク)の正体 - UTF-8 に BOM は必要か
テキストファイルを開いたとき、先頭に見覚えのない文字化けが現れた経験はないだろうか。その正体は BOM(Byte Order Mark)かもしれない。文字エンコーディングの判定に使われるこの仕組みは、UTF-16 では不可欠だが UTF-8 では厄介な問題を引き起こすことがある。
BOM とは何か
BOM は Unicode テキストの先頭に置かれる特殊な文字で、コードポイントは U+FEFF である。もともとの名前は「Zero Width No-Break Space(ゼロ幅改行なしスペース)」だったが、現在はバイト順マークとしての用途に限定されている。
この文字がファイル先頭に置かれたとき、テキストを読むプログラムは 2 つの情報を得られる。1 つはそのファイルが Unicode であること、もう 1 つはバイト順(エンディアン)がどちらであるかということだ。
複数バイトで 1 つの値を表すとき、上位バイトを先に並べるか(ビッグエンディアン)、下位バイトを先に並べるか(リトルエンディアン)という順序の違い。
なぜバイト順が問題になるのか
コンピュータのプロセッサによって、メモリ上のバイトの並べ方が異なる。UTF-16 では 1 文字を 2 バイトで表すため、この並び順の違いがデータの解釈に直結する。
「A」(U+0041)を UTF-16 で表す場合を見てみよう。
上位バイトが先に来る。0x00 0x41 の順で格納される。ネットワーク通信の標準バイト順でもある。
下位バイトが先に来る。0x41 0x00 の順で格納される。Intel 系プロセッサが採用しており、Windows 環境で多い。
BOM なしのファイルを受け取ったプログラムは、0x00 0x41 が「A」なのか 0x41 0x00 が「A」なのか判断できない。そこで先頭に U+FEFF を書いておけば、そのバイト列の並び順からエンディアンを特定できる。
| エンディアン | BOM のバイト列 | 意味 |
|---|---|---|
| BE | 0xFE 0xFF | ビッグエンディアン |
| LE | 0xFF 0xFE | リトルエンディアン |
0xFE 0xFF と読めればビッグエンディアン、0xFF 0xFE と読めればリトルエンディアンだとわかる。逆の並びである U+FFFE は Unicode で永久に文字を割り当てないと定められているため、誤認の心配がない。
UTF-8 の BOM
UTF-8 にはバイト順の問題が存在しない。UTF-8 は 1 バイト単位でエンコードするため、エンディアンの概念がそもそも不要だ。それにもかかわらず、UTF-8 にも BOM が付くことがある。
UTF-8 で U+FEFF をエンコードすると 0xEF 0xBB 0xBF という 3 バイトになる。これがファイル先頭にあると「このファイルは UTF-8 である」という目印になる。
# ファイル先頭のバイトを確認する
xxd -l 4 file.txt
# BOM ありの場合
# 00000000: efbb bf48 ...H
# → 先頭 3 バイトが EF BB BF = UTF-8 BOM
# BOM なしの場合
# 00000000: 4865 6c6c Hell
# → いきなりテキスト内容が始まるWindows のメモ帳は長らく UTF-8 で保存するときに BOM を自動付加していた。この挙動は多くのトラブルの原因となってきた。
BOM が引き起こす問題
UTF-8 の BOM は「あると便利」どころか「あると壊れる」場面が少なくない。
PHP でファイル先頭に BOM があると、header() や session_start() の前に出力が発生したとみなされ、“Headers already sent” エラーになる。BOM の 3 バイトがブラウザへの出力として扱われるためだ。
JSON の仕様(RFC 8259)は先頭に BOM を付けることを禁止していないが、パーサーによっては BOM を不正なトークンとして拒否する。Node.js の JSON.parse() に BOM 付き文字列を渡すとエラーになる。
#!/bin/bash の前に BOM があると、OS はシバン行を正しく認識できず、スクリプトの実行に失敗する。
Excel は UTF-8 BOM 付きの CSV を正しく開けるが、BOM なしの UTF-8 CSV は文字化けすることがある。これが「BOM を付けるべき」という誤解を生む一因にもなっている。
実際のバイト列を確認する
BOM の有無でファイル先頭のバイト列がどう変わるか、JavaScript で可視化してみよう。
<div id="bom-demo">
<button id="btn-with">BOM あり</button>
<button id="btn-without">BOM なし</button>
<div id="output"></div>
</div>#bom-demo {
font-family: monospace;
font-size: 14px;
}
button {
padding: 6px 14px;
margin: 0 6px 12px 0;
cursor: pointer;
border: 1px solid #999;
background: #f5f5f5;
border-radius: 4px;
}
button:hover {
background: #e8e8e8;
}
#output {
white-space: pre;
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 4px;
min-height: 60px;
line-height: 1.6;
}
.bom-byte {
color: #f44;
font-weight: bold;
}
.text-byte {
color: #9cdcfe;
}function toHex(arr) {
return Array.from(arr).map(function(b) { return b.toString(16).padStart(2, '0').toUpperCase(); });
}
function display(bytes, bomLen) {
var hex = toHex(bytes);
var lines = [];
lines.push('バイト列:');
var parts = hex.map(function(h, i) {
if (i < bomLen) return '<span class="bom-byte">' + h + '</span>';
return '<span class="text-byte">' + h + '</span>';
});
lines.push(' ' + parts.join(' '));
lines.push('');
if (bomLen > 0) {
lines.push('<span class="bom-byte">赤 = BOM (EF BB BF)</span>');
} else {
lines.push('BOM なし: テキストが先頭から始まる');
}
lines.push('<span class="text-byte">青 = テキスト "Hello"</span>');
document.getElementById('output').innerHTML = lines.join('\n');
}
document.getElementById('btn-with').addEventListener('click', function() {
var text = new TextEncoder().encode('Hello');
var bom = new Uint8Array([0xEF, 0xBB, 0xBF]);
var combined = new Uint8Array(bom.length + text.length);
combined.set(bom);
combined.set(text, bom.length);
display(combined, 3);
});
document.getElementById('btn-without').addEventListener('click', function() {
var text = new TextEncoder().encode('Hello');
display(text, 0);
});
document.getElementById('btn-with').click()各仕様の BOM に対するスタンス
BOM を付けるべきかどうかは仕様ごとに方針が異なる。
| 仕様 | BOM の扱い |
|---|---|
| Unicode 規格 | UTF-8 では BOM は任意 |
| HTML(WHATWG) | BOM を認識するが非推奨 |
| JSON(RFC 8259) | 先頭に BOM を付けてはならない |
| XML 1.0 | BOM を認識する |
Unicode 規格自体は UTF-8 の BOM を禁止していないが、WHATWG の Encoding Standard では UTF-8 に BOM を付けないことが推奨されている。
WHATWG はブラウザベンダーが主導する Web 標準化団体で、HTML や DOM、Encoding などの Living Standard を策定している。
W3C の HTML 仕様でも、meta タグの charset 指定と BOM が矛盾した場合は BOM が優先されると定められている。この挙動は意図しないエンコーディングの誤認を引き起こす可能性があるため、BOM に頼らず charset を正しく指定するほうが安全だ。
BOM の検出と除去
既存のファイルに BOM が含まれているかを確認し、必要に応じて除去する方法を見ておこう。
# BOM の検出(Linux / macOS)
file --mime-encoding target.txt
# UTF-8 Unicode (with BOM) と表示されれば BOM あり
# BOM の除去
sed -i '1s/^\xEF\xBB\xBF//' target.txt
# ディレクトリ内の全ファイルから BOM を除去
find . -name "*.txt" -exec sed -i '1s/^\xEF\xBB\xBF//' {} \;JavaScript でも BOM を除去できる。
function removeBOM(str) {
if (str.charCodeAt(0) === 0xFEFF) {
return str.slice(1);
}
return str;
}
// fetch で取得したテキストから BOM を除去
fetch('data.json')
.then(res => res.text())
.then(text => {
const clean = removeBOM(text);
const data = JSON.parse(clean);
});Node.js で JSON ファイルを読み込むときに BOM 付きファイルでエラーが出た場合は、この除去処理を挟むことで解決する。
結局 UTF-8 に BOM は必要か
結論としては、UTF-8 のファイルには BOM を付けないのが現在の標準的な慣行だ。
UTF-8 で保存するファイルに BOM を付けるべき場面はどれですか?
- PHP のソースファイル
- JSON データファイル
- Excel で開く CSV ファイル
- シェルスクリプト
UTF-16 では BOM がバイト順の判定に不可欠だが、UTF-8 ではエンディアンの問題がないため本来不要な存在である。歴史的経緯から BOM が付いているファイルは今でも流通しているが、新規に作成するファイルには付けないほうがトラブルが少ない。エディタの設定を確認して、UTF-8 保存時に BOM を自動付加しない設定にしておくことを勧める。
Excel は BOM なしの UTF-8 CSV を正しく認識できないことがあるため、Excel での閲覧を想定する CSV には BOM を付けるのが実用的な対処法です。それ以外の場面では BOM を付けない方が安全です。