Python 数据处理提速秘籍:Jsonpath 在复杂 JSON 提取中的高效实践

221次阅读
没有评论

共计 5540 个字符,预计需要花费 14 分钟才能阅读完成。

上周帮同事调试接口时,发现很多人还在用 for 循环一层层处理那些复杂嵌套的 JSON 数据。好家伙,代码又长又容易出错,排查起来更是血压飙升。其实,用 jsonpath 库能省一半时间,代码不仅更精炼,逻辑也清晰得多。今天,我就带大家实操一遍,看看我是怎么用它高效提取数据的,告别那些繁琐的循环地狱。

第一步:初识 Jsonpath —— 告别“大海捞针”式的遍历

很多人拿到 JSON 数据,第一反应就是用 for 循环、if 判断,一层层往下挖。当 JSON 结构简单时还勉强能应付,但一旦遇到多层嵌套、数组混杂的情况,代码很快就变得难以维护。Jsonpath 提供了一种类似 XPath 的方式来定位和提取 JSON 中的数据,让你直接“指哪打哪”。

我平时倾向使用 jsonpath_ng 这个库,它比传统的 jsonpath 库更活跃,语法支持也更全面,尤其是处理一些复杂表达式时,体验会好很多。

安装 jsonpath_ng

pip install jsonpath_ng

基础用法演示:提取简单路径

假设我们有这样一份用户数据:

{
    "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
            }
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    },
    "expensive": 10
}

现在我想提取所有书的作者和标题,如果用传统方法,你可能需要循环 store 下的 book 数组,再提取每个对象的 authortitle。用 jsonpath_ng 就会简单很多。

from jsonpath_ng import jsonpath, parse
import json

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
            }
        ],
        "bicycle": {
            "color": "red",
            "price": 19.95
        }
    },
    "expensive": 10
}

# 提取 store 下所有书的作者
jsonpath_expr_author = parse('$.store.book[*].author') # 这里 [*] 表示匹配数组中的所有元素
authors = [match.value for match in jsonpath_expr_author.find(data)]
print(f"所有作者: {authors}")
# 输出: 所有作者: ['Nigel Rees', 'Evelyn Waugh']

# 提取第一本书的标题
jsonpath_expr_title = parse('$.store.book[0].title') # [0] 表示数组的第一个元素
first_book_title = [match.value for match in jsonpath_expr_title.find(data)]
print(f"第一本书的标题: {first_book_title[0] if first_book_title else' 未找到 '}")
# 输出: 第一本书的标题: Sayings of the Century

# 提取自行车的颜色
jsonpath_expr_color = parse('$.store.bicycle.color')
bicycle_color = [match.value for match in jsonpath_expr_color.find(data)]
print(f"自行车的颜色: {bicycle_color[0] if bicycle_color else' 未找到 '}")
# 输出: 自行车的颜色: red

# 小提醒:Jsonpath 表达式跟 XPath 有点像,初学者容易搞混。# 记住,它是针对 JSON 结构的,`.` 用于对象属性,`[]` 用于数组索引或条件筛选。# 如果路径不存在,`find()` 方法会返回一个空列表,而不是报错,这是个好习惯,避免了额外的 try-except。

第二步:进阶用法——数组、过滤与通配符

jsonpath_ng 的强大之处在于它能处理更复杂的场景,比如根据条件过滤、使用通配符匹配等。

根据条件过滤数组元素:

# 提取所有价格低于 10 的书的标题
# 注意这里的 [?()] 语法,它允许你在数组中进行条件筛选
jsonpath_expr_cheap_books = parse('$.store.book[?price < 10].title')
cheap_book_titles = [match.value for match in jsonpath_expr_cheap_books.find(data)]
print(f"价格低于 10 的书的标题: {cheap_book_titles}")
# 输出: 价格低于 10 的书的标题: ['Sayings of the Century']

# 提取所有 fiction 类书籍的作者
jsonpath_expr_fiction_author = parse('$.store.book[?category ="fiction"].author') # 字符串值需要用引号
fiction_authors = [match.value for match in jsonpath_expr_fiction_author.find(data)]
print(f"fiction 类书籍的作者: {fiction_authors}")
# 输出: fiction 类书籍的作者: ['Evelyn Waugh']

# 小提醒:条件过滤的语法 `[?()]` 里面支持各种比较操作符(<, >, =, !=, <=, >=)。# 如果遇到需要判断是否存在某个键的情况,可以用 `[?(@.key)]`,意思是存在 key 属性的元素。# 我之前爬取豆瓣时,就遇到过有些电影条目没有评分,用这个就能很方便地筛选掉。

* 使用通配符 ` 和递归下降 ..`:**

* 匹配当前层级的所有属性或数组元素。
.. 递归下降,匹配所有子孙节点。

# 提取所有书的任意属性(不推荐,但演示用法)# jsonpath_expr_all_book_props = parse('$.store.book[*].*')
# all_book_props = [match.value for match in jsonpath_expr_all_book_props.find(data)]
# print(f"所有书的所有属性值: {all_book_props}")
# 输出会很长,因为它会提取每个书对象的所有属性值。# 提取所有价格(无论它在哪里,使用递归下降)jsonpath_expr_all_prices = parse('$..price') # 匹配所有子孙节点中的 'price' 属性
all_prices = [match.value for match in jsonpath_expr_all_prices.find(data)]
print(f"所有价格: {all_prices}")
# 输出: 所有价格: [8.95, 12.99, 19.95]

