pandas.json_normalize 这个函数,说起来挺有意思的——很多人以为它能一键把嵌套 JSON 拍扁成表格,结果往往发现列名变成了一串带点的“火车”(比如 profile.address.city),真正棘手的问题却一个都没解决。字段缺失怎么办?列表嵌套怎么炸开?多层结构怎么拆?答案其实都藏在参数组合里。

record_path:列表嵌套的“开关”
想象一下,JSON 里有个 "orders": [{"id": 1, "item": "book"}, {"id": 2, "item": "pen"}],你期望它变成两行数据对吧?但默认情况下,json_normalize 直接把它整个塞进一列里,根本不会自动展开。这时候就得靠 record_path 来告诉它:“把这个列表里的每个元素拆成单独一行”。
- 如果源数据是一个字典,订单藏在
data['user']['orders'],那就传record_path=['user', 'orders'] - 如果源数据是字典列表,每个元素都有
'items'字段,就传record_path='items'或者record_path=['items'] - 不提供
record_path时,函数默认把整个data当作记录列表——这只能处理最外层就是[{}, {}]的简单情况
meta:别让父级字段“走丢了
一旦用了 record_path 去提取子记录(比如订单),父级数据里的用户 ID、时间戳这些信息默认不会自动跟着过来。怎么办?用 meta 参数把它们“提上来”。
- 要保留
user_id和created_at,直接传meta=['user_id', 'created_at'] - 如果字段藏在嵌套路径里,比如
profile.name,可以用字符串形式meta='profile.name'或者列表形式meta=[['profile', 'name']] - 某些记录里
profile.name可能缺失,这时候必须加errors='ignore',否则你会看到一个KeyError meta_prefix也是个好帮手:设为'user_',那提上来的user_id就变成user_user_id列,避免和子记录里的字段重名
sep 和 max_level:控制“拍”的深度
默认分隔符 sep='.' 会把 {"a": {"b": {"c": 1}}} 变成列名 a.b.c,看着挺直观?但麻烦在于,万一 API 返回里真的有一个叫 "a.b" 的 key,那就乱套了。更稳妥的做法是换分隔符,比如 sep='_' 得到 a_b_c,导出到数据库或 BI 工具时兼容性更好。
max_level 则用来控制展开的深度。只想拿到 profile 这一级,不想管 profile.address.city?设 max_level=1 就够了。超出层级的嵌套会被保留为字典或字符串类型——既不会报错也不会丢数据,这个细节很容易被忽略。
errors='ignore' 不是万能药,预处理才是王道
很多人以为加上 errors='ignore' 就能处理所有缺失情况,其实它只对 meta 里声明的路径生效,对 record_path 本身毫无作用。
- 如果
record_path=['results', 'data'],但某条记录里根本没有'results'这个 key,照样会抛KeyError - 如果
record_path指向一个None值(比如"orders": null),函数直接罢工,不会跳过那条记录 - 安全做法是预处理:用类似
pd.json_normalize([x for x in data if x.get('results') and x['results'].get('data')])的过滤逻辑,把脏数据提前筛掉,比硬扛错误可控得多
嵌套 JSON 的“扁平化”从来不是一次调用就能搞定的。最常被偷懒跳过的一步是:先 print(json.dumps(data[0], indent=2)) 看清第一项的结构,再动手配 record_path 和 meta——而不是对着报错信息反复试参数。这个习惯养成了,后面能省下不少时间。