TL;DR — 按用途选方法 10 秒速查
- 现在就要把 JSON 文件转成表格: FormatArc JSON to CSV,浏览器内完成,无需上传,嵌套对象自动展开为点号列
- 每条记录里包含对象数组(如订单和订单明细): 先决定「一行 = 一笔订单」还是「一行 = 一条明细」。见下文 按 JSON 形状选择转换策略
- 需要数组中每个元素单独一行: 用 Miller 的
mlr --ijson --ocsv cat或pandas.json_normalize(data, record_path=...) - 数据中有 16 位以上 ID: 不要用 JavaScript 系的转换器。见下文 2^53 以上的大整数
- 打算用 Excel 打开: 双击之前先读 在 Excel 中不损坏数据地打开 CSV
| 方法 | 安装 | 嵌套对象 | 记录内数组 | 浏览器内完成(无上传) |
|---|---|---|---|---|
| FormatArc | 无需安装 | 点号列 | 保留为 JSON 字符串存 1 个单元格 | 是 |
| json-2-csv (npm) | npm i json-2-csv | 点号列 | 保留为 JSON 字符串存 1 个单元格 | 否(Node) |
Miller (mlr) | brew install miller | 点号列 | 展开为 items.1.sku、items.2.sku … | 否(CLI) |
pandas json_normalize | pip install pandas | 点号列 | Python repr 存 1 格,record_path 按行展开 | 否(Python) |
jq | brew install jq | 需手写映射 | 需手写映射 | 否(CLI) |
以上表格内容全部为实测值。输入数据、测量脚本和原始输出保存在仓库 scripts/benchmarks/json-to-csv-guide/ 目录下(2026-07-26 实测),下文引用的所有输出均直接来自该次运行结果。
30 秒完成转换
打开 JSON to CSV,粘贴对象数组,点击运行。以以下输入为例:
[
{ "name": "张伟", "email": "zhangwei@example.com", "role": "admin", "address": { "city": "北京" } },
{ "name": "李娜", "email": "lina@example.com", "role": "viewer", "address": { "city": "上海" } }
]
输出 CSV 如下:
name,email,role,address.city
张伟,zhangwei@example.com,admin,北京
李娜,lina@example.com,viewer,上海


