JSで出力したCSVがExcelで文字化けする原因と対策(BOM付きUTF-8・デモ付き)

症状:ダウンロードしたCSVをExcelで開くと日本語が化ける

JavaScriptで Blob からCSVを生成してダウンロードさせる。ブラウザやテキストエディタで開くと日本語は正しく表示されるのに、同じファイルをExcelでダブルクリックして開くと「譁�蟄怜喧縺�」のような文字化けになる。これがCSVダウンロード機能の定番トラブルです。

結論から言うと、原因はExcelがBOMなしのUTF-8ファイルをShift_JIS(正確にはWindows-31J)として読み込んでしまうことです。ファイル先頭にBOM(バイトオーダーマーク)を付けてUTF-8だと明示すれば直ります。まずは実際に両方をダウンロードして、手元のExcelで違いを確かめてください。

まず試す:BOMなし版とBOM付き版を実際にダウンロードする

下の2つのボタンは、まったく同じ日本語データのCSVを生成します。違うのは先頭にBOMを付けているかどうかだけです。両方ダウンロードして、それぞれExcelで開いてみてください。BOMなしは文字化けし、BOM付きは正しく表示されるはずです(Windows版Excelの場合)。

BOMなしUTF-8(Excelで文字化けする)

ブラウザやVS Codeでは正しく開けますが、Windows版Excelでダブルクリックすると日本語が化けます。

BOM付きUTF-8(Excelで正しく開ける)

同じデータですが、先頭にBOM(JavaScriptでは \uFEFF というエスケープシーケンスで書ける、画面には何も表示されない1文字)を足しています。これだけでExcelが「UTF-8だ」と判断します。

原因:ExcelはBOMがないと文字コードを推測する

CSVはただのテキストファイルで、ファイル自身に「これはUTF-8です」という情報を持っていません。だからファイルを開くソフト側が文字コードを判定する必要があります。ここでExcelとブラウザ・エディタで判定のしかたが違うため、同じファイルなのに片方だけ化けるという現象が起きます。

ExcelはCSVを開くとき、先頭にUTF-8のBOMがなければ、OSの既定文字コード(日本語版WindowsではShift_JIS系のWindows-31J)として読み込む。UTF-8で書かれた日本語バイト列をShift_JISとして解釈するため、文字化けする。

BOMとは、ファイル先頭に置く数バイトの目印です。UTF-8のBOMは EF BB BF の3バイトで、JavaScriptの文字列としては \uFEFF という1文字で表せます。この目印があると、Excelは「このファイルはUTF-8だ」と判断して正しくデコードします。ブラウザやテキストエディタはBOMがなくてもUTF-8を優先的に推測するので化けません。この推測方針の差が、原因のすべてです。

なお、この問題はダウンロード時のHTTPヘッダーやBlobのMIMEタイプでは解決しません。ExcelはCSVをダブルクリックで開くとき、通信時の charset 指定を見ず、ファイルの中身(先頭バイト)だけを見て判定するためです。対策はあくまで「ファイルの中身にBOMを入れる」ことになります。

対策:Blobの先頭にBOMを付ける

直し方は1行です。CSV文字列の先頭に \uFEFF を足すか、Blob の配列の先頭にBOMのバイト列を置きます。後者のほうが、CSV本文の文字列を汚さないので扱いやすいです。

// CSVの中身(普通に組み立てた文字列)
const rows = [
  ['注文ID', '顧客名', '商品', '金額'],
  ['ORD-1001', '山田商事株式会社', 'プレミアムプラン', '120000'],
  ['ORD-1002', '佐藤デザイン事務所', 'スタンダードプラン', '48000'],
];
const csv = rows.map(r => r.join(',')).join('\r\n');

// これがポイント:BOM(\uFEFF)を先頭に置く
const bom = '\uFEFF';
const blob = new Blob([bom + csv], { type: 'text/csv' });

// ダウンロードさせる
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'orders.csv';
a.click();
URL.revokeObjectURL(url);

Blob の第1引数は配列なので、次のように文字列を汚さずBOMだけ別要素で足す書き方もできます。どちらでも結果は同じです。

// 文字列側は BOM を含めず、Blob の配列先頭に分けて置く
const blob = new Blob(['\uFEFF', csv], { type: 'text/csv' });

改行コードを \r\n(CRLF)にしているのも意図的です。CSVの仕様(RFC 4180)はCRLFを基本とし、古いExcelやほかの表計算ソフトでは \n だけだと行が正しく分かれないことがあります。BOMとあわせて、CRLFで出力しておくと無難です。

