Skip to content
返回博客
教程

嵌套 JSON 扁平化为 CSV:5 种策略与决策矩阵

立即将嵌套 JSON 扁平化为 CSV:5 种策略 — 点号、索引数组、行展开、Stringify。覆盖 Excel、Pandas、BigQuery、Postgres 的决策矩阵和真实负载示例。

12 分钟

嵌套 JSON 扁平化(flatten)为 CSV:5 种策略与决策矩阵

几何问题

每次都会撞上同一堵墙。API 返回嵌套 JSON,Slack 上的分析师只想要一份电子表格;mongoexport 吐出带 $oid 包装和三层元数据的文档,而 BigQuery 期望一张扁平表。把嵌套 JSON 扁平化为 CSV不是语法问题,而是几何问题。JSON 是一棵树,CSV 是一张网格,树要进网格,就必须先决定枝条如何坍缩。

可选的坍缩策略一共只有五种。选错一种,你可能给 Excel 推送 200 列数据、丢失 Twitter ID 的精度,或者破坏掉整条管道依赖的往返一致性。选对了,整次转换就是一行代码的事。

策略一句话示例适用场景
点号customer.address.cityExcel/Sheets 分析
下划线customer_address_citySQL 友好列名
索引数组items.0.sku, items.1.sku定长数组
行展开每个子元素复制一遍父字段Pandas/BigQuery 分析
Stringify一个 cell 里塞 "{\"city\":\"Seattle\"}"无损往返

下面分别看每种策略,配一张按消费者(Excel、Pandas、BigQuery、Postgres)反查的决策矩阵,再用四份真实负载演示「直觉里的正确答案」往往不是合适的那一个。如果你还需要更宏观的双向转换概览(解析库、流式处理、编码陷阱),可以参考 CSV 与 JSON 互转:方法、陷阱与代码示例

为什么嵌套 JSON 装不进 CSV

JSON 携带了三类 CSV 缺失的结构。层级是对象套对象;序列是数组;混合是两者的组合:对象数组、带数组的对象、数组的数组。一份典型的电商订单往往三者同时出现。

CSV 只有两个维度:行和列。没有第三根坐标轴来承载「这一列底下还挂着三个子节点」。当你要求一棵树进网格时,总要牺牲点什么。要么把子节点摊开成更多列(接受 items.0.options.0.value 这种列名),要么摊开成更多行(父字段重复),要么干脆把它们当字符串塞进单元格,不再当作结构。

下面 5 种策略各自给出了不同的回答。有的保住了可读性,丢掉了往返安全;有的反过来。没有哪种万能,关键看下一个读文件的人是谁。

5 种扁平化策略对比