转换在浏览器标签页内完成,JSON 数据不会发送到任何服务器。当你需要直接粘贴生产环境的 API 响应(包含用户信息、订单数据等)时,这一点尤为重要。哪些是受保护的、哪些不是,请参见在线工具安全检测。
如果输入不是合法 JSON,错误信息会带行号返回,路径与 JSON 格式化工具 一致。常见报错的排查方法见 JSON parse 报错排查。
同一份 JSON 经过 4 个转换器,结果完全不同
JSON 是树形结构,CSV 是二维矩形表格。每个转换器都有自己的规则把树压平为矩形,而这些规则因工具而异。我们用 15 条输入数据分别跑过 4 个转换器,逐一记录了输出。
运行环境(依据 results.json):macOS arm64,Node v26.3.1,FormatArc 使用 lib/tooling.ts(PapaParse 5.5.2),json-2-csv 5.5.11,Miller 6.19.0,pandas 3.0.5。4 个工具均未指定任何额外参数,使用默认设置——这正是我们想确认的:什么都不配置时各工具输出什么。
4 个工具一致的部分
嵌套对象会被展平为点号表示法(dot notation)。对 {"name":"张伟","address":{"city":"北京","zip":"100000"}},4 个工具均输出:
name,address.city,address.zip
张伟,北京,100000
三层嵌套(如 meta.created.by.name)行为相同。未用数组包裹的单个对象,4 个工具均输出为 1 行 CSV。包含逗号、双引号、换行符的值的引用处理也完全一致:
who,quote,note
"Smith, John","She said ""hi""","line1
line2"
这与 RFC 4180在新标签页中打开 第 2 节的引用规则一致:包含换行、双引号或逗号的字段用双引号包裹,内部双引号用两个连续双引号转义。一个值得注意的差异:记录分隔符(行尾)FormatArc 使用 CRLF(PapaParse 默认值,也是 RFC 4180 规定的格式),而 json-2-csv、Miller、pandas 使用 LF。
数组会分裂为 3 种结果
输入:
[{"name":"张伟","tags":["admin","billing"]},{"name":"李娜","tags":["viewer"]}]
| 转换器 | 输出 |
|---|---|
| FormatArc | name,tags / 张伟,"[""admin"",""billing""]" / 李娜,"[""viewer""]" |
| json-2-csv | 与 FormatArc 相同 |
| Miller | name,tags.1,tags.2 / 张伟,admin,billing / 李娜,viewer, |
| pandas | name,tags / 张伟,"['admin', 'billing']" / 李娜,['viewer'] |
三种完全不同的输出形态。FormatArc 和 json-2-csv 将数组作为合法 JSON 文本保留在 1 个单元格中,后续可以用脚本重新解析。Miller 把表横向扩展——方便,但如果某条记录的 tag 有 40 个,列数就会变成 40。pandas 输出带单引号的 Python repr 字符串,这不是合法 JSON,后续步骤如果对其执行 JSON.parse 会直接报错。
对象数组的行为类似。{"order":"A-1","items":[{"sku":"X1","qty":2},{"sku":"X2","qty":1}]} 在 FormatArc 和 json-2-csv 中变成 1 个单元格的 JSON 文本,在 Miller 中展开为 items.1.sku, items.1.qty, items.2.sku, items.2.qty 列。
键缺失:空单元格、字符串 "undefined"、还是报错
实际 API 响应中经常包含可选字段。输入:
[{"id":1,"name":"张伟"},{"id":2,"name":"李娜","nickname":"小娜"},{"id":3,"phone":"010-12345678"}]
FormatArc 和 pandas 输出空单元格:
id,name,nickname,phone
1,张伟,,
2,李娜,小娜,
3,,,010-12345678
json-2-csv 在缺失位置写入字符串 undefined:
id,name,nickname,phone
1,张伟,undefined,undefined
2,李娜,小娜,undefined
3,undefined,undefined,010-12345678
Miller 直接拒绝该文件:mlr: CSV schema change: first keys "id,name"; current keys "id,phone"。作为流式处理工具这是合理的设计,但意味着昨天还在正常运行的管道可能因为多了一个可选字段就中断。
FormatArc 的做法是:收集所有记录中出现的键的并集,按首次出现顺序排列,在写第 1 行之前确定列数。行不会错位。
null:空单元格还是 4 个字母 n-u-l-l
[{"id":1,"deleted_at":null},{"id":2,"deleted_at":"2026-07-01"}]
FormatArc 和 pandas 输出空单元格,json-2-csv 和 Miller 写入 4 个字母的字符串 null。如果把这个 CSV 导入数据库,前者变成真正的 NULL,后者变成看起来像 NULL 的字符串。JSON 转 CSV 之后「WHERE 条件匹配不上」的问题,大多数根源就在这里。
空对象会产生 4 种不同的表
[{"id":1,"meta":{}},{"id":2,"meta":{"source":"api"}}] 的情况:
| 转换器 | 输出 |
|---|---|
| FormatArc | id,meta,meta.source — 空的 {} 导致每行多出一个空 meta 列 |
| json-2-csv | 表头相同,但把 {} 和 {"source":"api"} 原样写入单元格,值重复 |
| Miller | 报错:CSV schema change: first keys "id,meta"; current keys "id,meta.source" |
| pandas | meta 列直接消失:id,meta.source |
FormatArc 的行为是诚实的,但看起来不太干净。如果你发现多了一列全是空值,说明数据中某处存在空对象 {}。
键名中包含点号时,3 个工具会静默覆盖列
这是会丢数据的情况,4 个工具中 3 个会丢失数据。输入:
[{"a.b":1,"a":{"b":2}}]
这条记录有 2 个不同的值:字面名为 a.b 的键持有 1,嵌套路径 a 下的 b 持有 2。FormatArc、Miller、pandas 均输出:
a.b
2
1 丢了。只有 json-2-csv 区分了两者,对字面键做转义:
a\.b,a.b
1,2
如果你的 JSON 使用了带点号的键名——比如分析事件名 page.view.count、MongoDB 风格的文档、Prometheus 风格的标签——转换前务必检查是否存在冲突,或者使用 json-2-csv。
2^53 以上的大整数在 JavaScript 系工具中会被四舍五入
[{"id":9007199254740993,"order_no":12345678901234567890}]
| 转换器 | 输出 |
|---|---|
| FormatArc | 9007199254740992,12345678901234567000 |
| json-2-csv | 9007199254740992,12345678901234567000 |
| Miller | 9007199254740993,12345678901234567890 |
| pandas | 9007199254740993,12345678901234567890 |
这不是 CSV 的问题,也不是这两个 JavaScript 工具的 bug。JavaScript 的 JSON.parse 把所有数字都转为双精度浮点数(double),双精度浮点数无法精确表示 2^53 以上的整数。值在到达 CSV 写入器之前就已经被篡改了。Snowflake ID、X(Twitter)ID、部分支付流水号、64 位数据库主键都属于这个范围。如果你的 ID 位数很长,在 JSON 中用字符串包裹它们,或者使用 Miller、pandas、jq 等不经过 JavaScript 数字类型的工具。
格式错误的输入:显式报错还是静默输出空文件
| 输入 | FormatArc | json-2-csv | Miller | pandas |
|---|---|---|---|---|
["a","b","c"] | 报错「数组的每个元素必须是对象」 | 输出 3 个空行,不报错 | 抛出异常 | 抛出异常 |
[] | 报错「JSON 数组为空」 | 输出 1 个空行,不报错 | 输出空,不报错 | 输出空,不报错 |
最危险的是「退出码 0 但输出为空文件」这种情况。cron 定时任务会毫无警告地把昨天正常的文件覆盖为空白。
FormatArc 的转换规则(实现层面)
以下是实现的实际规则,不是概括。对应 lib/tooling.ts 中的 convertJsonToCsv 函数,与上文实测结果完全一致。
- 顶层必须是对象数组或单个对象。单个对象输出为 1 行 CSV。原始值数组、空数组、裸字符串或数字会被拒绝并附带提示信息
- 嵌套对象递归展平为点号列(
address.city、meta.created.by.name)。无深度限制 - 数组不展开。通过
JSON.stringify序列化后保留在 1 个单元格中,因此["admin","billing"]仍然是可解析的 JSON - 列是所有记录键的并集,按首次出现顺序排列。只出现在最后一条记录中的键也会有对应列,前面的行该列为空单元格。行不会错位
null和undefined变为空单元格。空对象{}也变为空单元格,但保留自己的列- 引用处理遵循 PapaParse 的
unparse规则:包含逗号、引号、换行的字段加引号,内部引号双写,记录分隔符为 CRLF - 非法 JSON 带行号报错(与 JSON 格式化工具 相同的错误处理路径)
数据不会离开浏览器标签页。没有上传步骤,服务器上没有临时文件,转换就是页面内一次函数调用。
按 JSON 形状选择转换策略
扁平对象数组
没有需要判断的地方。粘贴、运行,直接得到结果。
包含嵌套对象的记录
点号表示法是各工具的默认答案,人眼也能轻松追溯值的来源(address.city 来自哪里一目了然)。需要预先确认的只有两点:现有键名中是否已包含点号(即上述冲突情况),以及读取 CSV 的下游程序能否正确处理表头中的点号。部分 SQL 加载器需要表头加引号,Google Sheets 则没有这个问题。
包含标量数组的记录
有 3 种选择:
- 保留为 JSON 文本存 1 个单元格(FormatArc 默认行为)。适合 CSV 作为中间文件、后续脚本会重新读取的场景
- 用 Miller 展开为
tags.1、tags.2列。适合元素数量少且固定不变的情况(坐标对、RGB 三通道值等) - 人类直接阅读的表,在转换前用分隔符拼接:先跑
jq '.[] |= (.tags |= join(";"))' data.json,再转换。分隔符用;而非,,避免单元格被引号包裹
包含对象数组的记录(最棘手的情况)
使用转换工具之前,先定义「CSV 的 1 行代表什么」。
- 1 行 = 父级(1 笔订单):数组保留为 JSON 文本存 1 个单元格。FormatArc 默认就是这个行为。明细完整保留,后续脚本可以解析单元格
- 1 行 = 子级(1 条明细):这不是格式变化,而是完全不同的表。使用
pandas.json_normalize(data, record_path="items", meta=["order"]),父级字段会在每条子记录中重复展开 - 拆成 2 个文件:父级字段输出为
orders.csv,子级用jq '[.[] | .order as $o | .items[] | {order:$o} + .]' data.json提取后输出为items.csv。这是规范化(normalization)的结果,如果 CSV 要导入数据库,选这个
应该避免的做法:把可变长度的数组按 Miller 默认行为展开为 items.1.sku, items.2.sku, …。列数由「恰好子元素最多的那条记录」决定,数据一变 schema 就变。
键不一致的记录
FormatArc 和 pandas 无需配置即可处理。Miller 需要加 unsparsify 参数才能填补缺失值而不中断:
mlr --ijson --ocsv unsparsify data.json
或者先用 jq '[.[] | {id, name, nickname, phone}]' 把所有记录统一为相同的键集合。
在 Excel 和 Google Sheets 中不损坏数据地打开
损坏数据的往往不是转换工具,而是电子表格软件本身。
- 编码与乱码。CSV 格式本身没有编码信息字段。RFC 4180在新标签页中打开 也仅说明非 US-ASCII 字符集需通过 MIME
charset参数传递,本地文件不具备该参数。FormatArc 下载的 CSV 是无 BOM 的 UTF-8 文件。Windows 版 Excel 在没有 BOM 的情况下可能将非 ASCII 字符按 GBK 或 Windows-1252 解读导致乱码。不要双击文件,而是通过「数据」选项卡 >「从文本/CSV 导入」,手动指定编码为 UTF-8。Google Sheets 能正确自动识别 UTF-8 - 16 位以上的数字。Excel 官方文档明确标注数字精度为 15 位在新标签页中打开,超出部分会被替换为 0。与前述 2^53 四舍五入叠加,长 ID 可能被双重损坏。导入时将该列指定为「文本」类型
- 前导零。
007、13800001234这样的值,如果不以文本列导入,会被 Excel 当作数字处理,前导零丢失或变成日期 - 看起来像日期的字符串。
2026-07-01不用说,1-2这样的版本号、部分基因名也会被自动转为日期。导入时指定列类型为文本最安全 - 以
=、+、-、@开头的单元格。本次对比的 4 个工具均未对其做转义(1+1原样输出)。是否将其作为公式求值由电子表格决定,这是 CSV 注入攻击在新标签页中打开的基本形式。如果 CSV 包含用户输入且会被他人打开,在这些单元格前加单引号(')或以文本列导入
分隔符:逗号、分号还是制表符
FormatArc 始终用逗号(,)分隔。输出经过 PapaParse 的 unparse 函数(lib/tooling.ts 中的 convertJsonToCsv),没有分隔符设置选项。RFC 4180在新标签页中打开 也规定分隔符为逗号。
这个默认值在有人双击打开文件时就会出问题。Microsoft 文档说明:Excel 保存为 .csv 时「默认列表分隔符是逗号,可通过 Windows 区域设置更改」(导入或导出文本文件在新标签页中打开)。同一页面还指出:将小数点符号设为逗号后,「Excel 会使用分号作为列表分隔符」。欧洲大陆和拉美大部分地区就是这个配置。在那样的环境下,双击打开逗号分隔的 CSV 文件,所有数据会挤在 A 列里。
2 种有效方法,1 种无效方法:
- 不要双击,用导入功能。「数据」>「从文本/CSV 导入」,分隔符选逗号,编码选 UTF-8。与上节编码问题的处理路径相同,不增加额外步骤
- 如果文件要交给会双击打开的人,用 Miller 生成分号分隔的文件。实测 Miller 6.19.0 中
mlr --ijson --ocsv --ofs ';' cat data.json会把表头a,b变为a;b - 不要用查找替换把逗号全部分号替换。包含逗号的字段已经被双引号包裹,简单替换会把该字段从中间截断
浏览器不适合的场景
用 curl 拿到的 API 响应粘贴后快速查看,浏览器转换器最快且最安全。但 2GB 的导出文件、或夜间批处理任务中的一个步骤,就不太适合了。
# Miller:对象展平,数组展开为索引列,支持流式处理
mlr --ijson --ocsv cat data.json > out.csv
# jq:映射由你手写,不依赖工具猜测
jq -r '(.[0] | keys_unsorted), (.[] | [.[]]) | @csv' data.json > out.csv
# pandas:点号展平 + record_path 按数组元素展开为行
python3 -c "import pandas,json; print(pandas.json_normalize(json.load(open('data.json'))).to_csv(index=False))" > out.csv
这 3 个工具互不兼容。json_normalize 把字典展开为点号列,但要按数组元素拆行必须指定 record_path。jq 的 @csv 要求输入已经是标量数组,传入嵌套对象会报错而非自动展平。Miller 默认展平对象和数组(数组索引从 1 开始),但记录 schema 不一致时会中断。详见 Miller flatten 文档在新标签页中打开、jq 手册在新标签页中打开、pandas.json_normalize 参考在新标签页中打开。
反向转换(CSV 转 JSON)及其注意事项,见 CSV 转 JSON 指南。
从 CSV 转回 JSON 不能还原
即使把转换后的 CSV 再转回 JSON(往返转换),也无法恢复原始文档。用第一个例子的 CSV 结果反向转换:
[
{
"name": "张伟",
"email": "zhangwei@example.com",
"role": "admin",
"address.city": "北京"
}
]
address.city 回来的时候是一个带点号的扁平键名,而不是嵌套的 address 对象。没有任何工具能自动把点号表示法重新嵌套,因为 address.city 本身就是一个完全合法的 JSON 键名,转换器无法判断你的原始意图。这与前面讨论的键冲突问题是同一个原因。
因此,JSON 转 CSV 应视为面向电子表格或分析场景的单向导出操作,原始数据始终保留在 JSON 中。如果确实需要恢复嵌套结构,用 jq 'map(reduce (to_entries[]) as {$key,$value} ({}; setpath($key | split("."); $value)))' 手动重建,并务必检查结果。
常见问题
CSV 只生成了 1 列且里面是 JSON 文本?
顶层是 {"data":[...]} 这样的「只包含 1 个数组的单个对象」。先把数组提取出来再转换(只粘贴 data 的值,或跑 jq '.data' response.json)。FormatArc 只把外层对象展平为 data 列,不会把内部数组当作行来处理。
嵌套数组中每个元素能单独成 1 行吗?
浏览器版工具不支持,这是有意为之。行数量取决于数据内容,父级字段会被静默重复。解决方案是 pandas.json_normalize(data, record_path="items", meta=[...]),或者先用 jq 重塑数据。
某列内容是 [{"sku":"X1"...}] 这样的 JSON?
这是对象数组被保留为 JSON 文本存 1 个单元格的结果,是预期行为,数据没有丢失。想改变这种形式的 3 种选择见上文 包含对象数组的记录。
数字会被改变吗?
只在 JavaScript 自身规范的范围内会。2^53 以上的整数在 JSON.parse 时会被四舍五入。字符串数据完全不受影响。如果你的 ID 超过 15 位,在 JSON 中用字符串包裹,或使用非 JavaScript 工具。
数据会上传到哪里吗?
不会。所有转换在浏览器标签页内的 JavaScript 中完成,不产生任何网络请求。你可以在开发者工具的网络面板中确认没有外部通信。
JSON 格式不合法时会怎样?
不会输出损坏的 CSV,而是返回带行号的明确错误信息。保持 JSON 排版整洁、易于阅读的方法,见 JSON 排版技巧。
原始数据应该用哪种格式保存?
用能保留结构的格式。CSV 是无类型的二维表,嵌套、数组、null 与空字符串 "" 的区别全部丢失。JSON 作为原始数据,CSV 作为它的视图来分析。关于 CSV 格式本身的限制与编码细节,见 什么是 CSV?。