直接给结论:双引号里面的分隔符是数据,不是分隔符;被引号包起来的字段里如果要出现一个双引号, 就写两个。一个只会「按逗号切」、忽略这两条规则的解析器,遇到任何用了这两条规则的文件都会丢数据 —— 这就是导入表格时列会整体错位的根源。
CSV 没有一个人人遵守的规范、没有类型系统、没有编码声明,而且 —— 造成最大破坏的这一条 —— 它有一条引号规则,绝大多数用来读它的代码都写错了。
如果你曾经导入一个表格,发现从第 47 行开始列整体偏了一格,那几乎可以肯定就是这个原因。
规则第一条:引号里的逗号不是分隔符
所谓「标准」全部加起来就是这一句:字段可以被双引号包起来。在这些引号里面,逗号只是一个普通字符。 所以下面这一行是两个字段,不是三个:
"Smith, John",42
而天真的实现 —— line.split(",") —— 会切出三个。每一个带逗号的地址、每一个写成
「Acme, Inc.」的公司名、每一段含逗号的产品描述,都会让它所在那一行剩下的部分整体向右错一列。
而且因为错误只出现在碰巧含逗号的那些行,拿一份干净样本测试时根本看不出来。
规则第二条,也是更糟的一条:引号里怎么放引号
CSV 里没有反斜杠转义。要在被引号包起来的字段里放一个实实在在的双引号,你就写两个:
"He said ""hello"" and left",2026-01-04
这是一个字段,值是 He said "hello" and left。一个懂第一条规则、却不懂这条的解析器,
会切出 He said 、孤零零的 hello 和 and left ——
并且照样会把这些多出来的碎片算成列,让整行继续错位。
规则第三条:字段里可以有换行
被引号包起来的字段里可以含真正的换行。这是合法的 CSV,而且它只是一行:
id,notes
7,"First line
second line"
这正是「按行读」的解析器彻底失败的地方。逐行读文件、再逐行切分,这条路走不通, 因为一行不等于一条记录。解析器必须一边逐字符扫描,一边记住自己现在是否在引号里面。
手写文件时最容易踩的几个字符
| 字符 | 会发生什么 | 怎么处理 |
|---|---|---|
, | 把字段切成两半 | 把整个字段用双引号包起来 |
" | 让引号字段提前结束 | 写成两个:"" |
| 换行符 | 把一行变成两行 | 把字段用双引号包起来 |
开头的 =、+、-、@ | 表格软件可能把这个单元格当成公式 | 前面加一个单引号,或者用引号包起来并在导入时关掉公式解析 |
前导零:007 | 表格软件会把它转成数字 7 | 用引号包起来,并把那一列按文本导入 |
| 文件开头的字节序标记(BOM) | 第一列的表头对不上,因为它前面多了一个看不见的字符 | 写成不带 BOM 的 UTF-8,或者在读取时把它剥掉 |
第四行值得停一下。表格软件历史上会把任何以 = 开头的单元格当成公式来解释,
这意味着一份由用户数据生成的 CSV,可能成为对打开它的人发起代码执行攻击的载体。
如果你在从不是自己写的数据生成 CSV,请把这些前导字符清洗掉。
顺手说一下编码
CSV 没有办法声明自己的编码,所以读取方只能猜。实际情况下:
- 写 UTF-8。这是唯一合理的默认值。
- 优先选不带 BOM 的 UTF-8。BOM 看不见但真实存在, 它就是「为什么我第一列的表头前面有个怪字符」的成因。
- 老文件要做好是别的编码的准备。从旧版 Windows 软件导出的数据经常是 Windows-1252 或 GBK。
一个显示成乱码的文件 —— 你本来期望
é,看到的是é—— 就是 UTF-8 的字节被当成 Windows-1252 读了,或者反过来。
那为什么不干脆用 TSV 或者分号
两者都没解决问题,只是把问题挪了个位置。你选的任何分隔符都可能出现在字段里,所以无论如何都需要引号规则。 制表符分隔的文件更难调试,因为那个分隔符是看不见的,而且一旦某个字段里含制表符它就当场崩掉 —— 在粘贴来的数据里,这比你想的要常见。
分号分隔在欧洲部分地区很常见,恰恰是因为那里逗号是小数点 —— 这是一种本地化绕路,不是更好的格式。 如果你收到一个这样的文件,CSV 转 JSON 工具能自动识别分隔符, 但在你知道答案的时候,明确指定会更好。
一个正确的解析器遵循什么
- 逐字符扫描。不要按行切,也不要按分隔符切。
- 在引号外:分隔符结束字段、换行结束记录、字段开头的
"进入引号模式。 - 在引号内:所有内容都是字面值,唯一的例外是
"",它代表一个双引号。 - 在引号外、字段中间出现的引号字符,通常说明文件格式有问题 —— 但多数解析器实际采取的做法是把它当普通字符保留,而不是直接报错。
- 除非已知文件是补齐过空格的,否则不要做 trim。被引号包起来的字段里,首尾空格通常是有意义的。
这在多数语言里大概就是三十行代码,而这三十行是大量生产代码里没有的。如果你在写导入功能, 这部分值得写对,也值得为它写测试 —— 具体就是三个用例:含逗号的字段、含转义引号的字段、含换行的字段。 这三个用例覆盖了现实里几乎所有的数据损坏。
不把文件传上去也能检查
从业务系统导出的 CSV 往往是一个人手上最敏感的文件:客户名单、薪资数据、交易流水。 为了重新排个格式就把它们送过一台服务器,没有任何道理。这里的转换工具 在页面内解析,正确处理被引号包起来的字段和字段内换行,并把 JSON 显示出来, 让你一眼看清每一列落在了哪里。没有任何东西被传出去。