CSV はカンマで切るものではない:RFC 4180 の引用符と改行

引用符付きのフィールドが 1 つ現れた時点で split は破綻します。引用符は区切り文字と改行を包み、内部の引用符は 2 つ重ねて書きます。30 行の状態機械が正規表現より確実です。

CSV はカンマで切ればいい。この思い込みは最初の引用符付きフィールドで崩れます。a,"b,c" は 3 つに分かれますが、正解は 2 つです。

ルール自体は短い

RFC 4180 で実際に対処すべきことは 4 つです。

  1. フィールドは引用符で囲める。囲んだ中には区切り文字、改行、引用符が現れてよい
  2. フィールド内の引用符は 2 つ重ねて書く("" は ")
  3. 行区切りは CRLF だが、実ファイルでは LF も多い。両方を受け入れる
  4. 末尾の改行で空のレコードを余分に作らない

状態機械

「いま引用符の中か」を表す真偽値とバッファで足ります。要点は、引用符の中の引用符は次の 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 しない。真偽値とバッファの方が賢い書き方より確実にデバッグできます。

← 記事一覧に戻る

コメント

…