もう一つの選択肢:Shift_JISに変換して出力する

「BOM付きUTF-8ではなく、そもそもShift_JISで書き出す」という対策もあります。相手先の受け取り側システムがShift_JIS固定でしか読めない(古い業務システムや一部の会計ソフトへの取り込みなど)場合は、こちらが必要になります。ただしブラウザ標準の TextEncoder はUTF-8しか出力できないため、変換用ライブラリが要ります。

<!-- encoding.js をCDNから読み込む -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/encoding.min.js"></script>

<script>
  const csv = rows.map(r => r.join(',')).join('\r\n');

  // UTF-8文字列をShift_JIS(SJIS)のバイト配列に変換
  const sjisArray = Encoding.convert(Encoding.stringToCode(csv), {
    to: 'SJIS',
    from: 'UNICODE',
  });
  const blob = new Blob([new Uint8Array(sjisArray)], { type: 'text/csv' });
  // 以降のダウンロード処理はBOM版と同じ
</script>

どちらを選ぶかの判断はシンプルです。Webブラウザとの相性・将来性を取るならBOM付きUTF-8、レガシーな受け取り側システムに合わせる必要があるならShift_JISです。私は基本的にBOM付きUTF-8を既定にし、Shift_JISは「取り込み先がUTF-8を受け付けない」と分かっているときだけ使い分けています。Shift_JISは表現できない文字(一部の丸数字・外字・絵文字など)があり、変換で欠落するリスクがあるからです。UTF-8ならその心配がありません。

エッジケース:環境ごとの挙動の違い

「BOMを付ければ全環境で完璧」というわけではありません。開く側の環境で挙動が変わるので、代表的なケースを表にまとめます。ここを押さえておくと、問い合わせ対応で迷いません。

開く環境 BOMなしUTF-8 BOM付きUTF-8 補足
Windows版Excel 文字化けする 正しく開ける もっとも問題になる組み合わせ。BOM付与が定番対策。
Mac版Excel 化けやすい 基本は正しく開ける Windows版より判定が不安定な場面があり、バージョンによってはBOM付きでも列区切りがずれる報告がある。相手がMac中心ならBOM付きUTF-8にした上で、実機での確認をすすめる。
Googleスプレッドシート 正しく開ける 正しく開ける インポート時にUTF-8を前提に文字コードを判定するため、そもそも化けない。BOMがあっても無視される。「スプレッドシートでは問題ないのにExcelだけ化ける」のはこの判定方針の差が理由。
ブラウザ・VS Code等のエディタ 正しく開ける 正しく開ける UTF-8を優先して推測するため化けない。「開発中は正常なのに納品先のExcelで化ける」の落とし穴はここ。
CSVを別システムへ再取り込み システム次第 BOMがゴミになる場合あり 取り込み側がBOMを想定していないと、先頭カラムに \uFEFF が混入し「1列目のヘッダー名が一致しない」といった不具合になる。取り込み用途ではBOMなし、Excel閲覧用途ではBOM付き、と出し分ける判断もある。

表のとおり、Googleスプレッドシートで問題が起きないのはインポート時にUTF-8前提で文字コードを判定するからで、Excelのように環境の既定コードにフォールバックしません。「うちのブラウザやスプレッドシートでは再現しない」という報告が来ても、Windows版Excelでは化けているという前提で対応するのが確実です。

まとめ

JSで出力したCSVがExcelで文字化けするのは、BOMなしUTF-8をExcelがShift_JISと解釈するためです。対策は、Blobの先頭にBOM(\uFEFF)を1つ足すだけで済みます。

const blob = new Blob(['\uFEFF', csv], { type: 'text/csv' }); これだけで、Windows版Excelの文字化けは直ります。

受け取り側がUTF-8を受け付けない古いシステム向けにはShift_JIS変換を選び、Excelで閲覧してもらう用途にはBOM付きUTF-8を選ぶ。この使い分けさえ押さえておけば、CSVダウンロードの文字化けで詰まることはほぼなくなります。開発中のブラウザでは化けないので気づきにくい点にだけ注意してください。

関連するUI事例

CSVの出力・取り込みを、実際に動くコードで確認できる事例です。

あわせて読みたい

文字化けとは別に、CSVをExcelで開くと日付や数値の見た目が勝手に変わってしまう問題もあります。あわせて読むとExcel連携のハマりどころをまとめて押さえられます。