# 小提醒:`..` 虽然方便,但在大型 JSON 中使用时要谨慎,因为它会遍历整个子树,可能会影响性能。# 只有当你确实不知道某个字段的具体路径时,才考虑使用它。我通常会尽量明确路径,这样可读性也更好。

第三步:实战——从复杂 API 响应中提取数据

假设我们正在处理一个电商平台的订单 API 响应,它可能包含多个订单、每个订单下又有很多商品,结构复杂。

api_response = {
    "status": "success",
    "timestamp": "2023-10-26T10:00:00Z",
    "data": {
        "orders": [
            {
                "orderId": "ORD001",
                "userId": "user_a",
                "status": "completed",
                "items": [{"productId": "P001", "name": "Python 编程指南", "price": 99.0, "quantity": 1},
                    {"productId": "P002", "name": "Django 开发实战", "price": 129.0, "quantity": 1}
                ],
                "totalAmount": 228.0
            },
            {
                "orderId": "ORD002",
                "userId": "user_b",
                "status": "pending",
                "items": [{"productId": "P003", "name": "数据结构与算法", "price": 88.0, "quantity": 2}
                ],
                "totalAmount": 176.0
            },
            {
                "orderId": "ORD003",
                "userId": "user_a",
                "status": "completed",
                "items": [{"productId": "P004", "name": "机器学习入门", "price": 150.0, "quantity": 1}
                ],
                "totalAmount": 150.0
            }
        ]
    },
    "metadata": {
        "page": 1,
        "pageSize": 10
    }
}

任务: 提取所有状态为 “completed” 的订单的 orderIdtotalAmount,以及这些订单中所有商品的名字。

# 提取所有已完成订单的 orderId 和 totalAmount
completed_orders_info_expr = parse('$.data.orders[?status ="completed"]')
completed_orders = completed_orders_info_expr.find(api_response)

for order_match in completed_orders:
    order_data = order_match.value
    order_id = order_data.get("orderId")
    total_amount = order_data.get("totalAmount")
    print(f"已完成订单 - Order ID: {order_id}, Total Amount: {total_amount}")

    # 进一步提取该订单下的所有商品名
    # 这里我们不能直接在原表达式上加 `.items[*].name`,因为 `order_match` 已经是过滤后的对象了
    # 我们可以对 `order_data` 这个子结构再应用 Jsonpath
    item_names_expr = parse('$.items[*].name') # 注意这里的路径是相对于当前订单对象了
    item_names = [match.value for match in item_names_expr.find(order_data)]
    print(f"商品列表: {', '.join(item_names)}")

# 输出示例:# 已完成订单 - Order ID: ORD001, Total Amount: 228.0
#   商品列表: Python 编程指南, Django 开发实战
# 已完成订单 - Order ID: ORD003, Total Amount: 150.0
#   商品列表: 机器学习入门

# 小提醒:遇到深度嵌套的数据,不要怕,一层层分析路径就好。# 我刚开始也容易迷路,会画个简图,把 JSON 结构梳理出来,再对应地写 Jsonpath 表达式,这样清晰很多。# `get()` 方法在这里也很有用,可以避免键不存在时抛出 KeyError。

常见误区与避坑指南

作为过来人,我总结了几个新手在使用 jsonpath_ng 时常犯的错误,希望大家能少走弯路:

  1. 误区一:混淆 parse()find() 的返回类型。
    jsonpath_ng.parse() 返回的是一个 JsonPath 对象,而 find(data) 方法返回的是一个 JsonPathFinder 对象的列表。你必须通过 match.value 才能获取到实际的数据值。我刚开始学 jsonpath_ng 时,总以为 find() 直接返回想要的值,结果老是拿到一堆 <JsonPathFinder object at ...>,调试半天才发现要加 .value,哭笑不得。

  2. 误区二:数组索引与对象键的混用。
    Jsonpath 表达式中,$ 代表根节点,. 用于访问对象的属性,[] 用于访问数组元素(通过索引)或进行条件筛选。例如,$.users[0].name 是正确的,表示访问 users 数组的第一个元素的 name 属性。但 $.users.0.name 就是错误的,因为 0 不是 users 对象下的一个属性。

  3. 误区三:错误处理空值或路径不存在的情况。
    如前所述,find() 方法在路径不存在时会返回一个空列表 [],而不是抛出 KeyError 或其他异常。这是一个设计上的优点,意味着你不需要为每次可能的路径缺失都编写 try-except 块。但你需要在使用结果时检查列表是否为空,例如 if result:if result[0] if result else None 这种方式。

掌握 Jsonpath,能让你的 JSON 数据处理代码更精炼、更高效,大幅提升开发效率。特别是在处理来自 API 的复杂响应时,它简直是神器!

你还遇到过哪些 Jsonpath 的奇葩用法或踩坑经历?欢迎在评论区分享,咱们一起交流学习!

正文完
 0
评论(没有评论)