如何用Python Pandas将多层嵌套JSON数据扁平化?

来源:站长站作者:泰国程序员头衔:程序员
导读:本期聚焦于泰国程序员创作的《如何用Python Pandas将多层嵌套JSON数据扁平化?》,敬请观看详情。多层嵌套JSON在接口响应、日志采集和数据同步场景中非常常见。直接用pandas.read_json读取后,字段值往往仍然是字典或列表,无法进行分组、筛选和统计。Pandas自带的json_normalize虽然能展开一层,但遇到数组与字典交错、字段缺失、列表长度不一致时容易报错或产生笛卡尔积,很多分析流程因此卡住。本文从json_normalize的record_path、meta、errors、sep等参数机制讲起,结合explode方法演示如何将订单、商品明细这类两层以上结构稳定转换成规整DataFrame。之后给出一个可复用的递归扁平化函数,能够处理任意嵌套层级以及列表和字典混合结构。文章包含真实代码示例、字段缺失的异常排查和性能取舍建议,适合需要把半结构化JSON整理成表格做特征工程或报表的中文技术用户。

接口返回的数据经常出现多层嵌套结构,例如用户信息里包含订单列表,订单列表里又有商品明细。如果直接把这种JSON用pandas.read_json读进来,会得到列中套字典或列表的DataFrame,无法直接做聚合、筛选和可视化。要发挥Pandas的向量化能力,必须先做扁平化处理。

如何用Python Pandas将多层嵌套JSON数据扁平化?

Pandas提供了一个专门函数json_normalize,在pandas.json_normalize模块中,0.25版本之后也可以直接通过pd.json_normalize调用。它能将半结构化JSON递归展开成平面表,但很多人只用默认参数展开一层,遇到下一层数组时就不知道如何处理。接下来从参数机制讲起。

一、理解json_normalize的展开机制

json_normalize的核心任务是把带嵌套字典的数据拆成列名带前缀的平面表。它接受一个字典列表或字典作为输入,默认会递归展开所有字典字段,并用sep参数指定的分隔符连接父键和子键,默认是英文句点。例如一个对象里包含profile.city,默认展开后列名就是profile.city。

问题出在列表字段。默认情况下,json_normalize不会自动进入列表内部展开,而是把整个列表作为一个单元格保留。要展开列表,需要使用record_path参数指定列表所在的路径。比如数据是[{"user_id":1, "orders":[...]}],要展开orders,可以设置record_path=["orders"]。同时还要通过meta参数把上层的用户信息携带到每一行,否则展开后只剩下订单字段,用户ID会丢失。

下面这段代码展示了默认展开和指定record_path的差异:

import pandas as pd

data = [
    {
        "user_id": 1,
        "profile": {"city": "Beijing", "level": 3},
        "orders": [
            {"order_id": 1001, "amount": 99.5, "items": [{"sku": "A1", "price": 20.5}]},
            {"order_id": 1002, "amount": 120.0, "items": [{"sku": "A2", "price": 60.0}, {"sku": "A3", "price": 60.0}]}
        ]
    }
]

# 默认展开,orders列仍然是列表
df = pd.json_normalize(data)
print(df)

# 指定record_path展开订单列表
df_orders = pd.json_normalize(
    data,
    record_path=["orders"],
    meta=["user_id", ["profile", "city"], ["profile", "level"]],
    record_prefix="order_",
    errors="ignore"
)
print(df_orders)

运行第一个打印会看到profile.city和profile.level两列,而orders列仍然是列表对象。第二个打印则会把每个订单拆成单独一行,并保留user_id和城市信息。这里meta中的嵌套路径写法["profile", "city"]表示从顶层开始逐级定位到城市字段,比直接写"profile.city"更不容易受分隔符干扰。

record_prefix和meta_prefix用于给展开后的列名加前缀,避免订单字段和用户字段重名。errors参数建议在数据字段不稳定时设置为"ignore",这样某个列表元素缺少某个字段时不会直接报错,而是返回NaN。不过要注意,errors="ignore"只作用于meta路径中的缺失字段,如果record_path本身不存在,依然会抛异常。

二、处理列表嵌套列表的实战展开

真正麻烦的是两层甚至三层列表嵌套。比如用户有多个订单,每个订单里还有一个商品明细列表items。如果直接设置record_path=["orders"],得到的是订单级DataFrame,但items列仍然是列表,里面每个元素都是字典。此时不能继续用同一个调用去指定items,因为record_path描述的路径无法穿过已经被拆开的订单列表。

一种稳定做法是分步处理:先用json_normalize展开订单列表,得到订单表;然后对items列执行explode,让列表中的每个商品字典成为一行;最后再对这些字典调用一次json_normalize,拆出商品字段,并把结果拼回原表。这里的关键是explode只负责把列表拉长,不会解析字典内容,所以第二次json_normalize必不可少。

