ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSONPath入门与实战:从JSON数据提取到接口测试应用

JSONPath入门与实战:从JSON数据提取到接口测试应用 从事后端开发这些年我处理过最多的数据格式就是JSON。刚开始那会儿每次要从一团嵌套七八层的接口返回里取某个字段我都老老实实写for循环一层层判断取完还得自己处理字段不存在的情况代码又臭又长。后来接手一个老项目接口文档缺失只能对着线上返回的JSON一点一点捋数据那感觉就像在没有路标的原始森林里硬找一条路。直到有次看同事用了一段$.store.book[?(.price 10)].title就把我写三十多行代码才搞定的活干完了我才意识到原来JSON早就有一门专门用来定位数据的查询语言它就是JSONPath。这篇内容就是给完全没接触过JSONPath的朋友准备的。我会从最基础的手动取数痛点讲起把JSONPath的核心语法、过滤条件、实战写法、踩坑经验一次说清楚最后再聊聊它在命令行、接口测试工具里的用法。学完之后你再看JSON数据基本就像开了导航一样想取哪里就取哪里。1. 为什么非要用JSONPath先看看徒手取数有多痛1.1 当JSON越嵌越深代码开始变得难看先来看一个特别常见的场景。假设你对接了一个电商订单接口返回结构大概是这样的{ code: 0, message: success, data: { order: { orderNo: ORD20240115001, buyer: { name: 张三, phone: 13800138000, address: { province: 广东省, city: 深圳市, detail: 南山区某街道某号 } }, items: [ {skuId: A1001, name: 无线鼠标, price: 79.9, count: 2}, {skuId: B2002, name: 机械键盘, price: 399, count: 1} ] } } }如果现在业务方告诉你需要提取订单里买家手机号的后四位用于营销统计。大多数人的第一反应是直接写代码一层层取值data response.json() try: phone data[data][order][buyer][phone] except KeyError: phone last_four phone[-4:] if phone else 这段代码只有三层嵌套看起来还好。那你再想想如果中间夹着数组、字段会动态变化、结构偶尔缺失你敢用这种链式[xxx][yyy]的写法吗一旦哪一层结构变了抛一个KeyError你还得逐层打断点查是哪个字段没了。这种代码写多了人的脾气都会变差。1.2 JSONPath是什么给JSON数据修的一条“导航路线”JSONPath本质上是一门面向JSON数据的查询语言。你给它一个JSON文档和一条路径表达式它就能顺着表达式把命中的数据捞出来。它的设计灵感来自XPath——XPath是专门查XML的JSONPath借鉴了那套思路又结合了JSON自身的特点对象、数组、嵌套做了一套更适合JSON的定位语法。打个比方JSON数据是一整座陌生的城市你需要去某个地方。手动取数相当于拿着纸质地图一个路口一个路口地认路走错了就得折返JSONPath则像是你直接打开导航软件输入目的地路线自动规划你只需要跟着走就行。表达式的每一段就像导航指令“往东走两个路口右转进入第三栋楼坐电梯到5层左拐第二间办公室”——只不过JSONPath的指令更简洁比如$.data.order.buyer.phone。需要注意的是JSONPath不是某个大厂出的官方标准它最早的实现来自Stefan Goessner在2007年写的一篇文章。因为不是标准所以不同语言、不同库在细节上会有些差异比如过滤器的写法、函数支持程度等。但核心语法基本一致学会了通用的换到任何语言里都能快速上手。1.3 什么时候值得上JSONPathJSONPath适合所有需要“从JSON里取数据”的人但有些场景用了它收益尤其大接口联调和Mock数据从几百行的接口返回里快速提取某个字段比肉眼搜索高效太多。自动化测试断言测试登录接口后取token、取订单号、取列表数量一条JSONPath比写一堆解析逻辑简洁得多。接口测试工具配置像JMeter、Postman这类工具都内置了JSONPath提取器配置一下就能把响应数据提取成变量。数据清洗和ETL从复杂JSON日志、第三方API响应里抽取需要的字段再灌入数据库或做进一步处理。配置文件解析很多应用配置是JSON格式读取配置时用JSONPath定位比逐层遍历更灵活。当然如果是自己项目内部定义好的、结构非常稳定的JSON直接用对象映射或者链式取值也完全没问题。JSONPath的精髓在于处理“结构复杂、层级多变、字段不确定”的JSON——这时候它的价值是决定性的。2. JSONPath核心语法先会走路再学跑步2.1 一套JSON就是一张地图从$开始认识路径JSONPath的路径表达式有一个固定的起点就是$符号它代表JSON文档的根节点。不管JSON最外层是对象还是数组$永远指向最顶层的那个“东西”。从根节点出发往下走的路主要有两种.key表示访问对象的某个属性比如$.data就是取根节点下的data字段。[index]表示访问数组中的某个元素索引从0开始。比如$.items[0]是取items数组的第一个元素。这两个符号组合起来就能表达一条完整的访问路径。$.data.order.buyer.phone的大致含义是“从根节点出发找data字段进入后找order再进入找buyer最后取buyer里的phone值”。JSONPath里还有两种不常用但偶尔能救命的写法[key]用中括号加引号的方式访问对象属性效果等同于.key。在属性名包含空格、点号、特殊字符时这种写法更稳。[0, 1]或[0:2]可以一次取多个数组元素。[0, 1]表示取索引0和1的元素[0:2]表示取索引0到1左闭右开。2.2 四种最常用的路径写法我整理了一个表格把零基础阶段最常用的几种路径写法列出来每个都带着实际JSON的对应关系写法含义示例返回结果$根节点$整个JSON文档$.store.book取store下的book字段$.store.book书数组$.store.book[0]取book数组第一个元素$.store.book[0]第一本书对象$.store.book[0].title取第一本书的title$.store.book[0].title第一本书的书名$[store][book][0][title]用中括号形式访问同上同上这里用的JSON还是最经典的那个图书商店示例很多JSONPath教程都用它结构清晰{ store: { book: [ {category: reference, author: Nigel Rees, title: Sayings of the Century, price: 8.95}, {category: fiction, author: Evelyn Waugh, title: Sword of Honour, price: 12.99}, {category: fiction, author: Herman Melville, title: Moby Dick, isbn: 0-553-21311-3, price: 8.99} ], bicycle: {color: red, price: 19.95} } }你只要把这几种基础写法练熟了日常大部分取数需求就已经能覆盖。2.3 用一段真实JSON走一遍完整路径光看语法不练等于白学。拿上面那个商店JSON来演练一下。比如你想取第一本书的标题也就是Sayings of the Century。我们按路径拆解从根出发用$。根下面有个store字段访问它用.store目前路径是$.store。store下面有book和bicycle两个字段进入book数组路径是$.store.book。book是数组第一本书用下标[0]路径是$.store.book[0]。第一本书对象里有category、author、title、price取title就加.title最终完整路径是$.store.book[0].title。跑一下这个表达式后面会讲具体怎么用代码跑得到的结果就是Sayings of the Century。整个过程就像用导航一步步走到目的地中间每一段都有明确的语义不会再出现“这个字段到底嵌在第几层”的困惑。3. 过滤器与通配符从“找得到”到“找得准”3.1 通配符*和递归下降..基础路径只能按固定结构取数据但JSON的世界里最不缺的就是“结构不确定”的尴尬。比如你想取所有书的作者总不能写$.store.book[0].author、$.store.book[1].author、$.store.book[2].author写三遍吧数组长度是可变的你根本不知道它有几本书。这时候就需要通配符和递归下降。通配符*的作用是“匹配任意一个节点”。最常见的用法有两种$.store.book[*].author遍历book数组的每一个元素取各自的author。返回结果是一个包含所有作者名字的数组。$.store.*取store对象下所有字段的值也就是整个book数组和bicycle对象。递归下降..则是JSONPath里最强大的一个符号。它表示“不再一层层往下走而是从当前位置出发往下扫描所有层级找到所有匹配目标”。比如$..price的意思是“在整个JSON文档里不管price出现在哪一层、在哪个对象里全部找出来”。在这个商店示例里它会返回[8.95, 12.99, 8.99, 19.95]——三本书的价格和自行车价格都在里面。递归下降加上通配符几乎可以解决“结构不确定”的取数问题。比如$..book[*].title不管book嵌在哪个位置不管数组里有多少本书直接把所有标题一次性取出来。不过这里要提醒一句递归下降好用但性能开销不小因为它要遍历整个子树。大数据量场景别滥用后面专门讲性能的时候会细说。3.2 过滤器表达式()的用法路径能解决“按结构取数”的问题但很多时候我们不是按结构而是按条件取数。比如找出所有价格低于10元的书。找出所有类别是fiction的书。找出所有还没下架的商品的库存。这类需求对应SQL里的WHERE条件JSONPath里的过滤器表达式就是干这个的。语法是[?()]在方括号里写一个以?开头的条件表达式满足条件的数组元素会被选中。最基本的写法长这样$.store.book[?(.price 10)].title拆开来看$.store.book找到books数组。[?(.price 10)]在数组里过滤代表“当前正在遍历的那个数组元素”.price就是当前这本书的价格条件就是价格小于10。.title对过滤后留下的书取它们的title字段。在这个示例里价格低于10元的书有两本《Sayings of the Century》8.95和《Moby Dick》8.99所以结果就是这两个书名。过滤器里常用的运算符还有这些比较、!、、、、逻辑与、||或、!非正则~部分实现支持比如Jayway JsonPath支持存在性.isbn判断字段是否存在一个复杂一点的例子找出所有category为fiction并且价格不低于9元的书的标题。表达式可以写成$.store.book[?(.category fiction .price 9)].title结果只有《Sword of Honour》12.99。这种多条件组合的过滤能力在接口测试里特别常用——比如筛选金额超过阈值的订单、挑选状态为成功的记录等。3.3 一个复杂的组合过滤案例把通配符、递归下降、过滤器组合起来能表达非常复杂的取数逻辑。我见过一个比较典型的场景日志系统里存储了一批用户操作记录每条记录长这样{ userId: u10001, action: login, detail: { device: {type: mobile, os: Android}, ip: 10.0.0.8 } }现在要统计所有使用Android手机登录的用户ID一条JSONPath就能搞定$..[?(.action login .detail.device.type mobile .detail.device.os Android)].userId这条表达式的意思是在整个文档里递归查找过滤出action是login、detail.device.type是mobile、detail.device.os是Android的记录最后取这些记录的userId。这种表达能力如果纯粹靠手写代码通常要写一个递归函数加三段条件判断。JSONPath一条表达式就解决了。4. 实战演练用Python跑通一段完整的JSONPath提取4.1 Python生态里怎么选JSONPath库Python的JSONPath库有好几个最主流的两个是jsonpath和jsonpath-ng。jsonpath是老牌库接口简单用起来方便但功能比较基础对过滤器表达式的支持不够全面。jsonpath-ng是它的升级版支持更完整的语法包括过滤器、函数扩展API设计也更现代返回的是匹配对象而非纯值能拿到匹配路径、匹配值等更多信息。我的建议是新项目直接用jsonpath-ng。它是目前Python生态里JSONPath功能最全、维护最活跃的库遇到问题也更容易搜到解决方案。Python里还有其他方案比如jmespath但它的语法和标准JSONPath不太一样算是另一套查询语言。如果你认准了JSONPath语法就选jsonpath-ng。4.2 安装、解析、提取一条龙安装很直接pip install jsonpath-ng然后看一个最基础的用法。把上面那个商店JSON存成字符串写代码提取所有书的价格import json from jsonpath_ng import parse data json.loads( { store: { book: [ {category: reference, author: Nigel Rees, title: Sayings of the Century, price: 8.95}, {category: fiction, author: Evelyn Waugh, title: Sword of Honour, price: 12.99}, {category: fiction, author: Herman Melville, title: Moby Dick, isbn: 0-553-21311-3, price: 8.99} ], bicycle: {color: red, price: 19.95} } } ) expr parse($.store.book[*].price) matches expr.find(data) for match in matches: print(match.value)运行结果会打印三行8.95、12.99、8.99。关键点在于parse负责把JSONPath表达式解析成一个表达式对象find负责在数据里执行查找。返回的matches是一个由DatumInContext对象组成的列表每个对象都有.value匹配到的值、.path匹配到的路径、.context上下文等属性。如果你只需要用户名那一类单个值可以直接从matches里取第一个expr parse($.store.book[0].title) matches expr.find(data) if matches: title matches[0].value print(title) # 输出: Sayings of the Century4.3 一个模拟接口数据的完整示例光跑静态JSON不够过瘾咱们模拟一个更真实的场景从HTTP接口拿数据再提取关键字段。假设有个接口返回了用户列表import requests import json from jsonpath_ng import parse # 模拟一个接口返回 response requests.get(https://example.com/api/users) data response.json() # 提取所有用户的邮箱 expr parse($.data.users[*].email) emails [match.value for match in expr.find(data)] # 提取第一个用户的用户名 expr2 parse($.data.users[0].username) matches expr2.find(data) username matches[0].value if matches else None print(emails) print(username)这里注意几点实际接口返回可能包了很多层比如data、result、response之类的包装字段JSONPath表达式要把这些包装层也写进去。如果接口返回的不是JSON而是纯文本先用response.json()转成字典。取不到数据时matches是空列表代码里要做判空处理否则matches[0]直接报IndexError。4.4 提取不到数据时怎么排查JSONPath提取不到数据是新手最常见的问题没有之一。表达式看起来没问题但结果就是空的。按照下面的顺序排查基本能定位到九成问题确认JSON本身没问题。先print(json.dumps(data, ensure_asciiFalse, indent2))把整个JSON打出来肉眼确认字段名和层级结构到底长什么样。临时拿掉路径的后半段一段一段验证。比如$.data.users[*].email不返回结果先试试$.data.users能不能取到数组再试$.data能不能取到对象一层层缩小范围。检查字段名拼写是否完全一致。JSON里是userName还是usernameEmail还是email一个字母大小写不对结果就天差地别。确认这个路径在JSON里是真的存在。有时候字段就在那里但你用了$.users而实际路径是$.data.userList。我自己排查的时候有一招特别管用把表达式拆成最小可运行的片段一步步加长就像调试SQL一样很快就能找到是哪个环节断了。5. JSONPath实战中的坑与排查经验5.1 不同实现的语法兼容性差异前面说过JSONPath没有官方标准所以不同库在细节上是有差异的。最典型的例子就是过滤器里的比较。在Jayway JsonPathJava版里字符串要用双引号$.store.book[?(.category fiction)]在Python的jsonpath-ng里字符串既可以用双引号也可以用单引号但有些老版本的库对单引号支持不好会直接解析失败。我的经验是跨库写完表达式后至少在当前用的库里跑一遍再说别想当然觉得所有地方语法一样。再比如..递归下降很多库支持但有的库会把$..book里的book当成一个过滤条件而不是路径片段行为完全不同。遇到诡异结果时优先怀疑库的实现差异。另外有些实现支持函数比如length()、min()、max()但语法和写法各不一样别指望一套函数语法到处通用。5.2 转义、空格和特殊字符的坑JSON的键名有时候真的让人抓狂比如带空格的键名user name、带点号的键名user.name、带中划线的键名user-name。直接用.user name写路径几乎肯定会出问题。我的处理方式带空格的键名用中括号加引号$[user name]。带点号的键名也用中括号$[user.name]。如果不加JSONPath会把.name当成下一层属性去解析结果必然为空。键名里有特殊字符比如引号本身需要根据具体库的转义规则处理这个躲不开只能查文档。过滤器表达式里的符号也要特别注意。代表当前元素但有的库里的某个版本如果你写.price它可能要求你写成[price]才识别特别是键名带特殊字符时。写表达式前不妨先试一下最简单的[?(.price 10)]如果报错再换[?([price] 10)]。还有一个容易忽略的点表达式里的空格。某些库在解析过滤器时对空格很敏感?(. price 10)这种写法看着没事但解析器可能不认识. price点号后面多了空格。写成?(.price 10)统一紧凑一些兼容性最好。5.3 性能问题递归下降.. 别滥用..这个符号太好用了容易让人上瘾但它是性能杀手。它的工作方式是递归遍历当前节点下的所有子节点数据量大时一次$..price会把整个JSON树全扫一遍。举个例子如果有一个包含10万条日志的数组每条日志20个字段你写$..userId那查询引擎要从根开始遍历所有20万个节点逐个判断键名是不是userId。如果写成$.logs[*].userId只遍历数组里10万个元素的userId属性效率完全不是一个量级。我的建议是能用显式路径.a.b.c表达的绝不用递归下降。数据量超过几千条优先用精确路径。实在需要..的场景比如完全不知道字段嵌在第几层先评估数据量再决定如果性能吃紧考虑在数据生产端就把结构固定好或者对JSON做一次预处理把需要频繁查询的字段提前拉平。5.4 我的排错三步法表达式不返回结果时我有一套固定的排查流程分享给大家第一步验证数据。先把JSON打印出来看结构别凭记忆盲写路径。这一步能排除一半以上的低级错误。第二步拆解路径。把长路径从头到尾部逐段拆开从$开始每加一段就find一次看结果是否为空。比如$.a.b.c先试$.a再试$.a.b最后试$.a.b.c。哪个环节开始返回空问题就锁定在那一层。第三步怀疑语法。路径本身没问题但结果仍然不对就到对应库的文档里查具体语法重点看过滤器、转义、递归下降这几个部分的实现差异。有条件的直接在官方在线解析器比如Jayway JsonPath的在线Demo上把表达式跑一遍看是否报语法错误。这三步走完99%的问题都能找到根因。剩下的1%基本就是库本身的bug换个版本或者换个实现就能解决。6. 从入门到落地命令行、JMeter与其他生态工具6.1 命令行工具jq与jsonpath的对比处理JSON数据很多人第一反应是用jq。jq确实是命令行里处理JSON的神器但它的语法和JSONPath是两套体系。jq里取字段用的是.store.book[0].title这种风格乍看很像JSONPath但它没有$根节点这一说过滤器的写法也不一样。如果你已经熟悉了JSONPath想在命令行里继续用同一套语法可以看看专门的JSONPath命令行工具比如jsonpath-clinpm包或者Python的jsonpath-ng直接配合Python一行命令。以Python为例echo {store: {book: [{title: A, price: 8.95}]}} | python -c import sys, json from jsonpath_ng import parse data json.load(sys.stdin) matches parse($.store.book[*].title).find(data) for m in matches: print(m.value) 这样在命令行里也能用JSONPath表达式取数据适合快速验证一个小表达式是否写对了。$ jq和JSONPath的选择我的建议是写Shell脚本、处理文件数据用jq因为它更成熟、功能更全面在接口测试、代码里嵌入查询逻辑用JSONPath两个不冲突看场景选就行。6.2 JMeter的JSON Extractor怎么配置接口测试领域JMeter是绕不开的工具。它自带JSON Extractor核心功能就是让你用JSONPath表达式从HTTP响应里提取数据存成变量供后续请求使用。最经典的场景是登录后提取token在HTTP请求登录接口上右键添加 → 后置处理器 → JSON Extractor。配置界面里Variable Names填tokenJSON Path Expressions填$.data.token具体路径看你的登录响应结构。Match Numbers填1表示取第一个匹配结果。Default Values填NOT_FOUND表示提取失败时变量取这个默认值。后续请求里用${token}就能引用到提取出来的token值。这个功能背后就是JSONPath在干活。我在实际压测脚本里经常用它提取订单号、商品ID、分页信息等动态数据配合循环控制器可以模拟真实的用户操作链路。需要注意一点JMeter的JSON Extractor对过滤表达式的支持相对有限。像$..[?(.type A)]这种复杂的过滤器建议先在本地用Python的jsonpath-ng验证一遍语法再贴到JMeter里避免在测试执行时才发现表达式不生效。6.3 其他语言里的JSONPathJSONPath并不是Python专属几乎所有主流语言都有对应的实现Java最常用的是Jayway JsonPathGitHub上star数非常高功能和性能都很成熟。它的语法支持完整包括过滤器、正则、函数等。JavaScript有jsonpath-plus和Stefan Goessner原版的jsonpath浏览器和Node.js环境下都能用。Go有oliveagle/jsonpath、PaesslerAG/jsonpath等实现配合gjson使用体验更好。C#有Newtonsoft.Json内置的SelectToken方法支持JSONPath语法。这些库的核心语法和我上面讲的基本一致学会了通用语法换个语言只是装包、调API的区别。从我个人经验来看$..book[?(.price10)].title这种表达式在Java、Python、JavaScript里写出来的结果几乎是一模一样的。最后再分享一个我在团队里经常强调的习惯凡是写复杂的JSONPath表达式一定要在代码里加注释把表达式的含义用自然语言写清楚。否则三个月后你自己回来看大概率也不知道$..[?(.status success .amount 100)]到底在筛什么。JSONPath本身已经很简洁了但简洁不等于自解释注释这东西写的时候多花十秒钟排查问题的时候能省一个小时。
返回列表