Python的jsonpath库使用方法实例

📅 发布时间:2026/10/10 13:39:08
Python的jsonpath库使用方法实例
前言处理嵌套很深的 JSON 时一层层写data[a][b][0][c]会非常脆少一层就KeyError多一层就TypeError中间任何一个键缺失都会让整行代码炸掉。JSONPath 就是为了解决这个问题而生的——它是一门查询语言用一条字符串表达式描述我要取值的位置把路径描述和取值的防御逻辑分开。一个重要的时间点JSONPath 在 2024 年 2 月有了正式标准 RFC 9535标题是 JSONPath: Query Expressions for JSON。在此之前各家实现各写各的语法细节互相不一致这正是照着网上例子写却跑不通的根源。关于 Python 里的实现jsonpath-ng、jsonpath等第三方库都能用但本文不写它们的具体 import 路径、类名与方法名——本机没有安装这些库官方文档站点也无法从本环境访问写出来无法核实。因此本文的路线是先讲 RFC 9535 定义的通用语法再用标准库实现一个最小可用的子集。各第三方库的具体 API 与扩展语法请以其官方文档为准。一、JSONPath 的基本词法按 RFC 9535一条 JSONPath 查询以根标识符$开头后面跟若干个段segment每段说明往下走一步。段有两类写法点记法$.store.book用.连接名字。括号记法$[store][book]用方括号加引号。带特殊字符的键名含空格、含点必须用这种写法。选择器selector有以下几种名字选择器、通配符*、索引选择器可为负-1表示最后一个、切片选择器[start:end:step]、过滤选择器[?过滤条件]。其中代表当前节点过滤条件就写在上。后代段..是最有特色的一个它表示递归下降匹配任意深度的后代。RFC 里给了几条简写规则..name等价于..[name]..*等价于..[*]。表达式含义$.store.book[*].author商店里所有书的作者$..author文档中所有位置的 author任意深度$.store.*商店下的所有成员值$.store..price商店下任意深度的 price$..book[2]第三本书下标从 0 开始$..book[-1]最后一本书$..book[0,1]前两本书联合$..book[:2]前两本书切片写法$..book[?.isbn]所有带 isbn 的书$..book[?.price10]所有价格低于 10 的书$..*所有成员值与数组元素有两个反直觉的细节要注意$..book[2].publisher在示例文档里返回空结果因为第三本书根本没有 publisher 这个字段——查询一条不存在的路径在 JSONPath 里通常返回空列表而不是报错。这既是它比索引链安全的地方也是它可能静默出错的地方写错了路径名它不会提醒你。二、为什么各家实现会不一致RFC 9535 之前JSONPath 只有原始提案没有标准。于是各库在几个点上分化过滤表达式的写法。有的用[?(...)]包一层括号有的用[?...]。脚本表达式。老实现允许在过滤器里写近乎任意的脚本这是巨大的安全风险相当于允许查询字符串执行代码。RFC 明确设计了无副作用、可静态分析的表达式就是为了堵住这个口子。扩展函数。length()、sum()这类聚合函数属于非标准扩展各库的支持与命名完全不同。结果格式。有的库返回值的列表有的返回带路径信息的匹配对象有的返回迭代器。所以换库就等于换语法。迁移时要逐条核对表达式尤其是带过滤器的那几条。选库时要留一个安全心眼如果 JSONPath 表达式来自不可信输入用户自定义查询、配置下发一定要用只支持标准语法的实现不要用支持任意脚本求值的老实现。一条精心构造的表达式有可能变成代码执行入口。三、标准库实现一个最小子集既然第三方库的接口无法核实不如自己用标准库写一个只支持核心语法的求值器。它支持$、点记法与括号记法、通配符*、索引含负数、切片、以及..后代段——覆盖了绝大多数实际用法。# 适用于 Python 3.8import reSEG_RE re.compile(r\.\.\* # ..* 后代通配| \.\.(?Pdesc_name[A-Za-z_][\w-]*) # ..name 后代名字| \.\.(?Pdesc_bracket\[[^\]]*\]) # ..[ ... ] 后代括号段| \.(?Pname[A-Za-z_][\w-]*) # .name| \.(?Pstar\*) # .*| (?Pbracket\[[^\]]*\]) # [ ... ], re.X)def _children(node):节点的直接子值字典取 value列表取元素。if isinstance(node, dict):return list(node.values())if isinstance(node, list):return list(node)return []def _descendants(node):产出节点自身及其所有后代深度优先。yield nodefor child in _children(node):yield from _descendants(child)def _apply_bracket(node, expr):处理方括号里的内容通配符、带引号的名字、切片、索引。expr expr.strip()if expr *:return _children(node)if expr.startswith() and expr.endswith():key expr[1:-1]return [node[key]] if isinstance(node, dict) and key in node else []if : in expr:start, _, rest expr.partition(:)end, _, step rest.partition(:)if not isinstance(node, list):return []s int(start) if start else Nonee int(end) if end else Nonest int(step) if step else Nonereturn list(node[slice(s, e, st)])index int(expr)if isinstance(node, list) and -len(node) index len(node):return [node[index]]return []def jsonpath(data, expr):求值返回所有匹配到的值组成的列表。if not expr.startswith($):raise ValueError(表达式必须以 $ 开头)current [data]pos 1while pos len(expr):match SEG_RE.match(expr, pos)if not match:raise ValueError(f无法解析的片段: {expr[pos:]!r})pos match.end()nxt []if match.group(0) ..*:for node in current:for d in _descendants(node):nxt.extend(_children(d)) # 所有后代节点本身elif match.group(desc_name):name match.group(desc_name)for node in current:for d in _descendants(node):if isinstance(d, dict) and name in d:nxt.append(d[name])elif match.group(desc_bracket):inner match.group(desc_bracket)[1:-1]for node in current:for d in _descendants(node):nxt.extend(_apply_bracket(d, inner))elif match.group(name):name match.group(name)for node in current:if isinstance(node, dict) and name in node:nxt.append(node[name])elif match.group(star):for node in current:nxt.extend(_children(node))else:inner match.group(bracket)[1:-1]for node in current:nxt.extend(_apply_bracket(node, inner))current nxtreturn currentif __name__ __main__:doc {store: {book: [{title: A, price: 8.95},{title: B, price: 12.99},{title: C},]}}print(jsonpath(doc, $.store.book[0].title)) # [A]print(jsonpath(doc, $.store.book[-1].title)) # [C]print(jsonpath(doc, $.store.book[:2].price)) # [8.95, 12.99]print(jsonpath(doc, $..price)) # [8.95, 12.99]print(jsonpath(doc, $..[title])) # [A, B, C]这段代码有几个设计取舍值得说明匹配不到就返回空列表。_apply_bracket与名字选择器都在键不存在时安静地跳过而不是抛KeyError。这正是 JSONPath 相比索引链的价值——路径写错时不会崩但也不会告诉你写错了。..name用生成器做递归。_descendants是递归生成器用yield from深度优先遍历整棵树。每次调用都新建一个生成器不存在被耗尽的问题。..*的语义是对每个后代取其子值。所以它覆盖面是除根节点以外的所有节点这正是 RFC 里那句所有成员值与数组元素的意思。切片复用 Python 的切片语义。node[slice(s, e, st)]直接借用列表切片所以负数与省略端点的行为和 Python 一致不必自己实现。没实现的语法要明确说出来。过滤表达式[?...]、联合[0,1]、带引号键名内部的转义这个实现都不支持。写文档时要老实标注避免别人误用。四、什么时候该用 JSONPath什么时候不该适合用结构嵌套深、只关心其中少数几个字段同一份结构要反复用不同路径取数路径本身要作为配置项下发。不适合用只需要取顶层一个键直接data[key]更清楚需要严格的类型校验JSONPath 只负责定位不负责校验表达式来自不可信输入又用了支持脚本的老实现安全风险。还有一个使用习惯建议把 JSONPath 表达式集中管理。写在散落的几十处代码里一旦上游 JSON 结构变了你会不知道要改哪些地方。常见坑点把某一家库的语法当成 JSONPath 标准。❌ 从网上抄来[?(...)]的过滤器写法换到另一个库就不认。 ✅ 先区分RFC 9535 标准语法与某库的扩展语法迁移时逐条核对带过滤器的表达式。路径写错却以为是数据问题。❌ 表达式里键名拼错拿到空列表后去怀疑数据没抓到。 ✅ JSONPath 匹配不到时通常返回空结果而不报错拿到空列表要先核对键名与层级。用索引链代替 JSONPath 后又抱怨它脆。❌ 继续写data[a][b][0][c]一旦中间缺失就KeyError。 ✅ 嵌套取值用 JSONPath 或加防御性的.get()链让缺失变成可处理的结果。在不可信输入上使用支持脚本求值的实现。❌ 让外部传入的查询字符串进入能执行任意表达式的老式实现。 ✅ 选择只实现标准语法的实现标准明确把过滤表达式设计成无副作用、可静态分析正是为此。忽略..的代价。❌ 在大文档上频繁使用$..name递归下降。 ✅ 后代段要遍历整棵子树路径越深代价越大能用确定路径就不要用递归下降。混用点记法与括号记法处理特殊键名。❌ 键名里带点或空格时仍写$.a.b被当成两级路径。 ✅ 特殊键名用括号记法$[a.b]。把 JSONPath 当成校验工具。❌ 用路径能取到值就认为数据结构完全正确。 ✅ 定位与校验是两件事取到值之后还要自己检查类型、范围与必需字段。总结维度结论标准RFC 9535 于 2024 年 2 月发布之前各实现互不兼容核心语法$根、.名字、[n]索引、[*]通配、[a:b]切片、[?filter]过滤、..后代未命中行为通常返回空结果而非报错扩展语法length()等聚合、脚本求值属非标准各库不同安全不可信输入不要用支持脚本求值的实现第三方库 API本环境无法核实请以各库官方文档为准用 JSONPath 的正确心态是它让取数这件事变得可声明、可配置、可容错但代价是错误会变得安静。所以配套的做法是——表达式集中管理、对取到的值单独做校验、以及在文档里明确标注自己用的是哪一版语法。