CSV はカンマで切るものではない:RFC 4180 の引用符と改行
引用符付きのフィールドが 1 つ現れた時点で split は破綻します。引用符は区切り文字と改行を包み、内部の引用符は 2 つ重ねて書きます。30 行の状態機械が正規表現より確実です。
CSV はカンマで切ればいい。この思い込みは最初の引用符付きフィールドで崩れます。a,"b,c" は 3 つに分かれますが、正解は 2 つです。
ルール自体は短い
RFC 4180 で実際に対処すべきことは 4 つです。
- フィールドは引用符で囲める。囲んだ中には区切り文字、改行、引用符が現れてよい
- フィールド内の引用符は 2 つ重ねて書く(
""は") - 行区切りは CRLF だが、実ファイルでは LF も多い。両方を受け入れる
- 末尾の改行で空のレコードを余分に作らない
状態機械
「いま引用符の中か」を表す真偽値とバッファで足ります。要点は、引用符の中の引用符は次の 1 文字で決まることです。
function parseCsv(input, delimiter) {
const rows = [];
let row = [];
let field = '';
let quoted = false;
for (let i = 0; i < input.length; i += 1) {
const ch = input[i];
if (quoted) {
if (ch === '\u0022') {
if (input[i + 1] === '\u0022') { field += ch; i += 1; }
else quoted = false;
} else field += ch;
} else if (ch === '\u0022') {
quoted = true;
} else if (ch === delimiter) {
row.push(field);
field = '';
} else if (ch === '\n' || ch === '\r') {
if (ch === '\r' && input[i + 1] === '\n') i += 1;
row.push(field);
rows.push(row);
row = [];
field = '';
} else field += ch;
}
if (field !== '' || row.length) { row.push(field); rows.push(row); }
return rows;
}
最後の if が 4 番目です。ファイルが改行で終わっていれば field も row も空なので、何も追加しません。この 1 行がないと、普通のファイルすべてに空行が 1 行増え、後続のプログラムが空レコードとして保存します。
よくある 4 つの罠
- BOM:Excel の書き出しは
\uFEFFで始まることが多く、これが最初の列名に貼り付いてidが見つからなくなります。解析前に取り除きます。 - CRLF と LF の混在:同じファイルに両方が現れることも珍しくありません。1 文字ずつ処理して両方を改行として扱います。
- 短い行を埋めない:
a,bの次の行が1だけなら列は 1 つです。空文字で埋めて渡す方が、表示がずれるよりましです。 - 出力時の過剰な引用:必要なときだけ付けます。区切り文字、引用符、改行を含む場合です。そうしないと
a,bが"a","b"になり、合法でも読みづらくなります。
このサイトでの選択
CSV ツール では解析に上の状態機械を使い、出力は最小限の引用、区切り文字の推定は最初の行で候補(カンマ、タブ、セミコロン、縦棒)の出現回数を数えます。推定は最初の行だけを見て、引用符で囲まれた部分を先に取り除きます。そうしないと "a;b,c" のようなフィールドが集計を狂わせます。
CSV を正規表現で解析しない。カンマで split しない。真偽値とバッファの方が賢い書き方より確実にデバッグできます。

コメント
…