做数据爬虫或对接第三方API的同学,几乎每天都在和深层嵌套的JSON数据打交道。比如一条商品数据,可能长这样:data['result']['items'][0]['sku_info']['price'],一层套一层,任何一环缺失都会让程序直接崩溃。更麻烦的是,有些接口在不同情况下返回的结构还不完全一致,字段可能缺失、列表可能为空、某一层可能干脆是null。如何在Python中高效且安全地访问这些嵌套结构里的特定键值对,是一个非常实际的问题。

为什么直接用方括号层层取值不可取
最直观的写法就是一路方括号取下去:data['a']['b'][0]['c']。这种写法在结构完全确定、数据来源可控的场景下没有问题,速度也快。但一旦数据来自外部,比如网络接口、用户上传的JSON文件、爬虫抓取的页面,风险就来了。
假设我们要从一份API响应中取出用户昵称:
data = {
"code": 0,
"data": {
"user_list": [
{"id": 1, "profile": {"nickname": "张三"}}
]
}
}
# 直接取值,任何一层缺失都会抛异常
nickname = data["data"]["user_list"][0]["profile"]["nickname"]
上面这段代码在结构完整时能正常工作,但如果user_list是空列表,会抛出IndexError;如果profile键不存在,会抛出KeyError;如果接口出错时返回的结构变了,还可能抛出TypeError,比如对None使用下标操作。为了让程序不崩,你可能会写出这样的防御代码:
if "data" in data and data["data"] is not None:
user_list = data["data"].get("user_list", [])
if user_list:
profile = user_list[0].get("profile")
if profile:
nickname = profile.get("nickname", "未知用户")
四层判断只为了取一个字段,代码臃肿且难以维护。当需要取的字段有十几个时,这种写法会让函数体迅速膨胀。所以我们需要更优雅的方案。
使用get方法和默认值构建安全访问链
字典自带的get方法是第一个要掌握的工具。它接受两个参数:键名和默认值,键不存在时返回默认值而不是抛异常。结合默认值设计,可以构建一条安全的访问链。
# 每一层都返回字典,最后再取目标键
nickname = (
data.get("data", {})
.get("user_list", [{}])[0]
.get("profile", {})
.get("nickname", "未知用户")
)
这个写法的核心思路是:字典层的默认值给空字典{},列表层用user_list[0]之前先确保列表非空,可以写成data.get("user_list", [{}])或者先取列表再判断长度。这样即使中间某层缺失,后续的get也能在空字典上正常执行,最终返回默认值。
需要注意的是,如果某一层的值可能是None而不是缺失,比如"profile": None,那么None.get()依然会报AttributeError。解决办法是给get传一个明确的空字典默认值,或者封装一个辅助函数:
def safe_get(d, *keys, default=None):
"""逐层安全取值,任何一层失败返回default"""
cur = d
for k in keys:
if isinstance(cur, dict):
cur = cur.get(k, default)
elif isinstance(cur, list) and isinstance(k, int) and -len(cur) <= k < len(cur):
cur = cur[k]
else:
return default
return cur
nickname = safe_get(data, "data", "user_list", 0, "profile", "nickname", default="未知用户")
这个自定义函数把访问路径抽象成一串参数,字典和列表统一处理,代码一下子就清爽了。自己造轮子的好处是完全可控、零依赖,缺点是需要自己维护和测试。下面看看现成的第三方库能提供什么。
借助第三方库:jsonpath_ng与glom的实战对比
当嵌套结构非常深、或者需要批量提取多个字段时,使用专门的查询库效率更高。先看jsonpath_ng,它实现了JSONPath查询语法,适合在一次解析中按路径表达式取值:
from jsonpath_ng import parse
expr = parse("data.user_list[*].profile.nickname")
matches = expr.find(data)
nicknames = [m.value for m in matches]
print(nicknames) # ['张三']
JSONPath的优势是表达式即路径,支持通配符*和递归下降..,比如parse("$..nickname")可以找出整棵树里所有的nickname,不用关心它藏在哪一层。对于结构不固定、只需按字段名全量搜索的场景特别好用。缺点是性能一般,每次find都要遍历匹配,不适合超大体积数据的高频调用。
再看glom,它的定位是结构化数据访问与重构,指定路径和默认值都很简洁:
from glom import glom, PathAccessError
# 用元组表示路径,整数代表列表下标
nickname = glom(data, ("data", "user_list", 0, "profile", "nickname"), default="未知用户")
# 还能一次性提取多个字段
spec = {"nick": ("data", "user_list", 0, "profile", "nickname"),
"uid": ("data", "user_list", 0, "id")}
result = glom(data, spec)
glom的杀手锏是多字段批量提取,一个spec字典就能把散落在各处的字段汇聚成一个新结构,这在做数据清洗时非常省事。此外pydash也值得一试,它的get函数支持点号路径,如pydash.get(data, "data.user_list.0.profile.nickname", default="未知用户"),写法接近JavaScript的lodash风格,上手成本几乎为零。
性能与场景选择建议
不同方案的性能差异明显。简单说:原生方括号 > get链 > 自定义safe_get > glom > jsonpath_ng。原生访问走的是字典哈希查找,速度最快;jsonpath每次find都要解析和遍历,开销最大。在一个百万次循环的取值测试中,原生访问和JSONPath之间可能有十倍以上的差距。
选择建议如下:如果结构完全可信,比如程序内部构造的配置对象,直接用方括号,没有性能负担;如果结构来自外部但只需要取一两个字段,用get链或自定义的safe_get即可,零依赖且够快;如果一次要提取几十个字段、或者结构不固定需要模糊搜索,上glom或jsonpath_ng;而做配置管理时,pydash的点号路径写法最直观。
最后提醒一点,无论用哪种方案,取到值之后的类型校验同样重要。嵌套结构里"price"可能是数字也可能是字符串形式的数字,取值安全了不代表用值安全。养成取值后统一转换和校验的习惯,配合上面这些访问技巧,处理再深的嵌套数据也能做到游刃有余。
Python嵌套字典嵌套列表键值对访问修改时间:2026-09-11 08:44:42