# 第一步:展开orders,保留订单元信息
orders_df = pd.json_normalize(
    data,
    record_path=["orders"],
    meta=["user_id", ["profile", "city"]],
    record_prefix="order_"
)

# 第二步:explode items列,使每个商品字典独立成行
orders_exploded = orders_df.explode("items").dropna(subset=["items"]).reset_index(drop=True)

# 第三步:对items字典列再做json_normalize
items_df = pd.json_normalize(orders_exploded.pop("items")).add_prefix("item_")
flat_df = pd.concat([orders_exploded, items_df], axis=1)
print(flat_df)

这段代码中,explode之后需要调用reset_index(drop=True),否则索引会重复,后续拼接可能出现对齐错误。对于items列中可能存在的None值,先用dropna(subset=["items"])过滤掉,避免第二次json_normalize处理空值时报错。如果商品明细缺失但你又想保留订单行,可以在过滤前把None替换成空字典,配合errors="ignore"生成全NaN的商品列。

这种分步方案适合列表层级固定、字段结构比较清晰的场景。当层级超过两层时,可以继续重复“explode再normalize”的过程,但代码会逐渐变得冗长。下一节会给出一个递归函数,把任意深度的嵌套结构一次性拍平。

三、编写递归扁平化函数处理任意层级

递归扁平化的思路是遍历对象,遇到字典就继续进入子键,遇到列表就按索引进入每个元素,遇到标量就把它作为最终列的值。这样处理之后,列名中会带上列表索引,例如orders.0.items.1.price。对于单条记录或少量记录,这种宽表非常直观,配合pd.DataFrame.from_records可以快速生成DataFrame。

from collections.abc import MutableMapping

def flatten_json(y, parent_key="", sep="."):
    items = {}
    if isinstance(y, MutableMapping):
        for k, v in y.items():
            new_key = f"{parent_key}{sep}{k}" if parent_key else k
            items.update(flatten_json(v, new_key, sep=sep))
    elif isinstance(y, list):
        for i, item in enumerate(y):
            new_key = f"{parent_key}{sep}{i}"
            items.update(flatten_json(item, new_key, sep=sep))
    else:
        items[parent_key] = y
    return items

nested = {
    "user_id": 1,
    "profile": {"city": "Beijing", "level": 3},
    "orders": [
        {"order_id": 1001, "items": [{"sku": "A1", "price": 20.5}]}
    ]
}

flat_row = flatten_json(nested)
print(pd.DataFrame([flat_row]).T)

这个函数用collections.abc.MutableMapping判断字典类型,比直接判断dict更稳妥,因为它能覆盖OrderedDict等映射类型。列表使用enumerate生成索引,父键和当前键之间用sep连接,默认也是句点。最终返回一个扁平的字典。对多条记录,可以用列表推导式逐条调用flatten_json,再传给pd.DataFrame.from_records。由于from_records会取所有记录字段的并集作为列,某些记录缺少字段时会自动填NaN,很适合参差不齐的日志数据。

递归函数虽然直观,但也有明显缺点:列表长度不一致时列会大量膨胀,比如一次接口返回100条记录但其中一条订单特别多,就可能多出几十列。另外,它把列表索引固化到列名中,不利于后续按订单维度继续分析。因此通常的做法是:如果目标是做用户级特征,可以用递归宽表;如果目标是订单级或商品级明细,还是使用json_normalize配合explode更合适。

四、常见异常与性能取舍

在处理真实业务JSON时,最常见的问题是字段缺失和类型不一致。某个用户的orders可能是列表,另一个用户可能是None,直接调用json_normalize就会报TypeError或AttributeError。稳妥的做法是在读取后先做一次轻量清洗:把None列表替换成空列表,把字符串类型的JSON字段用json.loads解析成对象,再进入扁平化流程。字段名中包含点号时,建议把sep改成下划线,否则json_normalize可能把它误判成路径分隔符。

另一个容易被忽视的问题是笛卡尔积。如果同一层存在两个列表字段,比如每个用户有多个订单和多个优惠券,直接把两列各自explode,会使行数从N变成N乘以M,优惠券与订单错误组合。这种情况应该分别展开成两张表,再通过主键关联,或者先聚合其中一个列表再展开另一个。性能方面,json_normalize在展开外层结构时走的是pandas内部实现,速度通常优于纯Python递归;但当列表嵌套很深或字段极多时,递归函数会频繁创建字典,数据量超过十万条就会明显变慢。此时建议先抽样验证逻辑,再把稳定部分固化成json_normalize调用,减少Python层循环。

最后,扁平化之后的字段经常是字符串类型,例如时间、金额都可能以字符串形式存在。可以继续用pd.to_datetime和pd.to_numeric做类型转换,并在转换前检查异常值。整个流程可以封装成一个读取、清洗、扁平化、转换四步的预处理函数,方便在不同数据源之间复用。

Python Pandas嵌套JSON数据扁平化修改时间:2026-09-20 20:13:22

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0920/59778.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。