CSV 不是「按逗号切」:RFC 4180 的引号与换行
一旦字段里有引号,split 立刻出错:引号能包住逗号和换行,引号自己写成两个。三十行状态机比任何正则都可靠。
「CSV 嘛,按逗号切一下就行」—— 这句话在第一个带引号的字段上就破产了。a,"b,c" 用 split(',') 会得到三个字段,而正确结果只有两个。
规则本身很短
RFC 4180 里真正要处理的只有四条:
- 字段可以用引号包起来,包起来后里面可以出现分隔符、换行和引号
- 字段内部的引号写成两个(
""→") - 行分隔符是 CRLF,但实际文件里 LF 也很常见,两种都要认
- 文件末尾的换行不该多产生一条空记录
状态机
一个「当前是否在引号里」的布尔值加一个缓冲区就够,关键是引号内的引号要看下一个字符:
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。一个布尔值加一个缓冲区,比任何聪明写法都好调试。

评论
…