ARTICLE DETAIL

资讯详情

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

JSON与JSONPath实战:从入门到高效提取嵌套数据

JSON与JSONPath实战:从入门到高效提取嵌套数据 前阵子接了个老项目的维护第三方接口返回的JSON嵌套了四层里面数组套对象、对象套数组前端同事写了一大坨遍历逻辑提取数据每次接口结构调整都要跟着改代码改到后来他自己都晕了。我过去看了一眼跟他说这东西就别傻傻地手动解析了一条JSONPath表达式就能把数据提出来结构变了最多改一行匹配规则。他听完实验了一把回头跟我感叹这玩意儿怎么没早点知道。如果你也在写接口对接、写爬虫、做测试断言、处理配置文件或者每天跟各种API返回的数据打交道那“JSON JSONPath”这对组合绝对是值得花半小时吃透的基础功。JSON负责数据的组织与传输JSONPath负责从深层JSON里精准捞数据两者搭配使用能省掉大量手写遍历的重复代码。这篇文章我会从JSON最基本的格式规范讲起再重点拆解JSONPath的语法和实战用法最后把高频报错的排查思路一起整理出来前后端、测试脚本、数据处理这些场景都能直接用上。1. 先把基本功打牢JSON 格式与数据模型1.1 JSON到底是个什么东西JSONJavaScript Object Notation虽然名字里带着JavaScript但它早就不属于JavaScript了而是一种跨语言的数据交换格式。后端服务给前端返回数据、两个系统之间做接口通信、软件存配置文件、数据库存半结构化字段到处都能看到JSON的身影。它之所以这么普及靠的是三个优势对人可读、对机器易解析、几乎所有编程语言都内置了支持。拿咱们最常用的场景来类比XML像是写论文时用的严格格式标签开头结尾必须配对信息完整但啰嗦YAML像是写便签简洁好看但对缩进极其敏感一个空格错了就解析失败而JSON更像是填表结构清晰字段名和值一一对应不搞花活。所以现在绝大多数Web API都默认返回JSON新工具链也基本把JSON当作配置格式的首选。1.2 语法规则与六种数据类型JSON的数据模型并不复杂它只有六种值类型对象object、数组array、字符串string、数字number、布尔值true/false、空值null。其中对象和数组是复合结构可以任意嵌套字符串、数字、布尔、null是叶子节点负责存具体数据。一个典型的订单数据结构长这样{ orderId: A10086, userId: 9527, amount: 199.5, paid: true, remark: null, items: [ { sku: SKU-001, name: 无线鼠标, price: 99.75, count: 2 } ] }这里有几个语法红线新手特别容易踩键名必须用双引号包裹单引号不行裸键名也不行。字符串必须用双引号普通文本内容里如果包含双引号需要转义为\。JSON不支持注释不要想着写//或者/* */进去。数组和对象的最后一个元素后面不能加逗号也就是不能有“尾逗号”。数字不能写成01这种前导零格式也不能出现NaN、Infinity。我见过很多线上问题都是从一个多余的逗号开始的尤其是当JSON文件由多个部分拼接生成时漏掉一个逗号或者多出一个逗号整个文件就解析失败。建议养成的习惯是只要是手工维护的JSON写完一定过一遍格式校验工具。1.3 从“一行”到“竖排”格式化与常用工具很多接口返回或者日志里的JSON是压缩成一行输出的机器读起来没问题人眼看起来就是灾难。“vs json文件全部在一行”这种需求本质上就是把压缩的JSON格式化也叫美化、pretty print成带缩进的可读形式。解决途径好几个按场景选就行VS Code里打开JSON文件用快捷键ShiftAltF直接格式化这个操作内置支持不用装插件。Notepad可以装JSON Viewer插件格式化之外还能在侧边栏以树形结构查看JSON层级。插件如果是离线环境就下载对应版本的NPPJSON插件放到plugins目录里手动加载即可。命令行环境用jq一句jq . data.json就能格式化输出还能顺便做筛选。Python环境可以用标准库python -m json.tool data.json简单直接。如果只是临时看一下在线工具也无妨但要注意别把含敏感信息的JSON贴到不信任的网站上公司项目更要谨慎。格式化这件事我会多说一句不要觉得这是小事格式化后的JSON配合行号、缩进、括号高亮排查问题时人脑处理效率能提升一大截。许多解析报错格式化之后一眼就能看出括号少了半边。2. JSONPath给 JSON 写的查询语言2.1 为什么要单独学一门查询语法JSON本身只负责“存数据”它不提供查询能力。平时要从一个嵌套很深的JSON里取数据最常见的做法是什么用代码一层层取先取对象属性再判断是不是数组再循环遍历……代码写起来啰嗦不说一旦数据结构层级变了所有取值逻辑都得跟着动。JSONPath就是来解决这个问题的。它相当于给JSON写的“SQL”用一条表达式描述“我要的数据在哪、满足什么条件”然后在JSON文档里匹配并提取出来。学到这个程度你会发现很多数据结构上的奇技淫巧都变得透明了。JSONPath最早由Stefan Goessner在2007年提出设计上参考了XPathXML的查询语言所以熟悉XPath的人看JSONPath会有亲切感。2024年它正式成为了RFC 9535标准也就是说现在市面上的JSONPath实现基本都有共同遵循的规范了。2.2 核心语法速查JSONPath的语法不算多核心是几个符号的组合。为了好记你可以把JSON的嵌套结构想象成一棵树对象是树干数组是树枝叶子节点就是具体数据。JSONPath表达式的任务就是告诉程序“从哪个树干走到哪个树枝甚至跨过树枝直达某片叶子”。先把常用操作符一次性列清楚操作符含义举例$根节点表达式的起点$.store表示根下的store.key或[key]取对象的指定属性$.store.book[n]从数组取第n个元素从0开始$.store.book[0][*]通配符匹配所有元素$.store.book[*]..递归下降跳过中间层级找所有匹配项$..price当前节点通常在过滤表达式里用$.store.book[?(.price 10)][?(条件)]过滤表达式筛选符合条件的元素$.store.book[?(.price 10)][start:end:step]数组切片类似Python的切片$.store.book[0:2][-n:]取数组最后n个元素$.store.book[-1:]这些语法组合起来表达能力很强。不过有一点要注意不同语言和不同第三方库对JSONPath的细节支持有差异比如过滤表达式有的库要求用[?()]有的库支持[?()]但不支持单引号有的库还额外支持|联合、~键名匹配等扩展操作。写之前瞄一眼你用的库的文档能省不少纠结。2.3 经典示例图书商店查询实战用一段经典的JSON示例数据来说明会清晰很多。假设我们有这样一个书店数据结构{ 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 }, { category: fiction, author: J. R. R. Tolkien, title: The Lord of the Rings, isbn: 0-395-19395-8, price: 22.99 } ], bicycle: { color: red, price: 19.95 } } }我们来逐个查询$.store.book[*].title取所有书的标题。结果是四条title的数组。$..price递归取所有price。你会拿到所有书和自行车的价格共5个数字。$.store.book[1]取第二本书下标从0开始所以1对应第二本。$.store.book[-1:]取最后一本书。$.store.book[?(.price 10)].title取价格小于10的书的标题。这里代表数组中的每一个元素.price就是当前这本书的价格条件筛选后取title。这些表达式基本覆盖了日常开发的90%场景取某个字段、取列表、按条件过滤、递归找值。掌握了这些JSON在代码里的“盲区”就基本不存在了。3. 实操在 Python 和 JavaScript 中跑通 JSONPath3.1 Python标准库 json jsonpath-ngPython标准库自带json模块负责序列化和反序列化。JSONPath解析则需要安装一个第三方库我用得比较多的是jsonpath-ng它严格实现了JSONPath语法还支持对匹配结果做修改。安装pip install jsonpath-ng然后可以这样查上面的书店数据import json from jsonpath_ng import parse data { 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, price: 8.99}, {category: fiction, author: J. R. R. Tolkien, title: The Lord of the Rings, price: 22.99} ], bicycle: {color: red, price: 19.95} } } # 解析JSONPath表达式 expr parse($.store.book[?(.price 10)].title) matches expr.find(data) for match in matches: print(match.value)执行这段代码会打印出两本书的标题Sayings of the Century和Moby Dick。expr.find(data)返回的是匹配对象列表每个match.value就是实际取到的值。如果匹配不到任何东西matches就是空列表不会抛异常这点在写脚本时很友好。如果只是从接口返回的JSON字符串开始就先用json.loads()把字符串转成Python对象再丢给jsonpath_ng查。顺序是文本 - 对象 - 查询。3.2 JavaScript / TypeScriptJSON.parse jsonpath-plus前端原生环境里有JSON.parse()可以直接把JSON字符串转成对象但原生JavaScript并没有内置JSONPath。需要使用第三方库我常用jsonpath-plus它支持浏览器和Node环境功能很全。安装npm install jsonpath-plus使用示例import { JSONPath } from jsonpath-plus; const data { store: { book: [ { title: Sayings of the Century, price: 8.95 }, { title: Sword of Honour, price: 12.99 }, { title: Moby Dick, price: 8.99 }, { title: The Lord of the Rings, price: 22.99 } ], bicycle: { color: red, price: 19.95 } } }; const result JSONPath({ path: $.store.book[?(.price 10)].title, json: data }); console.log(result); // [Sayings of the Century, Moby Dick]注意jsonpath-plus的JSONPath()函数接收对象参数path是JSONPath表达式json是要查询的数据对象。返回结果直接是一个数组即使只匹配到一个元素也会被包在数组里。这个特性和Python库不太一样写代码时需要注意区分。3.3 其他常见场景Qt 读写、jq 命令行、MySQL JSON 字段除了Python和前端JSON相关的需求在工作中出现的频率也很高。我简单盘几个高频场景的用法Qt/C读写JSON如果是用Qt做桌面客户端或嵌入式开发一般用QJsonDocument来解析#include QJsonDocument #include QJsonObject #include QJsonArray #include QFile QFile file(data.json); file.open(QIODevice::ReadOnly); QByteArray bytes file.readAll(); QJsonDocument doc QJsonDocument::fromJson(bytes); QJsonObject root doc.object(); QJsonArray books root.value(store).toObject().value(book).toArray(); for (int i 0; i books.size(); i) { QJsonObject book books[i].toObject(); qDebug() book.value(title).toString(); }Qt没有内置JSONPath但QJsonDocument提供的树形访问已经能覆盖大部分读写需求。真要复杂查询可以引入jsonpath-ng类似功能的C库或者先用Python等脚本把数据预处理成扁平结构再交给Qt。jq 命令行在Linux服务器上排查数据jq是神器。虽然jq的语法不是标准JSONPath但思想一样cat data.json | jq .store.book[] | select(.price 10) | .title这一句能做同样的事情而且输出还能继续做管道处理。平时在终端里想快速看接口返回某个字段curl xxx | jq .data.list比写脚本快太多。MySQL JSON 字段MySQL从5.7开始支持JSON类型可以把半结构化数据直接存进表里再用JSON函数查询-- 提取字段 SELECT profile-$.age FROM users; -- 按JSON字段查询 SELECT * FROM users WHERE JSON_EXTRACT(profile, $.age) 18; -- 给JSON字段创建索引 ALTER TABLE users ADD INDEX idx_age ((CAST(profile-$.age AS UNSIGNED)));-是提取JSON字段并转为SQL字符串的语法JSON_EXTRACT返回JSON类型。千万要注意直接对JSON列创建普通索引是无效的必须用CAST(... AS ...)这种生成列的方式否则查询还是会走全表扫描。4. 高频报错排查那些年我们踩过的 JSON 坑4.1 格式类错误Unexpected end of JSON input / JSONDecodeError先看三个高频格式报错它们出镜率极高。unexpected end of JSON input是Go语言里的典型报错意思是JSON字符串还没结束就没了。比如var raw []byte []byte({name:demo) var obj map[string]interface{} if err : json.Unmarshal(raw, obj); err ! nil { log.Fatal(err) }这段代码会直接报错因为大括号没闭合。类似的Python里会报JSONDecodeError: Expecting value或者Expecting property name enclosed in double quotes。JavaScript的JSON.parse()会报Unexpected end of JSON input或Unexpected token。这类错误九成发生在以下场景接口返回被截断了网络问题或者服务端超时只收到一半数据。日志系统自动截断字段存到文件里JSON不完整。手工拼JSON字符串时漏了后半个括号。多个JSON片段拼接时中间少了逗号或者括号没配对。排查方法很简单把报错的JSON文本拷贝出来用格式化工具过一遍看到哪一行缩进或者括号对不上问题就在哪。如果数据量特别大可以写个简单的Python脚本逐个字符扫描括号配对快速定位位置。4.2 Python 中 TypeError: Object of type set is not JSON serializable这是Python特有的坑。Python的数据类型比JSON丰富set、datetime、bytes、Path这些类型都不能直接被json.dumps()序列化。比如import json data {tags: {python, json, tips}} json.dumps(data)直接报错TypeError: Object of type set is not JSON serializable。原因很简单JSON标准里没有“集合”这个概念只有数组。Python的list对得上JSON的数组set对不上。解决办法import json data {tags: {python, json, tips}} data[tags] list(data[tags]) # 如果数据里有datetime用default参数统一处理 from datetime import datetime data[update_time] datetime.now() json.dumps(data, ensure_asciiFalse, indent2, defaultstr)defaultstr的意思是碰到不知道怎么序列化的对象就调用str()把它转成字符串这个技巧处理datetime、Decimal特别方便。不过要注意转成字符串之后下游解析时就需要自己再转了。4.3 Rust 的 missing field 报错与结构体映射再来看一个后端开发高频报错failed to deserialize the json body into the target type: input: missing field。这是Rust生态里serde库的经典报错意思是目标结构体里声明了一个必填字段但输入的JSON里没有这个字段。比如定义了一个订单结构体#[derive(serde::Deserialize)] struct Order { order_id: String, total_amount: f64, }请求体只传了{order_id: A10086}serde反序列化时就会报missing field total_amount。这类报错几乎都是接口联调时字段名对不上导致的。排查方向有这么几个请求JSON里的字段名拼写跟结构体字段是否完全一致。字段名大小写是否匹配很多JSON字段是下划线风格snake_caseRust结构体默认也是snake_case但如果接口那边用的是驼峰camelCase就需要用#[serde(rename_all camelCase)]来标注。某些字段允许缺失的话类型要设为OptionString并且在结构体上标注#[serde(default)]。还有一类常见情况是类型不匹配。比如接口返回count: 12字符串数字但结构体里声明的是i32serde也会反序列化失败。必要时用#[serde(deserialize_with ...)]写一个自定义反序列化函数把字符串数字转成数字。4.4 数组处理、中文编码与映射错误JSON数组的处理接口返回的最外层经常是数组比如[{name: Alice}, {name: Bob}]。Python里用json.loads()后直接得到list遍历即可。但很多人容易忽略的是数组里每一项都是独立对象如果取其中某个字段要先遍历还是直接索引取决于业务需求。用JSONPath的话$[*].name一下就把所有name取出来了比手写循环简洁得多。中文编码乱码json.dumps()默认会将非ASCII字符转义成\uXXXX形式比如“中文”变成\u4e2d\u6587。这其实没错JSON标准允许很多接口也会这么做。但如果想让文件里直接看到中文加一个参数json.dumps(data, ensure_asciiFalse, indent2)无法将JSON输入源映射到目标字段这类报错出现在低代码平台或配置型集成工具里原文类似“无法将 json 输入源 /body/total_amount 映射到目标字段转账总金额中”。这本质上还是字段映射问题源JSON里/body/total_amount这个路径找不到或者类型对不上。排查思路是先确认源数据里字段的真实路径和真实类型再看目标字段的映射配置是否写对了路径最后看有没有类型转换步骤。特别是金额字段源是字符串还是数字目标有没有做精度要求都要一一核对。4.5 格式对比与结构变更接口升级后经常出现JSON结构变了、老的解析代码全部失效的情况。我建议养成一个习惯每次接口变更后把新旧JSON各存一份用在线工具或Beyond Compare做结构对比看看哪些字段删了、哪些字段改了类型。相比人眼盯着一大段JSON看对比工具能高亮差异效率翻倍。JSON数据对比对于测试人员来说尤其重要。接口自动化测试里断言某个字段是否符合预期用JSONPath提取目标值再做断言比做全量JSON相等断言稳定得多——因为全量断言只要新增一个字段就失败而JSONPath只关心你要验证的那几个点。5. 进阶玩法JSON Schema、大模型结构化输出与 JSON RPC5.1 用 JSON Schema 给数据上“枷锁”前面讲的所有内容都默认JSON数据是对的但现实是别人给你的接口返回经常不按文档来。这时候就需要在消费数据之前加一道校验关卡JSON Schema就是干这个的。JSON Schema本身也是一个JSON文件用来描述数据结构和校验规则。看一个用户信息的Schema{ $schema: https://json-schema.org/draft/2020-12/schema, type: object, required: [username, email, age], properties: { username: { type: string, minLength: 3, maxLength: 20 }, email: { type: string, format: email }, age: { type: integer, minimum: 0, maximum: 150 } } }用这个Schema去校验实际数据username太短、email格式不对、age缺失或超范围都能报出来。对于“deepseek json schema报错”这类问题其实多半就是模型生成的数据没满足你给定的Schema约束解法是把Schema写得更具体、在Prompt里明确要求必须严格按Schema输出、输出后自动校验不通过就重新生成。JSON Schema的好处是把“数据长什么样才算合法”从业务代码里抽离出来变成一份可配置、可版本化的声明文件。接口联调时双方约定好Schema等于提前把数据契约定死了比口头文档靠谱得多。5.2 大模型应用里的 JSON 输出约束现在大模型应用非常多很多LLM接口允许你指定返回格式让模型输出JSON方便程序直接消费。比如让模型从一段文本中抽取结构化信息你可以在Prompt里明确要求“只返回JSON不要包含任何其他解释文字”并附上一段JSON Schema作为参考。不过实测下来模型的JSON输出不能百分百保证合法。常见问题有模型在JSON外面包了json代码块标记。字段名不是双引号。文本内容里出现未转义的控制字符。生成了多余的逗号或者缺少右括号。我的处理策略是三层兜底第一层在Prompt里写清楚约束条件并给一个负例提醒“不要添加注释、不要使用单引号”。第二层拿到返回文本先做一次JSON.parse()或json.loads()解析成功就直接用。第三层解析失败时用正则或者简单修复规则清理掉代码块标记、多余逗号再尝试二次解析二次还不行就重试请求让模型重新生成。如果使用大模型做数据分析大模型json这类需求会越来越多掌握这套“约束输出 解析 修复 重试”的链路是很有必要的。5.3 JSON RPC一种很轻的接口协议除了RESTful APIJSON还支持一种非常轻量的远程调用协议——JSON RPC。它的核心思想是把方法名和参数用JSON封装起来通过HTTP或其他传输层发出去对方用JSON返回结果。一个标准的JSON RPC 2.0请求{ jsonrpc: 2.0, method: subtract, params: [42, 23], id: 1 }对应的响应{ jsonrpc: 2.0, result: 19, id: 1 }id字段用于关联请求和响应params可以是数组也可以是对象result和error二者必填其一。相比RESTJSON RPC少了HTTP动词、状态码、资源路径这些概念接口定义更加集中特别适合内部系统之间的轻量调用、浏览器插件与本地服务通信、以及一些去中心化应用场景。6. 实战案例省市区数据查询与远程配置场景6.1 用 JSONPath 从省市区数据中提取目标地区“省市区三级联动”数据是前端经常要用到的公开JSON数据集结构通常是省套市、市套区嵌套三层。这种数据拿来做JSONPath练习特别合适。简化示例[ { name: 广东省, children: [ { name: 广州市, children: [ { name: 天河区 }, { name: 越秀区 } ] }, { name: 深圳市, children: [ { name: 南山区 }, { name: 福田区 } ] } ] } ]想一次性取出“广东省”下属所有城市用JSONPath$[?(.name 广东省)].children[*].name解析流程是先看根节点是一个数组用[?(...)]过滤出name等于“广东省”的对象再取它的children最后取每个子节点的name字段。想递归找出所有区一级的地名不管它属于哪个市$..children[?(.children null)].name这里利用递归下降..跳过中间层级先找到所有children数组再用过滤条件筛出没有下级子节点的对象即叶子节点然后取name。这个模式在处理任意多层级树形数据时非常通用像组织架构、分类树、评论楼中楼都能用。6.2 远程配置地址与 JSON 资源的分发模式现在很多工具类应用都流行“远程配置”的模式应用启动时从一个URL拉取JSON文件里面保存了最新的资源列表、功能开关、界面配置等本地不硬编码这些信息方便运营随时更新。举一个比较通用的例子某个应用的配置文件可能是这样的结构{ version: 2026.01.15, update_url: https://example.com/latest.json, groups: [ { name: 默认分组, entries: [ { name: 条目A, url: https://example.com/a }, { name: 条目B, url: https://example.com/b } ] } ] }应用启动时用HTTP库拉取这个JSON再用JSONPath快速提取自己关心的部分比如取第一个分组下的所有条目$.groups[0].entries[*]或者直接根据version字段判断要不要重新加载。这种“JSON地址同步配置”的模式本身不复杂核心就是把数据源和代码逻辑解耦。开发时还要注意一个问题远程JSON的schema可能会变化所以要给解析增加容错至少要判断关键字段是否存在别因为配置缺失直接把应用搞崩溃。6.3 从命令行到脚本JSONPath 的调试速度优势最后分享一个我自己工作中的调试套路。拿到一个陌生接口的返回后我不会急着写代码而是在命令行里先用curl把返回存到本地文件然后用jq或写一个临时Python脚本快速探索JSON结构。几个高频操作jq keys data.json看根节点有哪些字段。jq .data | type data.json判断某个字段的类型。jq .data.list[] | {name, id} data.json只输出需要的字段。等确认了数据结构和需要的字段再把这个JSONPath/jq表达式直接搬进正式代码里用语言对应的JSONPath库实现。整个流程从“看到接口”到“跑通代码”基本在几分钟内完成比边写代码边用print调试JSON要舒服得多。在实际项目中我发现JSONPath最容易被低估的场景是测试断言。接口自动化测试里与其解析整个响应体再层层取字段不如直接用JSONPath表达式定位目标值断言表达式的结果等于期望值。一旦接口结构微调只需要更新表达式不需要重写解析逻辑维护成本低很多。就我个人经验来说真正写好JSONPath表达式的一个关键是“先在脑子里把JSON结构画成树再决定用点号路径还是递归下降”。点号路径适合层级明确的静态结构递归下降适合那些“我也不知道它在第几层、反正叫这个名字”的字段。这两者配合过滤器基本能应对所有数据结构。最后再提醒一句JSON规范化这件事越早做越好不管是写脚本还是写接口养成先格式化再动手的习惯能帮你省下不少排查时间。
返回列表