CSV 不是「按逗号切」:RFC 4180 的引号与换行

一旦字段里有引号,split 立刻出错:引号能包住逗号和换行,引号自己写成两个。三十行状态机比任何正则都可靠。

「CSV 嘛,按逗号切一下就行」—— 这句话在第一个带引号的字段上就破产了。a,"b,c" 用 split(',') 会得到三个字段,而正确结果只有两个。

规则本身很短

RFC 4180 里真正要处理的只有四条:

  1. 字段可以用引号包起来,包起来后里面可以出现分隔符、换行和引号
  2. 字段内部的引号写成两个("" → ")
  3. 行分隔符是 CRLF,但实际文件里 LF 也很常见,两种都要认
  4. 文件末尾的换行不该多产生一条空记录

状态机

一个「当前是否在引号里」的布尔值加一个缓冲区就够,关键是引号内的引号要看下一个字符:

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 === '"') {
        if (input[i + 1] === '"') { field += ch; i += 1; }
        else quoted = false;
      } else field += ch;
    } else if (ch === '"') {
      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 都是空的,就不追加。少了这一行,每个正常文件都会多出一条空行 —— 而后面的程序会把它当成一条空记录写进数据库。

四个常见坑

  • BOM:Excel 导出的文件常以 \uFEFF 开头,它会被粘进第一个字段名,导致 id 找不到。解析前先剥掉。
  • CRLF 与 LF 混用:同一个文件里两种混着出现也不罕见,逐字符处理时统一当行尾即可。
  • 短行不补齐:a,b 后面接一行只有一个 1,第二行就只有一列。补齐成空串交给调用方,比在界面上显示错位要好。
  • 写出时过度加引号:只在需要时加(含分隔符、引号、换行),否则 a,b 变成 "a","b",虽然合法但没人愿意看。

本站的选择

CSV 工具 里解析走上面这个状态机,输出用「最小引号」策略,另有分隔符嗅探:看第一行里哪个候选(逗号、制表符、分号、竖线)出现次数最多。嗅探只在第一行进行,并且会先剥掉被引号包住的片段,否则 "a;b,c" 这种字段会误导统计。

不要用正则解析 CSV,也不要用 split。一个布尔值加一个缓冲区,比任何聪明写法都好调试。

← 返回文章列表

评论

…