问题的本质
工作表是矩形,JSON 是树。把前者变成后者,意味着要回答一个没有普适答案的问题:一个分支该变成什么?
JSON 里只有两种分支,而它们的行为截然不同。
对象是一组固定的具名字段。 {"city": "Berlin", "zip": "10115"} 对这条记录来说永远就是这两个键。固定的名字集合,能干净地映射到固定的列集合。
数组是数量未知的一串元素。 ["pro", "beta"] 这条记录里有两个,下一条可能有九个。没有哪个固定的列数装得下它。
这个区别决定了本文剩下的全部内容:对象可以展平,数组不行。
对象是怎么变成列的
展平会遍历这棵树,为每个叶子值收集一条路径:
{
"id": 1024,
"name": "Alice Chen",
"address": {
"city": "Berlin",
"zip": "10115"
}
}
找到的路径:id、name、address.city、address.zip。每条各成一列。
各家工具的差别在表头上。用完整点号路径精确,但读起来很不友好:
| id | name | address.city | address.zip |
|---|
用最后一段则可读,也更符合表格的实际用途:
| Id | Name | City | Zip |
|---|
取最后一段的做法还会顺带规范命名:下划线和连字符变成空格,驼峰拆成单词,首字母大写。于是 created_at 变成 Created at,orderId 变成 Order Id。
完整路径并没有消失——它作为这一列的来源路径保留下来,也正是你想让这一列取别的值时要改的东西。可读的表头和精确的来源,两者同时具备。
唯一会咬人的情况
两个不同分支可能以同一个词结尾:
{
"user": { "name": "Alice" },
"company": { "name": "Northwind" }
}
路径是 user.name 和 company.name,表头都是 Name。
列里的值是对的,来源路径也仍然可区分,但一张有两个同名列的表格会让查找函数出错,也会让打开它的人困惑。导出前改掉一个表头。每次转换不熟悉的结构时都值得检查这一项——这是展平过程中最常见的意外。
深度与列数膨胀
对象展平是无损的,但宽度涨得很快。一条记录里有四个各含五个字段的嵌套对象,还没算顶层字段就已经二十列了。
Excel 单张工作表上限 16,384 列,听起来很宽裕,直到你去转一份嵌套很深的配置数据。而可读性失效得比这早得多。如果展平后超过几十列,这是个信号:这份 JSON 同时在描述好几种东西,它更应该变成几张工作表,而不是一张很宽的表。
数组是怎么变成单元格的
基本类型的数组——字符串、数字、布尔——在单个单元格里有三种合理表示。以 tags: ["pro", "beta"] 为例:
| 模式 | 结果 | 什么时候用 |
|---|---|---|
| 保留 JSON | ["pro","beta"] |
后续还有程序会再解析这个单元格 |
| 拼接 | pro, beta |
这张表是给人看的 |
| 计数 | 2 |
你要按数量排序、求和或做透视 |
选之前值得先知道边界情况。空数组 [] 在保留 JSON 时是文本 [],拼接时是空单元格,计数时是数字 0。只有计数模式给出的是能直接参与算术的值。
最容易让人失望的是拼接模式,因为它看起来没问题,直到数组里装的是对象而不是标签:
{"sku":"A-1","qty":2}, {"sku":"B-7","qty":1}
这是一次正确的拼接,也是一个没法用的结果。于是就引出了真正的解法。
对象数组怎么变成第二张工作表
对象数组是一对多关系。用关系型的话说,它是一张子表,而它在表格里的正确表现形式是第二张工作表,不是一个挤爆的单元格。
拿两个带订单的客户举例:
{
"data": [
{
"id": 1024,
"name": "Alice Chen",
"orders": [
{ "sku": "A-1", "qty": 2 },
{ "sku": "B-7", "qty": 1 }
]
},
{
"id": 1025,
"name": "Bob Miller",
"orders": [{ "sku": "C-3", "qty": 5 }]
}
]
}
主表一个客户一行。orders 数组额外生成一张以其路径命名的工作表:
工作表:Orders
| Parent row | Item index | Parent id | Parent name | Sku | Qty |
|---|---|---|---|---|---|
| 1 | 1 | 1024 | Alice Chen | A-1 | 2 |
| 1 | 2 | 1024 | Alice Chen | B-7 | 1 |
| 2 | 1 | 1025 | Bob Miller | C-3 | 5 |
其中三列的存在,纯粹是为了保住原本由树结构编码的那层关系:
- Parent row —— 这个元素来自主表的第几行。
- Item index —— 元素在其数组中的位置,让原始顺序不丢。
- Parent id / Parent name —— 从父记录里复制下来的标识字段。
这些标识列取自父记录中可识别的字段名:id、uuid、uid、key、code、name、email、orderId、order_id、createdAt、created_at。父记录里存在哪些,就复制哪些到子行。
有了这张表,「按客户汇总 Qty」就是两分钟的数据透视表。而同样的数据挤在一个单元格里当 JSON 文本,就是一份手工重录的活。
直接试试: 我们的 JSON 转 Excel 工具在浏览器里就能做这件事。打开「嵌套数组导出为工作表」,JSON 里的每个对象数组都会变成独立工作表,父级标识列已经填好——不上传、免注册。
需要特别处理的值
展平决定的是形状。另外有几类具体的值也需要留意,否则表格软件会把它们改掉。
null 变成空单元格,而不是文本 null。这对聚合很关键:AVERAGE 会忽略空单元格,却会把 0 算进去,所以用空单元格表示缺失,是「平均值正确」和「平均值错误」之间的差别。
布尔值保持布尔类型,显示为 TRUE 和 FALSE。如果转换器把它们写成字符串 "true"/"false",筛选和条件格式就都失效了。
数字保持数值类型,包括 91.5 这样的小数。
16 位及以上的整数会被有意写成文本。超过约 15 到 16 位有效数字后,无论 JavaScript 的数字类型还是 Excel 的显示都无法精确保存,9007199254740993123 会变成 9007199254740993000。写成文本能保住每一位。更短的数字仍是数值类型,不影响正常计算。
最后这一条,在任何含订单号、雪花 ID、账号的数据集上都值得检查一遍。它的破坏是无声的——不报错、不警告,只是末尾几位错了。
动手前该知道的上限
| 上限 | 数值 |
|---|---|
| 单张工作表行数 | 1,048,576 |
| 单张工作表列数 | 16,384 |
| 单元格字符数 | 32,767 |
嵌套 JSON 真正会撞上的是单元格字符上限。把一个大数组作为原始 JSON 文本塞进一个单元格,很容易超过 32,767 个字符,超出部分会被截断。数组比较大就导出成工作表,别放单元格里。
浏览器端转换还额外受可用内存限制,具体多少取决于设备和浏览器,无法给出一个统一数字。在触及硬上限之前,数据量大到一定程度就会出现实际的性能拐点——大约 5 万行或 500 列之后,导出会开始明显变慢。
判断你的结构需要什么
按顺序过一遍:
- 记录数组在哪? 其余都是元信息。把根节点设成它。
- 有字段是嵌套对象吗? 它们会自动展平。展平后检查是否出现重复表头。
- 有字段是基本类型数组吗? 根据读者是谁,在保留 JSON、拼接、计数之间选一个。
- 有字段是对象数组吗? 把它们导出成独立工作表。
- 有超过 15 位的 ID 吗? 确认它们是以文本形式、每一位完整地过来了。
- 结果有多宽? 超过几十列通常意味着这份数据该拆开。
用 Python 实现
做可复用的数据管道,pandas 能覆盖同样的场景:
import json
import pandas as pd
with open("response.json", encoding="utf-8") as f:
payload = json.load(f)
# 对象展平成点号命名的列
main = pd.json_normalize(payload["data"], sep=".")
# 对象数组单独成表,并带上父级标识
orders = pd.json_normalize(
payload["data"],
record_path="orders",
meta=["id", "name"],
meta_prefix="parent_",
)
with pd.ExcelWriter("output.xlsx") as writer:
main.drop(columns=["orders"]).to_excel(writer, sheet_name="Customers", index=False)
orders.to_excel(writer, sheet_name="Orders", index=False)
与浏览器工具有两点差异值得注意。json_normalize 会把完整点号路径作为列名,所以你拿到的是 address.city 而不是 City,如果这张表要给人看,用 df.rename(columns=...) 改一下。另外 pandas 会把长整数读成浮点并四舍五入,ID 列要传 dtype=str,或者在写出前先转换。
常见问题
JSON 的对象数组怎么导出到 Excel?
给它一张独立的工作表。对象数组是一对多关系,在表格里的正确表现形式是第二张表:一个元素一行,再加上标识每个元素属于哪一条父记录的列。把它塞进单个单元格当成 JSON 文本或拼接文本,技术上可行,但产出的东西没法筛选、也没法做数据透视。
三种数组导出模式有什么区别?
保留原始 JSON 文本什么都不丢,适合后面还有程序会再解析这个单元格的场景。拼接得到「pro, beta」这类便于阅读的文本,适合装简单标签、给人看的表。计数把数组换成元素个数,是唯一能直接参与求和与排序的模式。空数组在三种模式下表现不同,分别是方括号文本、空单元格和数字 0。
同一条记录里的嵌套对象会变成什么?
会展平成独立的列,这个过程是无损的。每个叶子值保有自己的路径,所以位于 address.city 的值会单独成列。默认表头取该路径最后一段并做人性化处理,因此 address.city 生成的表头显示为 City,而完整点号路径会作为这一列的来源路径保留下来。
两个嵌套字段同名会怎样?
你会得到两个表头相同的列。因为默认表头取路径的最后一段,user.name 和 company.name 都会生成表头为 Name 的列。它们的完整来源路径仍然不同,并且在映射面板里可见,所以分享文件之前先改掉其中一个表头。重复表头会让 VLOOKUP 和数据透视表以很难排查的方式出错。
Excel 的行数、列数和单元格上限是多少?
单张工作表最多 1,048,576 行、16,384 列,单个单元格最多 32,767 个字符。数组真正会撞上的是单元格字符上限,因为把一个大数组当成原始 JSON 文本塞进一个单元格很容易超过它并被截断。列数也会随嵌套深度快速增长,而表格在远未触及 16,384 这个上限时就已经难以阅读了。
导出后的数组行怎么和父记录关联起来?
靠写进子工作表的标识列。每一条子行都会带上父记录的行号和它在数组中的位置,再加上父记录里可识别的标识字段,比如 id、name、email 或订单号。导出之后,正是这些列让你能用查找函数或数据透视表把关系重新拼回来。
延伸阅读
- JSON 转 Excel 完整指南 —— 完整流程,以及什么时候该选在线工具而不是 Power Query 或 Python。
- JSON 转 CSV 指南 —— CSV 为什么表达不了多工作表,以及这对嵌套数据意味着什么代价。
小结
对象展平成列,不丢任何东西。真正需要决策的是数组:基本类型可以在单元格里以 JSON、拼接文本或计数的形式存在,但对象数组属于独立工作表,并用标识列链回父行。记得检查重复表头、确认长 ID 以文本形式完整保留;如果结果宽达几十列,那是数据在告诉你,它不该只是一张表。
打开 JSON 转 Excel 工具 → —— 按数组选择导出方式、嵌套数组导出为工作表,全部在浏览器里完成。