何をしたいユーティリティか:「BOM 除去」
ここでの「BOM 除去」は、テキストの先頭にこっそり付いている「BOM(Byte Order Mark)」という特殊な目に見えない文字を取り除く処理です。
CSV や JSON を外部システムから受け取ったとき、
「先頭に変な 1 文字がいるせいでパースに失敗する」「ヘッダー名がずれる」
といった、地味だけど厄介なトラブルの原因が、この BOM であることがよくあります。
だからこそ、
「外から来たテキストは、まず BOM を取ってから処理する」
というユーティリティを持っておくと、業務ではかなり効きます。
BOM とは何かをざっくり理解する
BOM は「見えない先頭マーク」
BOM(Byte Order Mark)は、本来は「このテキストは UTF-8 ですよ」などを示すためのマークです。
UTF-8 の BOM は、バイト列で書くと EF BB BF、
JavaScript の文字列としては "\uFEFF" という 1 文字になります。
やっかいなのは、画面には見えないのに「1 文字」として存在することです。
例えば、BOM 付きの CSV の先頭行は、見た目はこう見えます。
id,name
1,foo
でも実際には、先頭に BOM が付いていて、
\uFEFFid,name
1,foo
という状態になっていることがあります。
このとき、"id" だと思っているヘッダー名が、実は "\uFEFFid" になってしまい、
キー名の比較やパースがうまくいかなくなります。
JavaScript で BOM をどう扱うか
文字列としての BOM は \uFEFF
JavaScript の世界では、UTF-8 の BOM は文字コード U+FEFF として扱われます。
つまり、文字列の先頭に "\uFEFF" がいるかどうかを見れば、
「BOM が付いているかどうか」を判定できます。
const s = "\uFEFFid,name";
s.charCodeAt(0).toString(16); // "feff"
JavaScriptこの「先頭の \uFEFF を取り除く」のが、BOM 除去ユーティリティの役割です。
一番シンプルな BOM 除去ユーティリティ
removeBom の実装
まずは、文字列の先頭にある BOM を 1 個だけ取り除くシンプルな関数を書きます。
function removeBom(text) {
if (text == null) {
return "";
}
const str = String(text);
if (str.charCodeAt(0) === 0xfeff) {
return str.slice(1);
}
return str;
}
JavaScript重要なポイントをかみ砕いて説明する
null / undefined を空文字にそろえる
if (text == null) {
return "";
}
JavaScriptnull や undefined が来てもエラーにせず、
「中身のないテキスト」として扱うようにしています。
BOM 除去は「前処理」のことが多いので、
ここで落ちるよりは空文字にしておいた方が扱いやすい場面が多いです。
まずは文字列に変換する
const str = String(text);
JavaScriptバッファや数値などが来ても、とりあえず文字列にしてしまえば、
あとは「先頭の 1 文字」を見るだけで済みます。
先頭 1 文字のコードポイントをチェックする
if (str.charCodeAt(0) === 0xfeff) {
return str.slice(1);
}
JavaScriptcharCodeAt(0) は、先頭の 1 文字のコードポイント(数値)を返します。
BOM のコードポイントは 0xFEFF なので、
それと一致していれば「BOM 付き」と判断し、slice(1) で先頭 1 文字を削ります。
一致していなければ、そのまま返します。
実際の動きを例で確認する
removeBom("\uFEFFid,name");
// "id,name"
removeBom("id,name");
// "id,name"(もともと BOM がなければそのまま)
removeBom(null);
// ""
JavaScriptCSV パースの前に removeBom を通しておけば、
ヘッダー名が "\uFEFFid" になってしまう事故を防げます。
正規表現を使った書き方(複数 BOM も想定)
先頭の BOM をまとめて削る版
理屈としては 1 個で十分ですが、
念のため「先頭に連続している BOM を全部削る」版も書いておきます。
function removeBomAll(text) {
if (text == null) {
return "";
}
const str = String(text);
return str.replace(/^\uFEFF+/, "");
}
JavaScript/^\uFEFF+/ は、「先頭にある \uFEFF が 1 個以上続く部分」を意味します。
それを空文字に置き換えることで、
先頭の BOM をまとめて削除します。
CSV 読み込みと組み合わせた例
BOM 付き CSV を安全に扱う
外部システムから受け取った CSV を行単位で処理する例を考えます。
function parseCsvLines(rawText) {
const text = removeBom(rawText);
const normalized = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
const lines = normalized.split("\n");
return lines;
}
JavaScriptここでは、
先に removeBom で BOM を削る。
次に「改行コード統一」で LF にそろえる。
最後に split("\n") で行に分割する。
という流れにしています。
これで、
BOM のせいで 1 行目のヘッダーが壊れる問題。
改行コードの違いで行数が合わない問題。
をまとめて避けられます。
実務で意識してほしい設計のポイント
「外から来たテキストはまず BOM を疑う」
特に、Excel から出力された CSV や、
Windows 系のツールが吐くファイルは、UTF-8 BOM 付きであることが多いです。
見た目では分からないので、
「外部ファイルを文字列として読み込んだら、まず removeBom を通す」
というルールを作っておくと、
原因不明のパースエラーに悩まされる時間が減ります。
BOM 除去は「最初の 1 回」でよい
BOM は「ファイルの先頭」に付くものなので、
テキストを途中で切り出したり、
自分で組み立てた文字列には基本的に付きません。
つまり、BOM 除去は、
ファイル読み込み直後。
HTTP レスポンスをテキスト化した直後。
など、「外から入ってきた瞬間」に 1 回やれば十分です。
バイナリレベルで扱う場合は別の話になる
ここで扱っているのは「文字列としての BOM(\uFEFF)」です。
Node.js の Buffer など、バイナリとして扱う場合は、
先頭 3 バイトが 0xEF 0xBB 0xBF かどうかを見て削る、という別の処理になります。
ただ、ブラウザや多くの業務コードでは、
「すでに文字列になっているテキスト」を扱うことが多いので、
まずは removeBom のような文字列版を押さえておけば十分です。
少し手を動かして感覚をつかむ
コンソールで、次のようなコードを実際に打ってみてください。
const withBom = "\uFEFFid,name\n1,foo";
const withoutBom = "id,name\n1,foo";
withBom;
removeBom(withBom);
removeBom(withoutBom);
removeBomAll("\uFEFF\uFEFFid,name");
JavaScriptwithBom と removeBom(withBom) を JSON.stringify で表示してみると、
先頭に "\ufeff" がいるかどうかの違いがはっきり分かります。
JSON.stringify(withBom); // "\"\ufeffid,name\n1,foo\""
JSON.stringify(removeBom(withBom)); // "\"id,name\n1,foo\""
JavaScriptそのうえで、自分のプロジェクトに
export function removeBom(...) { ... }
export function removeBomAll(...) { ... }
JavaScriptを置き、
「外部テキストを受け取ったら、まず“BOM 除去ユーティリティ”を通す」
というルールを作ってみてください。
それだけで、あなたのシステムのテキスト処理は、「見えない 1 文字」に振り回される状態から、
意図と一貫性を備えた「業務レベルの入力前処理」に変わっていきます。
