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?

CSV の文字コードを、置換文字の数え上げで決める

0
Posted at

背景

Excel で CSV を開くと日本語が化ける、というのを年に何度か踏みます。

直し方は知っているんですが、毎回メモ帳を開くのが面倒で…。

ブラウザだけで直すツールを作ったので、判定のところだけ書いておきます。

なぜ化けるのか

CSV はただのテキストです。

どの文字コードで書かれているかを、ファイル自身が持っていません。

Excel は日本語環境だと Shift_JIS として開こうとします。

一方、Web サービスやスクリプトが吐く CSV はたいてい UTF-8 です。

この組み合わせで化けます。

ここでいう BOM というのは Byte Order Mark の略で、ファイルの先頭に置く目印のことです。

UTF-8 の場合は EF BB BF の3バイトで、テキストとしては見えません。

Excel はこれを見つけると UTF-8 として読みます。

つまり中身を一切変えずに、先頭3バイトを足すだけで直ります。

判定を書く

問題は「もとのファイルが何で書かれているか」です。

BOM があるならそれに従えばいい。

無いときが困ります。

私が最初に考えたのは「UTF-8 として読めるか試す」でした。

TextDecoder に fatal: true を渡すと、読めないバイト列で例外が飛びます。

これで振り分けられる、と思ったんですが…うまくいきませんでした。

Shift_JIS のバイト列が、たまたま UTF-8 としても例外を出さずに読めてしまうことがあるからです。

そこで向きを変えました。

どれで読めるかではなく、どれが一番読めないかを数える。

fatal: false で読むと、読めなかった位置が U+FFFD(置換文字。いわゆる「�」)に置き換わります。

その個数を数えて、一番少ない候補を採ります。

下記が実際のコードです。bytes は FileReader で読んだ Uint8Array、BOM があれば先に切り落としてあります。

function guessEncoding(bytes) {
  const cands = ['utf-8', 'shift_jis', 'euc-jp'];
  let best = null;
  for (const enc of cands) {
    const text = new TextDecoder(enc, { fatal: false }).decode(bytes);
    const bad = (text.match(/\uFFFD/g) || []).length;   // 読めなかった位置の数
    if (best === null || bad < best.bad) best = { enc, bad, text };
  }
  return best;
}

戻り値の bad がそのまま「この候補で何文字読めなかったか」になります。

enc が採用した文字コード、text がデコード済みの文字列です。

具体例

Shift_JIS で「あ,い」と書いたファイルは、バイト列としてはこうなります。

82 A0 2C 82 A2

これを3つの候補で読ませた実行結果が下記です。手元の Node.js で実際に流したものです。

入力: 82 A0 2C 82 A2

utf-8      bad=4  text="\ufffd\ufffd,\ufffd\ufffd"
shift_jis  bad=0  text="あ,い"
euc-jp     bad=2  text="\ufffd,\ufffd"

出力: shift_jis (bad が最小)

bad が 0 の shift_jis が選ばれます。

逆に UTF-8 で書いた同じ内容を入れると、utf-8 が bad 0 で勝ちます。

数えるだけなので、判定の理由がそのまま数字で出るのが気に入っています。

判定が付かない回をどう扱うか

この方法にも外れる場合があります。

短いファイルや、どの文字コードでも成立してしまう内容です。

そういうときに「たぶん UTF-8 でしょう」と黙って決めてしまうと、あとで直しようがなくなります。

なので、bad が 0 でなかった回は画面にその件数を出すようにしました。

「BOM が無いため候補から選びましたが、読めない位置が 4 件あります。文字コードを指定してください」と表示します。

自動判定は当たると便利ですが、外れたことを黙っているほうが害が大きいので…。

ツールはここです。
https://hashitosystem.com/tools/csv-encoding/

書き出しは TextEncoder で UTF-8 にして、先頭に EF BB BF を足すだけです。

const body = new TextEncoder().encode(text);
const out = new Uint8Array(body.length + 3);
out[0] = 0xEF; out[1] = 0xBB; out[2] = 0xBF;
out.set(body, 3);

なお、プログラムから CSV を読む用途では BOM が先頭カラムの値に混ざって壊れることがあります。

そのため BOM なしでも出せるようにしてあります。

結論

文字コードの自動判定は、「どれで読めるか」で書こうとすると分岐が増えて詰まります。

「どれが一番読めないか」を数える形にすると、比較が1つの整数に落ちて素直に書けます。

そのうえで、数え上げた結果が 0 でなかった回は自動判定を名乗らず、指定を促す。

当てにいくところと、当たらなかったと言うところを分けるのが、この手の判定では一番効きました。


本記事はAI補助で執筆した、個人開発の紹介記事です。

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?