返回 网站动态
DevTool Team

JSON 数组转 Excel:把嵌套记录导出成关联工作表

对象数组是一对多关系,不是一个单元格的值。讲清数组如何变成带父行标识的独立工作表、三种导出模式各保留什么,以及哪些结构会真的丢数据。

问题的本质

工作表是矩形,JSON 是树。把前者变成后者,意味着要回答一个没有普适答案的问题:一个分支该变成什么?

JSON 里只有两种分支,而它们的行为截然不同。

对象是一组固定的具名字段。 {"city": "Berlin", "zip": "10115"} 对这条记录来说永远就是这两个键。固定的名字集合,能干净地映射到固定的列集合。

数组是数量未知的一串元素。 ["pro", "beta"] 这条记录里有两个,下一条可能有九个。没有哪个固定的列数装得下它。

这个区别决定了本文剩下的全部内容:对象可以展平,数组不行。

对象是怎么变成列的

展平会遍历这棵树,为每个叶子值收集一条路径:

{
  "id": 1024,
  "name": "Alice Chen",
  "address": {
    "city": "Berlin",
    "zip": "10115"
  }
}

找到的路径:idnameaddress.cityaddress.zip。每条各成一列。

各家工具的差别在表头上。用完整点号路径精确,但读起来很不友好:

id name address.city address.zip

用最后一段则可读,也更符合表格的实际用途:

Id Name City Zip

取最后一段的做法还会顺带规范命名:下划线和连字符变成空格,驼峰拆成单词,首字母大写。于是 created_at 变成 Created atorderId 变成 Order Id

完整路径并没有消失——它作为这一列的来源路径保留下来,也正是你想让这一列取别的值时要改的东西。可读的表头和精确的来源,两者同时具备。

唯一会咬人的情况

两个不同分支可能以同一个词结尾:

{
  "user": { "name": "Alice" },
  "company": { "name": "Northwind" }
}

路径是 user.namecompany.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 —— 从父记录里复制下来的标识字段。

这些标识列取自父记录中可识别的字段名:iduuiduidkeycodenameemailorderIdorder_idcreatedAtcreated_at。父记录里存在哪些,就复制哪些到子行。

有了这张表,「按客户汇总 Qty」就是两分钟的数据透视表。而同样的数据挤在一个单元格里当 JSON 文本,就是一份手工重录的活。

直接试试: 我们的 JSON 转 Excel 工具在浏览器里就能做这件事。打开「嵌套数组导出为工作表」,JSON 里的每个对象数组都会变成独立工作表,父级标识列已经填好——不上传、免注册。

需要特别处理的值

展平决定的是形状。另外有几类具体的值也需要留意,否则表格软件会把它们改掉。

null 变成空单元格,而不是文本 null。这对聚合很关键:AVERAGE 会忽略空单元格,却会把 0 算进去,所以用空单元格表示缺失,是「平均值正确」和「平均值错误」之间的差别。

布尔值保持布尔类型,显示为 TRUEFALSE。如果转换器把它们写成字符串 "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 列之后,导出会开始明显变慢。

判断你的结构需要什么

按顺序过一遍:

  1. 记录数组在哪? 其余都是元信息。把根节点设成它。
  2. 有字段是嵌套对象吗? 它们会自动展平。展平后检查是否出现重复表头。
  3. 有字段是基本类型数组吗? 根据读者是谁,在保留 JSON、拼接、计数之间选一个。
  4. 有字段是对象数组吗? 把它们导出成独立工作表。
  5. 有超过 15 位的 ID 吗? 确认它们是以文本形式、每一位完整地过来了。
  6. 结果有多宽? 超过几十列通常意味着这份数据该拆开。

用 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.namecompany.name 都会生成表头为 Name 的列。它们的完整来源路径仍然不同,并且在映射面板里可见,所以分享文件之前先改掉其中一个表头。重复表头会让 VLOOKUP 和数据透视表以很难排查的方式出错。

Excel 的行数、列数和单元格上限是多少?

单张工作表最多 1,048,576 行、16,384 列,单个单元格最多 32,767 个字符。数组真正会撞上的是单元格字符上限,因为把一个大数组当成原始 JSON 文本塞进一个单元格很容易超过它并被截断。列数也会随嵌套深度快速增长,而表格在远未触及 16,384 这个上限时就已经难以阅读了。

导出后的数组行怎么和父记录关联起来?

靠写进子工作表的标识列。每一条子行都会带上父记录的行号和它在数组中的位置,再加上父记录里可识别的标识字段,比如 idnameemail 或订单号。导出之后,正是这些列让你能用查找函数或数据透视表把关系重新拼回来。

延伸阅读

  • JSON 转 Excel 完整指南 —— 完整流程,以及什么时候该选在线工具而不是 Power Query 或 Python。
  • JSON 转 CSV 指南 —— CSV 为什么表达不了多工作表,以及这对嵌套数据意味着什么代价。

小结

对象展平成列,不丢任何东西。真正需要决策的是数组:基本类型可以在单元格里以 JSON、拼接文本或计数的形式存在,但对象数组属于独立工作表,并用标识列链回父行。记得检查重复表头、确认长 ID 以文本形式完整保留;如果结果宽达几十列,那是数据在告诉你,它不该只是一张表。

打开 JSON 转 Excel 工具 → —— 按数组选择导出方式、嵌套数组导出为工作表,全部在浏览器里完成。