in

如何使用Python中的JSONPath解析JSON

如何使用Python中的JSONPath解析JSON

JSONPath 是 JSON 的路径表达语言。它用于从 JSON 数据集中查询数据,类似于 XML 文档的 XPath 查询语言。

顾名思义,JSONPath 深受 XPath 的启发,并提供类似的语法和查询功能:

  • 递归数据查找。price例如查找数据集中的所有节点
  • 通配符匹配
  • 数组切片
  • 过滤
  • 函数调用和自定义函数扩展

在本 JSONPath 教程中,我们将了解如何在网页爬取的上下文中使用此路径语言。JSONPath 用许多不同的语言实现,但在本教程中,我们将介绍最流行的 Python 实现。

JSONPath 设置

JSONPath 是一个没有集中主体的 JSON 查询规范,因此它由许多不同的项目以多种不同的语言实现:

语言 执行
Python jsonpath-of
jsonpath2
JavaScript jsonpath-plus
Ruby jsonpath
R rjsonpath
Go ojg
implementation by Kubernetes

JSONPath简介

首先,所有 JSONPath 查询表达式都是由 JSON 键和运算符组成的简单字符串。让我们看一下这个例子:

import jsonpath_ng.ext as jp

data = {
    "products": [
        {"name": "Apple", "price": 12.88, "tags": ["fruit", "red"]},
        {"name": "Peach", "price": 27.25, "tags": ["fruit", "yellow"]},
        {"name": "Cake", "tags": ["pastry", "sweet"]},
    ]
}

# find all product names:
query = jp.parse("products[*].name")
for match in query.find(data):
    print(match.value)

# find all products with price > 20
query = jp.parse("products[?price>20].name")
for match in query.find(data):
    print(match.value)

在这里,为了提取所有产品名称,我们使用products[*].name查询迭代所有products数组元素([*]运算符)并返回name每个元素的属性。JSONPath 还支持过滤表达式,例如[?price>20]返回属性price大于 20 的所有元素。

让我们看一下所有可用的运算符和一些示例:

操作员 功能
$ 对象根选择器
@ or this 当前对象选择器
.. 递归后代选择器
* 通配符,选择对象的任意键或数组的索引
[] 下标运算符
[start:end:step] 数组切片运算符
[?<predicate>] or (?<predicate>) 过滤运算符,其中谓词是一些评估规则,例如[?price>20],更多示例:
[?price > 20 & price < 10]多种的
[?address.city = "Boston"]精确匹配
[?description.text =~ "house"]用于包含值

这些基本运算符提供了许多强大的查询选项。让我们看一下网络爬取背景下的一些示例。

网页爬取示例

让我们在一个真实的爬取器示例中使用 JSONPath,看看如何使用 Python 进行网页爬取。

我们将从realtor.com中获取房地产数据, realtor.com是一个流行的租赁和销售房地产的门户网站。

与许多现代网站一样,该网站使用 Javascript 来呈现其页面,这意味着我们不能只爬取 HTML 代码。相反,我们将找到前端用于呈现页面的 JSON 变量数据。这称为隐藏的网络数据

隐藏的 Web 数据通常可以在 HTML 代码中找到并使用 HTML 解析器提取,但是这些数据通常填充有关键字、ID 和其他非数据字段,这就是为什么我们将使用 JSONPath 来仅提取有用的数据。

让我们看一下像这样的随机示例属性

如果我们查看页面源代码,我们可以看到隐藏在标签中的 JSON 数据集<script>

how-to-scrape-realtorcom_page-source-prop

realtor.com 页面源中隐藏的网络数据插图

realtor.com 页面源中隐藏的网络数据插图
我们可以看到隐藏在脚本元素中的整个属性数据集

为了解决这个问题,我们将使用一些 Python 包:

  • httpx – 用于检索页面的 HTTP 客户端库。
  • parsel – 用于提取<script>元素数据的 HTML 解析库。
  • jsonpath-ng – 解析属性数据字段的 JSON 数据。