策略 1:点号(customer.address.city

点号从根走到叶,用 . 拼接 key。每个嵌套对象的叶子节点变成一列,路径直接编码在列名里。

{ "customer": { "address": { "city": "Seattle" } }, "email": "alice@example.com" }

变成

customer.address.city,email
Seattle,alice@example.com

Pandas 里一行就能搞定:

import pandas as pd

data = [{"customer": {"address": {"city": "Seattle"}}, "email": "alice@example.com"}]
df = pd.json_normalize(data, sep='.')
df.to_csv("out.csv", index=False)

JavaScript 里一个小递归函数就够:

function flattenDot(obj, prefix = '', acc = {}) {
  for (const [k, v] of Object.entries(obj)) {
    const key = prefix ? `${prefix}.${k}` : k;
    if (v && typeof v === 'object' && !Array.isArray(v)) {
      flattenDot(v, key, acc);
    } else {
      acc[key] = v;
    }
  }
  return acc;
}

优点:人类可读、是 Pandas 的默认行为、保留原始路径。缺点:列名容易拉得很长(Kubernetes spec 会产生形如 spec.template.spec.containers.0.resources.limits.memory 的列名),并且当真实 key 里本身就含 . 时(Google Analytics 4 的事件参数就是这样),点号会产生歧义。

策略 2:下划线(customer_address_city

思路一样,只是把分隔符换掉。把 . 替换为 _,结果就是 SQL 安全的:SELECT customer_address_city FROM events 不需要给标识符加引号。BigQuery、Snowflake、Postgres 都偏好这种写法。

import pandas as pd
df = pd.json_normalize(data, sep='_')

点号和下划线之间的取舍,纯粹取决于下游工具。Excel 分析师读点号更自然;SQL 引擎对下划线无怨无尤。换一种只要改一个参数。

优点:列名 SQL 安全、符合 BigQuery 标识符规则、无需引号。缺点:当 key 中含 _ 时仍可能歧义(比 . 少见,但并非不会发生)。

策略 3:索引数组(items.0.skuitems.1.sku

对象扁平化得很干净,因为 key 是唯一的;数组就不一样,长度不固定。索引策略把数组下标当作路径段:items[0] 变成 items.0

{ "id": "ord-001", "items": [{"sku": "A"}, {"sku": "B"}] }

变成

id,items.0.sku,items.1.sku
ord-001,A,B

这也是我们的 JSON 转 CSV 在线工具 在 Flatten 模式下的默认行为。每个叶子都占一列,位置信息编码在列名里。

优点:每个值都有自己的单元格、保留位置、不会出现行复制。缺点:列数爆炸(100 个 item 就是 100 列);当不同行的数组长度不同时会产生残缺的表格;下游聚合也做不动(无法 SUM(items.*.qty))。

策略 4:行展开(数组转多行)

不再用「加宽表格」去装下数组,改用「加长表格」。把父字段对每个数组元素重复一次,每个元素自己占一行。

{ "order_id": "ord-001", "customer": "Alice", "items": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 1}] }

变成

order_id,customer,items.sku,items.qty
ord-001,Alice,A,2
ord-001,Alice,B,1

Pandas 里一行同时完成展开和规范化:

import pandas as pd

orders = [{"order_id": "ord-001", "customer": "Alice",
           "items": [{"sku": "A", "qty": 2}, {"sku": "B", "qty": 1}]}]
df = pd.json_normalize(orders, record_path='items', meta=['order_id', 'customer'])
df.to_csv("orders.csv", index=False)

SQL 里 UNNEST 做的是同一件事:

SELECT order_id, item.sku, item.qty FROM orders, UNNEST(items) AS item;

优点:Pandas 和 BigQuery 都原生支持这种形状、聚合自然(GROUP BY order_id)、schema 保持窄。缺点:父字段在每行子记录里都要复制(存储膨胀)、1 对 N 的边界是隐式的(你得有一个 order_id),同层级出现两个数组时如果不仔细分别 UNNEST 就会产生笛卡尔积。

策略 5:Stringify(JSON 塞进 cell)

激进选项:根本不扁平。把整个嵌套值序列化成 JSON 字符串塞进一个单元格。外层表保持扁平,内部结构原封不动地保留下来。

{ "id": "ord-001", "items": [{"sku": "A"}, {"sku": "B"}] }

变成

id,items
ord-001,"[{""sku"":""A""},{""sku"":""B""}]"

这就是我们的 JSON 转 CSV 在线工具 里的 Stringify 模式。列数永远不会爆炸,原始形状逐字节保留,反向转换可以精确还原输入。

优点:100% 无损、列数可预期、反向时配合 Infer types 即可往返安全。缺点:Excel 用户会看到被转义的引号、SQL 引擎要查询内部值得调用 JSON 函数(BigQuery 用 JSON_EXTRACT_SCALAR,Postgres 用 ->>'key'),电子表格公式也无法直接探入单元格。

5 种策略并排对比

同一份输入:{"id":"ord-001","customer":{"name":"Alice"},"items":[{"sku":"A","qty":2},{"sku":"B","qty":1}]}

策略列数往返安全最佳消费者
点号随数组增长Excel 分析师
下划线随数组增长SQL 数仓
索引数组每个数组位 2 列否(反向有歧义)定长数组
行展开列窄,每个子元素 1 行部分(需要 key)Pandas / BigQuery
Stringify固定管道往返

决策矩阵:哪种消费者对应哪种策略

先按消费者查一下,再读出推荐的策略。

消费者推荐策略理由
Excel / Sheets(分析师)点号 + 大数组用 Stringify列名可读;大数组不会把表格撑爆
Excel-EU(DE/FR/IT/ES)点号 + ; 分隔符 + UTF-8 BOM必须用分号;BOM 防止编码乱码
Pandas(json_normalize + explode下划线 + 行展开列名 SQL 友好;展开后与 groupby 配合自然
BigQuery / SnowflakeTSV + Stringify 或 行展开制表符避开引号陷阱;JSON_EXTRACT 直接查 cell
PostgreSQL COPYRFC 4180 + 下划线 + 扁平列名 SQL 安全;严格 RFC 引号
MongoDB → BigQuery ETL直接 load NDJSON,跳过 CSVBigQuery 原生支持 NDJSON,绕开 CSV 这道弯

Excel / Google Sheets:locale 陷阱

Excel 的列名在实际使用里基本没有长度上限,真正的陷阱有三个。

第一是 locale 分裂。欧洲版 Excel(德国、法国、意大利、西班牙)期望 ; 作为分隔符,因为 , 是小数点。逗号分隔的 CSV 一打开,每行内容都会被塞进 A 列。我们 JSON 转 CSV 在线工具 的 Excel preset 一键切换到 ; + CRLF + UTF-8 BOM。

第二是科学记数法。Excel 看到 9007199254740993 会渲染成 9.00719925474E+15。把大整数在源 JSON 里就存成字符串,再打开 BOM,让 Excel 把单元格当作文本。我们的转换器会自动检测大整数。

第三是实际可用的列数上限。Excel 理论上支持 16,384 列,但超过 ~500 列就已经难以维护。要么用 Stringify 把笨重的子树打包,要么转换前用 jq 先做一次投影。

Pandas:json_normalize + explode

处理嵌套数组的标准做法是一次 pass 里同时用 record_pathmeta

import pandas as pd

orders = [{
    "order_id": "ord-001",
    "customer": {"name": "Alice", "city": "Seattle"},
    "items": [{"sku": "SKU-100", "qty": 2}, {"sku": "SKU-205", "qty": 1}]
}]

df = pd.json_normalize(orders, record_path='items',
    meta=['order_id', ['customer', 'name'], ['customer', 'city']], sep='_')
df.to_csv("orders.csv", index=False)

输出是每个 item 一行,order_idcustomer_namecustomer_city 在每行重复。这比先 explodejson_normalize 更好:record_path 跳过了中间那一列对象,而 meta 让你能精确控制哪些父字段会向下传递。当数组元素里又嵌套对象时,可以用 max_level= 限制深度。

BigQuery / Snowflake:TSV + JSON 塞 cell

BigQuery 的 LOAD DATA 对 CSV 引号要求严格,引号文本里再含逗号时经常解析失败。TSV 更安全,因为制表符几乎不会出现在文本字段里:

bq load --source_format=CSV --field_delimiter='\t' \
  dataset.orders gs://bucket/orders.tsv \
  order_id:STRING,customer:STRING,items:STRING

当嵌套数据以 Stringify 后的 JSON 形式落入单列时,BigQuery 仍能用 JSON_EXTRACT_SCALAR 查询内部值:

SELECT order_id, JSON_EXTRACT_SCALAR(items, '$[0].sku') AS first_sku
FROM dataset.orders;

Snowflake 通过 VARIANT 提供同样的能力,路径写法形如 items:0.sku::STRING。两种引擎里,当嵌套数组很大或长度不定时,Stringify + JSON 路径查询都比完全扁平化要好。

PostgreSQL COPY:严格 RFC 4180

COPY ... FROM ... WITH (FORMAT csv, HEADER true) 是你日常会遇到的最严格的 RFC 4180 读取器。有两个行为最容易踩坑。

第一,COPY 不接受 UTF-8 BOM。字节序标记会被当作第一列名的字面前缀(id 会变成 id),所有引用 id 的查询都会悄无声息地失败。给 Postgres 准备文件时关掉 BOM。

第二,COPY 本身没法解析嵌套数据。要么先把数组展开成多行再 load,要么把目标列定义为 jsonb、嵌套值走 Stringify:

CREATE TABLE orders (order_id text PRIMARY KEY, customer text, items jsonb);
COPY orders FROM '/tmp/orders.csv' WITH (FORMAT csv, HEADER true);
SELECT order_id, items->0->>'sku' AS first_sku FROM orders;

如果整条管道全程都是 JSON,干脆跳过 CSV,用 COPY ... FROM ... WITH (FORMAT text) 配合 JSON 单行输入。

真实负载实战

实战 1:电商订单(customer + items 数组)

一份典型订单同时包含嵌套的客户信息和长度不定的 items 数组:

[{ "id": "ord-001",
   "customer": { "name": "Alice", "address": {"city": "Seattle", "country": "US"} },
   "items": [{"sku": "SKU-100", "qty": 2}, {"sku": "SKU-205", "qty": 1}] }]

策略选哪种,看下游是谁。财务想要每个 SKU 一行核算收入,那就用行展开,输出两行,idcustomer.name 重复。运营想要每个订单一行做履约看板,那就点号 + 把 items Stringify 起来,免得数组把列数撑爆。同一份输入,两种输出,对各自的消费者都是对的。

直接动手试:把这段负载粘到我们的 JSON 转 CSV 在线工具 里,在 Nested 选项上来回切 Flatten 和 Stringify。「Nested E-commerce Orders」示例就是同一种形状。

实战 2:GitHub Issues API(labels 数组 + user 对象)

/repos/{owner}/{repo}/issues 接口返回的是一种混合嵌套形状:

[{ "id": 1001, "title": "Bug: login 404", "state": "open",
   "labels": ["bug", "priority:high"], "user": {"login": "alice"} }]

user 是只关心一个字段的对象;labels 是长度不定的字符串数组。务实的扁平方案是混合的:user 用点号(你只在意 user.login),labels 内联拼接成单个 cell,元素用 ; 隔开:

id,title,state,labels,user.login
1001,Bug: login 404,open,bug;priority:high,alice

「把数组拼成一个 cell」和「把对象用点号扁平」很难在同一种策略里同时完成。我们的转换器会自动把对象扁平化;labels 那一步要么用 jq 预处理(map(.labels = (.labels | join(";")))),要么接受默认的数组 Stringify 行为。

实战 3:MongoDB mongoexport($oid + metadata)

mongoexport --jsonArray 会输出带 Extended JSON 包装的内容:

[{ "_id": {"$oid": "6634a1b2c3d4e5f600000001"},
   "email": "alice@example.com",
   "metadata": { "signupDate": "2026-01-15T10:30:00Z",
                 "preferences": {"newsletter": true, "theme": "dark"} } }]

$oid 包装会产生一个字面名为 _id.$oid 的列,多数 SQL 引擎都不接受。先用 jq 拆掉包装:

mongoexport --collection=users --jsonArray | jq 'map(._id = ._id."$oid")' > users.json

对深嵌套的 metadata.preferences 块,按消费者来选。分析师导出:整段都用点号扁平化,metadata.preferences.theme 读起来很顺。管道往返:把 metadata Stringify 起来,保住结构。完整的 jq 与 CSV 管道搭配模式可以看 jq 速查手册

实战 4:Kubernetes Pod Spec(深度嵌套)

kubectl get pod -o json 的响应是扁平策略的最差情况。结构动不动就 6 层深(spec.template.spec.containers.0.resources.limits.memory)。无脑点号扁平会产生 70+ 字符的列名、超过 200 列的输出。两种策略可行。

kubectl jsonpath 预投影。只挑你真正需要的字段:

kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\t"}{.status.phase}{"\n"}{end}' > pods.tsv

把 spec Stringify,metadata 扁平化metadata(name、namespace、labels)保持扁平,把 spec 塞进单个 cell:

kubectl get pods -o json | jq 'map({name: .metadata.name, namespace: .metadata.namespace, spec: (.spec | tostring)})'

然后丢进转换器开 Flatten 模式。spec 列变成一个 JSON cell;metadata 列保持可读。要避开的反模式是直接 kubectl get pod -o json | json-to-csv flatten 而不做任何预投影,列数会失控。

往返安全:扁平化是有损的,Stringify 才是无损的

这里有一条竞品教程通常会跳过的定理。**点号、下划线、索引数组、行展开都是单向投影。**用其中任何一种扁平化之后,仅凭 CSV 就无法精确还原原始 JSON。

反例很容易构造。列名 customer.address.city{"customer": {"address": {"city": "..."}}}{"customer": {"address.city": "..."}} 之间是有歧义的。索引数组看起来可逆,但 CSV 无法判断 items.0.sku 反推出来到底是数组还是带数字键的对象。行展开则要求一个 group-by 键;没有 order_id,就无法判断哪些行原本属于同一个父记录。

只有 Stringify 经得起完整往返。嵌套值以 JSON 字符串原样保留,反向转换器读出单元格、解析、重新插入,原始结构完整无缺。用 Stringify 转换、存成 CSV、粘到我们的 CSV 转 JSON 工具、打开 Infer types,输出的字节会和输入完全一致。

实操准则:管道里要往返,就 Stringify;一次性分析或报表,按消费者从点号、下划线、行展开里选。

在我们的浏览器工具里实操

JSON 转 CSV 在线工具 把 5 种策略里的两种直接做成了选项:Flatten(同时实现点号和索引数组)和 Stringify(在 cell 里保留结构)。其余三种(下划线、行展开、面向 SQL 的 preset)只需要在前面多做一步预处理。

一次典型的会话只要五步:

  1. 用我们的 JSON 格式化工具 校验输入,避免语法错误悄悄变成转换失败。
  2. 把 JSON 粘到 JSON 转 CSV 在线工具,转换会即时完成。
  3. 把 Nested 设为 Flatten 得到点号 + 索引化 key,或者切到 Stringify 让数组和对象保留在单个 cell 里。
  4. 选一个 preset:管道用 RFC 4180、欧洲版电子表格用 Excel、数仓用 TSV、文本里逗号很多的用 Pipe。
  5. 点 Swap direction,用 CSV 转 JSON 工具 打开 Infer types 验证 Stringify 是否能完整往返。

所有处理都在你的浏览器里完成。PII、内部导出、生产环境密钥永远不会离开页面;页面加载完之后零网络请求。涉敏数据不允许上传到第三方网站时,这里是安全的选择。

常见陷阱

有六种失败模式反复出现。

  1. 列名爆炸。 Kubernetes spec 和 GitHub PR 评审线程会产生成百上千条叶子路径。解决:用 jqkubectl jsonpath 预投影,或者把笨重子树 Stringify 起来、只扁平化 metadata。
  2. 数组长度不一致。 第 1 行有 3 个 item,第 2 行有 5 个。索引数组会让第 1 行的 items.3.skuitems.4.sku 留空。解决:改用行展开。
  3. 反向时索引键被当作字符串。 CSV 转 JSON 看到 items.0.sku 时,0 严格说是字符串 key。某些反向转换器会重建出 {"0": {"sku": "A"}} 而不是 [{"sku": "A"}]。解决:要往返就用 Stringify。
  4. key 里本身已经含分隔符。 GA4 事件里有形如 event_params.key 的 key,里面就有字面点号;用 . 扁平化会产生歧义路径。解决:换下划线,或者把这些 key 改名。我们的 从 JSON5 到 JSONC 介绍了对 key 支持更宽松的 JSON 扩展格式。
  5. 类型在不同层级混用。 有些行的 address 是对象,有些是 null。扁平化后原本为 null 的位置会留下空白 cell。我们转换器的 schema notes 会给出警告,方便你提醒下游消费者。
  6. 大整数被 Excel 截断。 $oid 里的 Long、Twitter 雪花 ID、K8s 的 resourceVersion 都会超过 JavaScript 的安全整数范围(2^53 - 1),被悄悄四舍五入;Excel 又会把它们渲染成 9.00719925474E+15。解决:源 JSON 里就把 ID 存成字符串,打开 BOM,并用 Excel preset。

FAQ

把嵌套 JSON 扁平化为 CSV 的最佳方式是什么?

把嵌套 JSON 扁平化为 CSV 的最佳方式取决于下游消费者。Excel 或 Google Sheets 用点号;Pandas 或 BigQuery 要做聚合时用行展开;CSV 需要无损往返回 JSON 时用 Stringify。匹配下一个读者的需求。

怎么把 JSON 数组转成多行 CSV?

用行展开策略:父字段按数组元素数量复制一份,每个元素自己占一行。Pandas 里一行 pd.json_normalize(data, record_path='items', meta=['order_id']) 就能搞定;SQL 里 UNNEST(items) 是同一种形状。父键会在每个被展开的行里重复。

CSV 能完整还原回原始的嵌套 JSON 吗?

只有用 Stringify 模式才能完整往返。点号、下划线、索引数组、行展开都是有损的单向投影,反向转换器无法精确重建出原始的树。Stringify 把数组和对象作为 JSON 保留在单个 cell 里,配合 Infer types 时整个往返可以做到字节一致。

为什么 Excel 把我扁平化后的 JSON 显示成一长列?

Excel 把扁平化 JSON 显示成一长列,多半是因为你在欧洲 locale(德国、法国、意大利、西班牙),那边的 , 是小数点保留符,Excel 默认期望分号作分隔符。我们 JSON 转 CSV 在线工具 的 Excel preset 一键切换到 ; + CRLF + UTF-8 BOM。

列名该用点号还是下划线?

目标是 Excel、Google Sheets、Pandas 时用点号,点号是 json_normalize 的默认值,读起来也自然。目标是 SQL 时用下划线:Postgres、BigQuery、Snowflake 对含 . 的标识符要加引号,而下划线在任何引擎里都不需要加引号。

Pandas 的 json_normalize 如何处理对象数组?

Pandas json_normalize 处理对象数组靠 record_pathmeta 这两个参数。pd.json_normalize(data, record_path='items', meta=['order_id']) 会把 items 展开为每个元素一行,order_id 重复。对没有数组的嵌套对象,更简单的 pd.json_normalize(data, sep='_') 就能产出形如 customer_address_city 的下划线列名。处理很深的树时用 max_level= 限制深度。

扁平化深嵌套 JSON 的列数上限是多少?

扁平化深嵌套 JSON 的列数上限在 Excel 里是 16,384,CSV 本身实质上没有上限,但超过 500 列后输出就难以维护了。Kubernetes Pod spec 或 GraphQL 响应很容易就突破这个数。用 JSON 转 CSV 在线工具 把笨重子树 Stringify 起来,或者用 jqkubectl jsonpath 先投影。

jq 适合在 CSV 转换前先扁平化 JSON 吗?

是的,jq 就是 CSV 转换前扁平化 JSON 的合适工具。它能一行搞定预投影(map({id, name}))、预展开(.[] | {id, item: .items[]})以及形状规范化。jq 管道在 CSV 步骤之前运行,能精确控制哪些字段会到达转换器。配套模式可看 jq 速查手册

总结

五条要点:

  1. JSON 转 CSV 是几何问题,不是语法问题。一棵树要进网格,就必须先选定枝条如何坍缩。
  2. 5 种策略覆盖了实操里的全部宇宙(点号、下划线、索引数组、行展开、Stringify)。按消费者反查。
  3. Stringify 是唯一无损的路径。管道往返就用它。
  4. Excel-EU 和 BigQuery 的 preset 不是随便做的。直接用。
  5. 真实负载(mongoexport、Kubernetes spec、GitHub 响应)通常要先用 jqkubectl jsonpath 做一次预投影。

把你自己的负载丢进 JSON 转 CSV 在线工具 试试。本地运行,5 种策略都能处理,Stringify 保证无损往返。无需上传,无需注册,数据永不出页。