网页抓取:拦截XHR请求

你试过抓取 AJAX 网站吗?充满 Javascript 和 XHR 调用的站点?破译大量嵌套的 CSS 选择器?或者更糟的是,每天更换选择器?也许你再也不需要它了。继续阅读,XHR 抓取可能是您的最终解决方案!

先决条件

要使代码正常工作,您需要安装 python3。有些系统已经预装了它。之后,安装 Playwright 和适用于 Chromium、Firefox 和 WebKit 的浏览器二进制文件。

pip install playwright 
playwright install

拦截响应

正如我们在之前关于阻塞资源的博文中看到的,无头浏览器允许请求和响应检查。我们将使用Python 中的Playwright进行演示,但也可以使用 Javascript 或使用Puppeteer来完成。

我们可以快速检查页面上的所有响应。正如我们在下面看到的,该response参数包含状态、URL 和内容本身。这就是我们将要使用的,而不是使用 CSS 选择器直接抓取 HTML 中的内容。

page.on("response", lambda response: print( 
    "<<", response.status, response.url))

用例:auction.com

我们的第一个例子是auction.com。您可能需要代理或 VPN,因为它会阻止他们运营所在的国家/地区之外。无论如何,尝试从您的 IP 中抓取可能是个问题,因为他们最终会禁止它。如果发现任何问题,请查看如何避免阻塞。

下面是一个在记录所有响应时使用 Playwright 加载页面的基本示例。

from playwright.sync_api import sync_playwright 
 
url = "https://www.auction.com/residential/ca/" 
 
with sync_playwright() as p: 
    browser = p.chromium.launch() 
    page = browser.new_page() 
 
    page.on("response", lambda response: print( 
        "<<", response.status, response.url)) 
    page.goto(url, wait_until="networkidle", timeout=90000) 
 
    print(page.content()) 
 
    page.context.close() 
    browser.close()

auction.com将加载一个没有我们想要的内容(房价或拍卖日期)的 HTML 框架。然后他们将加载多种资源,例如图像、CSS、字体和 Javascript。如果我们想节省一些带宽,我们可以过滤掉其中的一些。现在,我们将专注于有吸引力的部分。

auction-896

正如我们在网络选项卡中看到的那样,几乎所有相关内容都来自对资产端点的 XHR 调用。忽略其余部分,我们可以通过检查响应 URL 是否包含此字符串来检查该调用:if ("v1/search/assets?" in response.url)

有一个大小和时间问题:页面将加载跟踪和地图,加载时间超过一分钟(使用代理)和 130 个请求😮。我们可以通过阻止某些域和资源来做得更好。我们能够在 20 秒内完成,在我们的测试中仅加载了 7 个资源。我们将把它留给你作为练习 😉

<< 407 https://www.auction.com/residential/ca/ 
<< 200 https://www.auction.com/residential/ca/ 
<< 200 https://cdn.auction.com/residential/page-assets/styles.d5079a39f6.prod.css 
<< 200 https://cdn.auction.com/residential/page-assets/framework.b3b944740c.prod.js 
<< 200 https://cdn.cookielaw.org/scripttemplates/otSDKStub.js 
<< 200 https://static.hotjar.com/c/hotjar-45084.js?sv=5 
<< 200 https://adc-tenbox-prod.imgix.net/resi/propertyImages/no_image_available.v1.jpg 
<< 200 https://cdn.mlhdocs.com/rcp_files/auctions/E-19200/photos/thumbnails/2985798-1-G_bigThumb.jpg 
# ...

为了更直接的解决方案,我们决定更改为函数wait_for_selector。这不是理想的解决方案,但我们注意到有时脚本会在加载内容之前完全停止。为了避免这些情况,我们改变了等待方法。

在检查结果时,我们看到骨架上有包装纸。但每个房子的内容不是。所以我们将等待其中之一:"h4[data-elm-id]"

with sync_playwright() as p: 
    def handle_response(response): 
        # the endpoint we are insterested in 
        if ("v1/search/assets?" in response.url): 
            print(response.json()["result"]["assets"]["asset"]) 
    # ... 
    page.on("response", handle_response) 
    # really long timeout since it gets stuck sometimes 
    page.goto(url, timeout=120000) 
    page.wait_for_selector("h4[data-elm-id]", timeout=120000)