pip install所有这些都可以使用命令安装:

$ pip install jsonpath-ng httpx parsel

因此,我们将检索 HTML 页面,找到<script>包含隐藏 Web 数据的元素,然后使用 JSONPath 提取最重要的属性数据字段:

import json
import httpx
from parsel import Selector
import jsonpath_ng as jp

# establish HTTP client and to prevent being instantly banned lets set some browser-like headers
session = httpx.Client(
    headers={
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
    },
)

# 1. Scrape the page and parse hidden web data
response = session.get(
    "https://www.realtor.com/realestateandhomes-detail/335-30th-Ave_San-Francisco_CA_94121_M17833-49194"
)
assert response.status_code == 200, "response is banned"
selector = Selector(text=response.text)
# find <script id="__NEXT_DATA__"> node and select it's text:
data = selector.css("script#__NEXT_DATA__::text").get()
# load the hidden JSON as python dictionary:
data = json.loads(data)

# here we define our JSONPath helpers: one to select first match and one to select all matches:
jp_first = lambda query, data: jp.parse(query).find(data)[0].value
jp_all = lambda query, data: [match.value for match in jp.parse(query).find(data)]

prop_data = jp_first("$..propertyDetails", data)
result = {
    # for some fields we don't need complex queries:
    "id": prop_data["listing_id"],
    "url": prop_data["href"],
    "status": prop_data["status"],
    "price": prop_data["list_price"],
    "price_per_sqft": prop_data["price_per_sqft"],
    "date": prop_data["list_date"],
    "details": prop_data["description"],
    # to reduce complex datafields we can use jsonpath again:
    # e.g. we can select by key anywhere in the data structure:
    "estimate_high": jp_first("$..estimate_high", prop_data),
    "estimate_low": jp_first("$..estimate_low", prop_data),
    "post_code": jp_first("$..postal_code", prop_data),
    # or iterate through arrays:
    "features": jp_all("$..details[*].text[0]", prop_data),
    "photos": jp_all("$..photos[*].href", prop_data),
    "buyer_emails": jp_all("$..buyers[*].email", prop_data),
    "buyer_phones": jp_all("$..buyers[*].phones[*].number", prop_data),
}
print(result)

示例输出

