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 で表す場合を見てみよう。

ビッグエンディアン(BE)

上位バイトが先に来る。0x00 0x41 の順で格納される。ネットワーク通信の標準バイト順でもある。

リトルエンディアン(LE)

下位バイトが先に来る。0x41 0x00 の順で格納される。Intel 系プロセッサが採用しており、Windows 環境で多い。

BOM なしのファイルを受け取ったプログラムは、0x00 0x41 が「A」なのか 0x41 0x00 が「A」なのか判断できない。そこで先頭に U+FEFF を書いておけば、そのバイト列の並び順からエンディアンを特定できる。

エンディアンBOM のバイト列意味
BE0xFE 0xFFビッグエンディアン
LE0xFF 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 のヘッダー送信エラー

PHP でファイル先頭に BOM があると、header() や session_start() の前に出力が発生したとみなされ、“Headers already sent” エラーになる。BOM の 3 バイトがブラウザへの出力として扱われるためだ。

JSON パースの失敗

JSON の仕様(RFC 8259)は先頭に BOM を付けることを禁止していないが、パーサーによっては BOM を不正なトークンとして拒否する。Node.js の JSON.parse() に BOM 付き文字列を渡すとエラーになる。

シェルスクリプトの shebang 破壊

#!/bin/bash の前に BOM があると、OS はシバン行を正しく認識できず、スクリプトの実行に失敗する。

CSV の文字化け

Excel は UTF-8 BOM 付きの CSV を正しく開けるが、BOM なしの UTF-8 CSV は文字化けすることがある。これが「BOM を付けるべき」という誤解を生む一因にもなっている。

実際のバイト列を確認する

BOM の有無でファイル先頭のバイト列がどう変わるか、JavaScript で可視化してみよう。

HTML
CSS
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.0BOM を認識する

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 ファイル
  • シェルスクリプト
__RESULT__

Excel は BOM なしの UTF-8 CSV を正しく認識できないことがあるため、Excel での閲覧を想定する CSV には BOM を付けるのが実用的な対処法です。それ以外の場面では BOM を付けない方が安全です。

UTF-16 では BOM がバイト順の判定に不可欠だが、UTF-8 ではエンディアンの問題がないため本来不要な存在である。歴史的経緯から BOM が付いているファイルは今でも流通しているが、新規に作成するファイルには付けないほうがトラブルが少ない。エディタの設定を確認して、UTF-8 保存時に BOM を自動付加しない設定にしておくことを勧める。