0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Excel で開くと文字化けする CSV、実機で確かめたら列までずれていた(BOM・Shift_JIS・ブラウザで Shift_JIS を書き出す方法)

0
Posted at

「CSV でダウンロードできるようにして」は業務システムで最もよくある要望の1つで、リリース後に最もよく来る問い合わせが「Excel で開くと文字化けする」です。

よく知られた話ですが、実際に Excel で確かめてみると、文字化けだけでなく列の区切りまで消えることがわかったので、結果と対策をまとめます。後半では、ライブラリなしでブラウザから Shift_JIS の CSV を書き出す方法も紹介します。

この記事は、生成AI(Claude)を使って書いた下書きを、実機での動作確認とあわせて検証してから投稿しています。

実験: 同じ CSV を3つの文字コードで保存して Excel で開く

中身は「名前,x」「テスト,1000」の2行だけです。日本語版 Windows(システムの文字コード 932)の Excel 16.0 で Workbooks.Open で開き、セルの値を読み取りました(2026年10月22日)。

ファイルの文字コード A1 A2 B2
UTF-8(BOM なし) 蜷榊燕 繝・せ繝・1000 (空)
UTF-8(BOM 付き) 名前 テスト 1000
Shift_JIS 名前 テスト 1000

BOM(先頭の EF BB BF)がない UTF-8 は、Shift_JIS として読まれて文字化けします。ここまでは有名な話です。

注目は A2 と B2 です。BOM なしでは 「1000」が B2 ではなく A2 に入っています。「テスト」の UTF-8 の最後のバイトと、続く , が Shift_JIS の2バイト文字として1文字に読まれてしまい、列の区切りが消えたためです。「文字化けするけど数字の列は読める」とは限らず、列ずれで別の列に値が入ることがあります。

どの文字コードで出すか

用途 文字コード
利用者が Excel でダブルクリックして開く UTF-8(BOM 付き)
銀行・会計ソフト・古い基幹システムに取り込む 取り込み先の仕様に合わせる(Shift_JIS が多い)
プログラムで読み込む UTF-8(BOM なし)

UTF-8(BOM 付き)は、こう作れます。

const body = new TextEncoder().encode(csvText);
const bytes = new Uint8Array(body.length + 3);
bytes.set([0xEF, 0xBB, 0xBF]); // BOM
bytes.set(body, 3);
const blob = new Blob([bytes], { type: 'text/csv' });

ブラウザで Shift_JIS を書き出すには

TextDecoder('shift_jis') で読むことはできても、TextEncoder は UTF-8 しか出力できません。そこで、TextDecoder で全バイト列をデコードして、逆引きの表を作る方法があります。

let SJIS = null;
function sjisTable() {
  if (SJIS) return SJIS;
  SJIS = new Map();
  const dec = new TextDecoder('shift_jis');
  for (let b = 0x00; b <= 0x80; b++) SJIS.set(dec.decode(new Uint8Array([b])), [b]);
  for (let b = 0xA1; b <= 0xDF; b++) SJIS.set(dec.decode(new Uint8Array([b])), [b]); // 半角カナ
  for (let hi = 0x81; hi <= 0xFC; hi++) {
    // 半角カナの範囲、NEC選定IBM拡張(IBM拡張と重複)、ユーザー定義領域は飛ばす
    if ((hi >= 0xA0 && hi <= 0xDF) || hi === 0xED || hi === 0xEE || (hi >= 0xF0 && hi <= 0xF9)) continue;
    for (let lo = 0x40; lo <= 0xFC; lo++) {
      if (lo === 0x7F) continue;
      const ch = dec.decode(new Uint8Array([hi, lo]));
      if (ch.length !== 1 || ch === '\uFFFD' || SJIS.has(ch)) continue;
      SJIS.set(ch, [hi, lo]);
    }
  }
  return SJIS;
}

先頭バイト 0xED・0xEE(NEC 選定 IBM 拡張文字)を飛ばしているのは、同じ文字が 0xFA〜0xFC(IBM 拡張文字)にもあり、Windows はそちらを使うためです。

本当に Windows と同じになっているか

この表(7,518文字)と、Windows の .NET の CP932 エンコーダー(Encoding.GetEncoding(932))が出すバイト列を、U+0000〜U+FFFF のすべての文字(サロゲートと私用領域を除く)で比べました。両方を「コードポイント:バイト列」の行にして SHA-256 を取ったところ、完全に一致しました(Chrome 152・Windows 11)。

「〜」が変換できない問題

Mac などで入力した「〜」は U+301C、Windows で入力した「~」は U+FF5E です。CP932 にあるのは U+FF5E だけなので、そのままだと「変換できない文字」になります。変換の前に置き換えます。

const TO_CP932 = { '\u301C': '\uFF5E', '\u2212': '\uFF0D', '\u2016': '\u2225', '\u00A2': '\uFFE0', '\u00A3': '\uFFE1', '\u00AC': '\uFFE2', '\u2014': '\u2015' };

function encodeSjis(text, replacement = '?') {
  const table = sjisTable();
  const out = [], bad = [];
  for (const ch of text) {
    const bytes = table.get(TO_CP932[ch] ?? ch);
    if (bytes) out.push(...bytes);
    else { bad.push(ch); out.push(...table.get(replacement)); }
  }
  return { bytes: new Uint8Array(out), bad };
}

encodeSjis('〜').bytes;    // Uint8Array [0x81, 0x60]
encodeSjis('𠮷野家😀').bad; // ['𠮷', '😀']

絵文字や「𠮷」は CP932 に存在しないので、bad で呼び出し元に返し、黙って「?」にしないようにしています。

まとめ

  • BOM なし UTF-8 の CSV は、Excel で文字化けするうえ、列の区切りが消えて値がずれることがある
  • Excel 向けは UTF-8(BOM 付き)、取り込み先の指定があれば Shift_JIS
  • ブラウザでも、TextDecoder の逆引きで Windows と同じ Shift_JIS を書き出せる。「〜」などは事前に置き換える

この記事の内容に、CSV のクォート処理・CSV インジェクション対策・読み込み時の文字コード判定を加えたものを、Zenn の本の1章にまとめています。振込カナ・営業日・和暦・消費税の端数など、日本の業務データの落とし穴を、公式資料の出典とテストつきで扱った本です(最初の2章は無料)。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?