{
  "id": "2950457253",
  "url": "https://www.realtor.com/realestateandhomes-detail/335-30th-Ave_San-Francisco_CA_94121_M17833-49194",
  "status": "sold",
  "price": 2995000,
  "price_per_sqft": 982,
  "date": "2022-12-04T23:43:42Z",
  "details": {
    "baths": 4,
    "baths_3qtr": null,
    "baths_full": 3,
    "baths_full_calc": 3,
    "baths_half": 1,
    "baths_max": null,
    "baths_min": null,
    "baths_partial_calc": 1,
    "baths_total": null,
    "beds": 4,
    "beds_max": null,
    "beds_min": null,
    "construction": null,
    "cooling": null,
    "exterior": null,
    "fireplace": null,
    "garage": null,
    "garage_max": null,
    "garage_min": null,
    "garage_type": null,
    "heating": null,
    "logo": null,
    "lot_sqft": 3000,
    "name": null,
    "pool": null,
    "roofing": null,
    "rooms": null,
    "sqft": 3066,
    "sqft_max": null,
    "sqft_min": null,
    "stories": null,
    "styles": [
      "craftsman_bungalow"
    ],
    "sub_type": null,
    "text": "With four bedrooms, three and one-half baths, and over 3, 000 square feet of living space, 335 30th avenue offers a fantastic modern floor plan with classic finishes in the best family-friendly neighborhood in San Francisco. Originally constructed in 1908, the house underwent a total gut renovation and expansion in 2014, with an upgraded foundation, all new plumbing and electrical, double-pane windows and all new energy efficient appliances. Interior walls were removed on the main level to create a large flowing space. The home is detached on three sides (East, South, and West) and enjoys an abundance of natural light. The top floor includes the primary bedroom with two gorgeous skylights and an en-suite bath; two kids bedrooms and a shared hall bath. The main floor offers soaring ten foot ceilings and a modern, open floor plan perfect for entertaining. The combined family room - kitchen space is the heart of the home and keeps everyone together in one space. Just outside the breakfast den, the back deck overlooks the spacious yard and offers indoor/outdoor living. The ground floor encompasses the garage, a laundry room, and a suite of rooms that could serve as work-from-home space, AirBnB, or in-law unit.",
    "type": "single_family",
    "units": null,
    "year_built": 1908,
    "year_renovated": null,
    "zoning": null,
    "__typename": "HomeDescription"
  },
  "estimate_high": 3253200,
  "estimate_low": 2824400,
  "post_code": "94111",
  "features": [
    "Bedrooms: 4",
    "Total Rooms: 11",
    "Total Bathrooms: 4",
    "Built-In Gas Oven",
    "Breakfast Area",
    "Fireplace Features: Brick, Family Room, Wood Burning",
    "Interior Amenities: Dining Room, Family Room, Guest Quarters, Kitchen, Laundry, Living Room, Primary Bathroom, Primary Bedroom, Office, Workshop",
    "Balcony",
    "Lot Description: Adjacent to Golf Course, Landscape Back, Landscape Front, Low Maintenance, Manual Sprinkler Rear, Zero Lot Line",
    "Driveway: Gated, Paved Sidewalk, Sidewalk/Curb/Gutter",
    "View: Bay, Bridges, City, San Francisco",
    "Association: No",
    "Source Listing Status: Closed",
    "Total Square Feet Living: 3066",
    "Sewer: Public Sewer, Septic Connected"
  ],
  "photos": [
    "https://www.jingzhengli.com/wp-content/uploads/2023/06/f707c59fa49468fde4999bbd9e2d433bl-m872089375s.jpg",
    "https://www.jingzhengli.com/wp-content/uploads/2023/06/f707c59fa49468fde4999bbd9e2d433bl-m872089375s.jpg",
    "https://www.jingzhengli.com/wp-content/uploads/2023/06/f707c59fa49468fde4999bbd9e2d433bl-m872089375s.jpg"
  ],
  "buyer_emails": [
    "REDACTED_FOR_BLOG@REDACTED_FOR_BLOG.com",
  ],
  "buyer_phones": [
    "415296XXXX",
    "415901XXXX",
    "415901XXXX",
  ]
}

正如我们使用 XPath 解析 HTML 数据集一样,我们可以使用 JSONPath 解析 JSON 数据集。JSONPath 是一种功能强大但简单的语言,在处理隐藏的 Web 数据时效果特别好。

常问问题

为了总结 JSONPath 教程,让我们看一下一些常见问题:

JMESPath 和 JSONPath 有什么区别?

JMESPath 是另一种流行的 JSON 查询语言,可用于更多编程语言。主要区别在于 JSONPath 遵循 XPath 语法,允许递归选择器和轻松的可扩展性,而 JMESPath 允许更轻松的数据集突变和过滤。我们建议使用 JSONPath 来提取嵌套数据,而 JMESPath 更适合处理更复杂但可预测的数据集。

JSONPath 慢吗?

由于 JSON 数据被转换为本机对象,因此 JSONPath 可以非常快,具体取决于实现和使用的算法。由于 JSONPath 只是一个查询规范,而不是单个项目,速度因每个实现而异,但一般来说,它应该与 XML 的 XPath 一样快,甚至更快。

网页爬取中的 JsonPath 总结

在本介绍教程中,我们了解了 Python 中 JSON 的 JSONPath 查询语言。这种路径语言深受 XPath 的启发,允许我们从 JSON 数据集中提取嵌套数据,这意味着它非常适合 Web 爬取堆栈,因为我们可以使用两种类似的技术从 HTML 和 JSON 中提取数据。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。