这里我们有输出,比界面提供的信息更多!一切都很干净,格式也很好😎

[{ 
    "item_id": "E192003", 
    "global_property_id": 2981226, 
    "property_id": 5444765, 
    "property_address": "13841 COBBLESTONE CT", 
    "property_city": "FONTANA", 
    "property_county": "San Bernardino", 
    "property_state": "CA", 
    "property_zip": "92335", 
    "property_type": "SFR", 
    "seller_code": "FSH", 
    "beds": 4, 
    "baths": 3, 
    "sqft": 1704, 
    "lot_size": 0.2, 
    "latitude": 34.10391, 
    "longitude": -117.50212, 
    ...

我们可以更进一步,使用分页来获取整个列表,但我们会把它留给你。

用例:twitter.com

另一个没有初始内容的典型案例是推特。为了能够抓取 Twitter,您无疑需要 Javascript 渲染。与前面的情况一样,您可以在加载全部内容后使用 CSS 选择器。但要注意,因为 Twitter 类是动态的,它们会经常更改。

最有可能保持不变的是他们在内部使用的 API 端点来获取主要内容:TweetDetail. 在这种情况下,最简单的方法是在 devTools 的网络选项卡中检查 XHR 调用,并在每个请求中查找一些内容。这是一个很好的例子,因为 Twitter 可以在每个页面视图中发出 20 到 30 个 JSON 或 XHR 请求。

twitter-896

一旦我们确定了我们感兴趣的呼叫和响应,该过程将是相似的。

import json 
from playwright.sync_api import sync_playwright 
 
url = "https://twitter.com/playwrightweb/status/1396888644019884033" 
 
with sync_playwright() as p: 
    def handle_response(response): 
        # the endpoint we are insterested in 
        if ("/TweetDetail?" in response.url): 
            print(json.dumps(response.json())) 
 
    browser = p.chromium.launch() 
    page = browser.new_page() 
    page.on("response", handle_response) 
    page.goto(url, wait_until="networkidle") 
    page.context.close() 
    browser.close()

输出将是一个相当大的 JSON (80kb),其内容比我们要求的更多。在我们到达推文内容之前,有十多个嵌套结构。好消息是我们现在可以访问收藏夹、转推或回复计数、图像、日期、回复推文及其内容等等。

用例:nseindia.com

股票市场是不断变化的基本数据来源。一些提供此信息的网站,例如印度国家证券交易所,将以空骨架开始。在网站上浏览几分钟后,我们看到市场数据是通过 XHR 加载的。

另一个常见的线索是查看页面源代码并检查那里的内容。如果它不存在,通常意味着它将稍后加载,这可能需要 XHR 请求,我们可以拦截那些。

nse-896

由于我们正在解析一个列表,我们将遍历它以结构化的方式只打印部分数据:每个条目的代码和价格。

from playwright.sync_api import sync_playwright 
 
url = "https://www.nseindia.com/market-data/live-equity-market" 
 
with sync_playwright() as p: 
    def handle_response(response): 
        # the endpoint we are insterested in 
        if ("equity-stockIndices?" in response.url): 
            items = response.json()["data"] 
            [print(item["symbol"], item["lastPrice"]) for item in items] 
 
    browser = p.chromium.launch() 
    page = browser.new_page() 
 
    page.on("response", handle_response) 
    page.goto(url, wait_until="networkidle") 
 
    page.context.close() 
    browser.close() 
 
# Output: 
# NIFTY 50 18125.4 
# ICICIBANK 846.75 
# AXISBANK 845 
# ...

与前面的示例一样,这是一个简化的示例。打印不是解决现实世界问题的方法。相反,每个页面结构都应该有一个内容提取器和一个存储它的方法。并且系统还应该独立处理爬行部分。

结论

我们希望您提出三个要点:

  1. 检查页面以查找干净的数据
  2. API 端点的更改频率低于 CSS 选择器和 HTML 结构
  3. Playwright 提供的不仅仅是 Javascript 渲染

即使提取的数据相同,容错性和编写爬虫的努力也是基本因素。手动更改它们的次数越少越好。

除了 XHR 请求之外,还有许多其他方式可以在选择器之外抓取数据。并非每个人都适用于给定的网站,但将它们添加到您的工具箱中可能经常对您有所帮助。

类似文章