JSON 和 CSV 不是可以任意互换的容器。JSON 原生支持嵌套对象、数组、明确的 null、布尔值、数字和任意键名。CSV 表示记录与字段,但分隔符、引号、编码、表头和类型约定取决于下游系统,并不存在一个被所有消费者完全一致执行的配置。转换因此需要 Schema 和明确的损失策略。
核心要点
- 在扁平化 JSON 前先定义一行 CSV 代表什么:一份文档、一个实体,还是数组中的一个元素。
- 嵌套对象需要列名策略;数组需要在索引列、分隔文本、子表或独立文件之间做选择。
- CSV 文本本身不能保留 JSON 类型。空字符串、
null、缺失、0和"0"必须定义表示方式。 - 始终使用真正的 CSV 解析器和写入器。按逗号或换行拆分会在引号、嵌入换行、转义引号、替代分隔符和 BOM 场景下损坏数据。
- 电子表格导出是输出安全边界。根据下游策略处理以
=、+、-或@开头的单元格,避免公式注入,也不要把密钥写进下载或日志。 - 验证表头、行宽、编码、资源限制和代表性往返结果;成功解析不等于语义可逆。
JSON 与 CSV 的模型差异
| 属性 | JSON | CSV |
|---|---|---|
| 结构 | 对象、数组和标量值 | 记录和字段 |
| 类型 | 字符串、数字、布尔、null |
通常是文本,加上下游约定 |
| 嵌套 | 原生支持 | 需要扁平化或拆成多个关系 |
| 数组 | 原生有序值 | 必须另行定义策略 |
| 缺失与空值 | 可以区分 | 没有约定时容易混淆 |
| 编码与方言 | JSON 语法明确,通常使用 UTF-8 | 分隔符、引号、换行、BOM 和编码因消费者而异 |
| 常见用途 | API、文档、配置 | 表格、导出、电子表格和批量导入 |
CSV 对重复的表格字段可能更小,但并不总是更小:重复转义、冗长表头、多行字段和数组反规范化都会增加体积。
先定义行与列 Schema
可以先写出类似下面的契约:
row = 一个客户
column "customer.id" = 必填字符串
column "customer.tags" = JSON 文本,不是逗号分隔列表
缺失字段 = 空单元格
显式 null = "\N"
导出和导入必须共享这些约定。还应考虑:
- 稳定的列顺序和表头名称;
- 选定路径分隔符出现在原始键名中时如何转义;
- 必填、可选和未知列;
- 字段长度和行数上限;
- 日期/时间与数字格式,包括小数分隔符和时区;
- 列是面向电子表格、数据库导入,还是机器间交换。
不要只根据一行样本推断 Schema,后续记录可能有不同键或类型。应从有界的代表性样本收集候选 Schema,再对每条记录验证。
嵌套 JSON 的扁平化
对于只包含对象的嵌套结构,可以使用 customer.address.city 这样的路径,但只有在定义分隔符和转义规则后才可逆。原始键名包含 . 时,不能与嵌套路径发生冲突。
数组需要业务决策:
| 策略 | 示例 | 权衡 |
|---|---|---|
| 索引列 | tags[0]、tags[1] |
宽度有界,但稀疏且不适合可变长度 |
| 单元格内 JSON | tags 存储 ["a","b"] |
保留结构,但消费者还要再次解析 |
| 分隔文本 | tags 存储 a; b |
直观,但分隔符和字段转义复杂 |
| 子表 | 客户表加客户标签表 | 关系清晰且损失少,但需要主键和多份输出 |
| 拒绝或省略 | 不支持的数组直接失败 | 严格安全,但不是所有场景都适用 |
把对象数组压进一行可能产生笛卡尔积或覆盖值。对于关系数据,应使用稳定标识导出父表和子表。
CSV 方言、编码与安全
可靠的写入器在字段包含分隔符、引号、回车或换行时进行引用,并按选定方言转义内部引号。读取器必须识别引号中的换行,不能按物理行简单拆分。
应记录:
- 分隔符、引号字符、转义方式和换行符;
- UTF-8 或其他编码,以及特定电子表格是否需要 BOM;
- 是否有表头、重复表头策略和预期列数;
- 最大字节数、行数、字段数、字段长度和嵌套深度;
- 输出给电子表格时,以
=、+、-或@开头的公式样式值如何处理。
CSV 公式注入是输出风险:电子表格可能把单元格解释为公式、外部链接或命令样表达式。根据消费者策略拒绝此类值、加安全前缀,或使用不会执行公式的格式。不要认为加引号就足以中和公式。
CSV 导出还可能暴露个人数据、令牌、内部 URL 和隐藏列。应授权导出、最小化字段、设置留存和下载策略,并避免记录完整行。
CSV 转 JSON:解析不是类型恢复
CSV 解析器可以恢复字段和记录,却不能恢复原始 JSON 的类型或结构。类型转换需要 Schema:
"00123"可能是必须保留为字符串的标识符;- 空字段可能表示空字符串、缺失或 null;
"true"可能是业务标签,而不是布尔值;- 小数和日期格式依赖地区与契约;
- 超大整数在 JavaScript
Number中可能丢失精度。
应优先使用显式列 Schema,而不是“猜测”转换。如果来源提供 Schema 行或旁车元数据,先验证再读取数据。
使用 Python 标准 CSV 模块
标准库可以正确处理引号和嵌入换行。下面的示例在 Schema 转换前保留所有 CSV 值为文本。
import csv
import io
import json
from typing import Any
def json_rows_to_csv(rows: list[dict[str, Any]]) -> str:
if not rows:
return ""
headers = sorted({key for row in rows for key in row})
output = io.StringIO(newline="")
writer = csv.DictWriter(
output,
fieldnames=headers,
extrasaction="raise",
lineterminator="\r\n",
)
writer.writeheader()
for row in rows:
writer.writerow({
key: json.dumps(row[key], ensure_ascii=False)
if isinstance(row.get(key), (dict, list))
else "" if row.get(key) is None
else str(row.get(key))
for key in headers
})
return output.getvalue()
def csv_to_rows(text: str) -> list[dict[str, str]]:
reader = csv.DictReader(io.StringIO(text, newline=""))
if reader.fieldnames is None or len(set(reader.fieldnames)) != len(reader.fieldnames):
raise ValueError("CSV must contain unique headers")
rows = []
for row in reader:
if None in row:
raise ValueError("row has more fields than the header")
rows.append({key: value for key, value in row.items()})
return rows
此示例选择用 JSON 文本保存嵌套值,用空文本表示 null;其他契约可以使用 \N 等哨兵。因为导出并不天然可逆,选择必须被记录。
JavaScript 与 Go 实现要点
JavaScript 应使用支持引号换行、方言选项、大小限制和必要时流式处理的维护中 CSV 解析/写入库。不要实现 line.split(","),也不要用 isNaN() 猜测数字,这些捷径会损坏数据和标识符。
Go 可以使用 encoding/csv.Reader 和 encoding/csv.Writer 处理正确引用。设置 FieldsPerRecord,检查 ParseError,限制底层 io.Reader,刷新后检查 Writer.Error()。encoding/json.Decoder.UseNumber() 可以比默认 float64 更久地保留数字文本,但最终仍由下游 Schema 决定数字是否合法。
往返与数据质量测试
只有在声明规范化策略后,往返才有意义。应测试:
- 嵌套对象和数组,包括空数组;
- 缺失字段、显式
null、空字符串、零、false 和"00123"; - 逗号、引号、CRLF、LF、制表符、Unicode 和嵌入换行;
- 重复表头、多余列、缺失列、空行和尾部分隔符;
- 大整数、小数精度、带时区日期和非 ASCII 文件名;
- 公式样式字符串和包含密钥的字段;
- 行、字段、字节、嵌套深度和输出限制。
比较应用约定后的语义记录,而不是原始字节。若要求无损导出,应使用能保留源模型的格式,或输出旁车 Schema 和关系文件。
常见使用场景
API 或事件导出
选择稳定投影,而不是倾倒完整 payload。保留 Schema 版本,脱敏凭据,并定义未知字段的处理方式。
电子表格审查
使用可读表头和目标电子表格兼容的方言,但中和公式样式单元格,并提醒下游可能自动转换日期和数字。保留未修改的机器可读源文件。
数据库导入
加载前验证列数、null 标记、编码、约束和事务行为。CSV 只是交换层,不能证明值已经符合数据库 Schema。
分析与报表
只扁平化报表需要的维度。一对多数据应使用独立表或明确聚合规则,避免度量值被重复计算。
常见问题
任意 JSON 都能无损转换为一个 CSV 文件吗?
不能。单个 CSV 表格无法自然表示任意嵌套、对象数组、混合类型数组,以及缺失与 null 的区别。应使用 Schema、多张相关表、单元格内 JSON,或能保留原模型的格式。
CSV 总是比 JSON 小吗?
不是。重复转义、扁平化表头、多行字段和数组反规范化都可能让 CSV 变大。应测量实际数据和编码,而不是依赖格式印象。
应该自动把 CSV 字符串转成数字和布尔值吗?
只有在显式 Schema 允许时才应该。自动推断可能把带前导零的标识符变成数字、改变精度,或把 "true" 这样的标签重新解释。领域要求文本时应保留文本。
加引号足以防止 CSV 公式注入吗?
不够。电子表格在解析后仍可能执行被识别为公式的单元格。应执行针对目标消费者的输出策略,并实测。
为什么按换行拆 CSV 会失败?
引号字段中可以合法包含换行,记录还可能使用 CRLF、LF 或其他方言。应使用能跟踪引号状态并验证行宽的解析器。
一手来源
- RFC 4180:CSV 通用格式与 MIME 类型
- W3C:CSV on the Web
- RFC 8259:JSON 数据交换格式
- Python
csv文档 - Go
encoding/csv文档 - OWASP:CSV Injection
总结
JSON 到 CSV 的转换是可能丢失信息的 Schema 变换,不是替换分隔符。应先定义行、列、数组、null、类型、编码、安全和限制;使用理解方言的解析器;验证代表性往返;当表格导出无法承载原始含义时,保留机器可读源文件。