🎯 一款可定制、具备反检测功能的云浏览器,由自主研发的 Chromium驱动,专为网页爬虫AI 代理设计。👉立即试用
返回博客

剧作家 + 无痕抓取浏览器:拦截隐藏的 JSON API

Alex Johnson
Alex Johnson

Senior Web Scraping Engineer

29-Jul-2026

一个页面上写着“quotes”,但没有传送任何引用。使用普通的HTTP客户端请求 https://quotes.toscrape.com/scroll,响应体包含一个空的 <div class="quotes"></div> — 实际的文本、作者和标签随后通过 GET /api/quotes?page=N 调用到达,页面自己的JavaScript在加载时和每次滚动时触发。直接读取该调用,而不是等待浏览器将其呈现为HTML并再解析回HTML,是网络请求拦截:通过Chrome DevTools协议连接一个真实的浏览器,监听它产生的流量,并读取网站自己已经生成的JSON。

本指南通过CDP将Playwright连接到Scrapeless Scraping Browser,然后以两种方式拦截该隐藏端点 — Playwright自己的响应事件和它们下面的原始CDP Network域 — 然后在知道其形状后直接通过普通HTTP重放该端点。下面的每个命令都是针对实时目标运行的。

为什么直接读取网络而不是DOM

quotes.toscrape.com/scroll是一个公共的爬取练习目标,专门用于演示无限滚动,其标记本身就证明了拦截的必要性。获取页面并查找数据:

bash Copy
curl -s https://quotes.toscrape.com/scroll | grep -o 'class="quote"' | wc -l

每次返回零,因为引用从未存在于服务器发送的HTML中。一个小的jQuery块在加载时调用一次 $.get('/api/quotes', {page: page}),并在滚动位置接近底部时再次调用,然后将返回的行手动附加到空容器中。一个运行了JavaScript并等待足够长时间的浏览器最终将会在其DOM中显示每页相同的10个引用 — 但在你回过头来统计 .quote 元素并从 <span class="text"><small class="author"> 节点中提取文本时,你已经手动重建了页面已经拥有的、格式清晰、类型明确的JSON数据:一个 quotes 数组,每个都包含 textauthor 对象和 tags 列表,以及一个 has_next 标志,准确地告诉你何时停止。直接读取响应跳过了重建步骤,给你提供了字段而不是节点。

先决条件

你需要Python 3.9或更新版本 — playwright 1.59.0在PyPI上声明 Requires-Python >=3.9 — 还需要 playwright 包本身,以及来自app.scrapeless.com的免费计划的Scrapeless API密钥。该密钥通过 Scraping Browser 的CDP端点作为 token 查询参数传递,因此请将其保存在环境变量中,而不是你的脚本中的字面量。不需要本地Chrome安装:connect_over_cdp 可以连接到云中已经存在的浏览器。

通过CDP将Playwright连接到爬虫浏览器

安装客户端并设置密钥:

bash Copy
pip install playwright
bash Copy
export SCRAPELESS_API_KEY="your_scrapeless_api_key"

Scraping Browser的CDP端点是 wss://browser.scrapeless.com/api/v2/browser,通过三个查询参数 — tokensessionTTLproxyCountry — 到达,所有在这个系列中每个Playwright-over-Scraping-Browser脚本都使用相同的构造函数:

python Copy
import os
from urllib.parse import urlencode

API_KEY = os.environ["SCRAPELESS_API_KEY"]

def scraping_browser_url(proxy_country="US", session_ttl=180):
    params = urlencode({
        "token": API_KEY,
        "sessionTTL": session_ttl,
        "proxyCountry": proxy_country,
    })
    return f"wss://browser.scrapeless.com/api/v2/browser?{params}"

chromium.connect_over_cdp(scraping_browser_url()) 返回一个标准的Playwright Browser 对象。下面的拦截模式与Scraping Browser无关 — 它们可以在任何可通过CDP访问的Chromium上运行 — 但是在Scraping Browser上运行它们意味着渲染发生在已经具备住宅流量和反侦测Chromium的基础设施上,因此那些积极识别指纹的网站在你阅读其流量时仍然可以正常呈现。

使用响应监听器捕获隐藏API

Playwright的 page.expect_response() 将等待绑定到触发它的动作 — 没有任意的睡眠,没有轮询选择器计数直到它看起来稳定。将它绑定在 page.goto() 周围以进行第一次 /api/quotes 调用,然后在触发第二次调用的滚动周围:

python Copy
import os
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

API_KEY = os.environ["SCRAPELESS_API_KEY"]

def scraping_browser_url(proxy_country="US", session_ttl=180):
    params = urlencode({
        "token": API_KEY,
        "sessionTTL": session_ttl,
        "proxyCountry": proxy_country,
    })
    return f"wss://browser.scrapeless.com/api/v2/browser?{params}"

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(scraping_browser_url())
    page = browser.new_page()

    with page.expect_response("**/api/quotes*") as first_page:
        page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
    data = first_page.value.json()

打印(“首次响应到达之前的DOM引用计数:”,页面.定位器(“.quote”).计数())
打印(f“拦截页面 {data['page']} -> {len(data['quotes'])} 引用,has_next={data['has_next']}”)

Copy
使用页面.期望_响应(“**/api/quotes*”)作为第二页:
    页面.评价(“窗口.滚动到(0,文档.正文.滚动高度)”)
数据 = 第二页.值. json ()
打印(“滚动稳定后DOM引用计数:”, 页面.定位器(“.quote”).计数())
打印(f“拦截页面 {data['page']} -> {len(data['quotes'])} 引用,has_next={data['has_next']}”)

浏览器.关闭()
Copy
在直播站点上运行会打印:

```文本
首次响应到达之前的DOM引用计数: 10
拦截页面 1 -> 10 引用,has_next=True
滚动稳定后DOM引用计数: 20
拦截页面 2 -> 10 引用,has_next=True

通配符模式“**/api/quotes*”通过URL形状匹配终端,Playwright的文档化模式,用于等待特定的网络响应,而不是固定的超时。因为等待与触发操作相关,所以data['page'],data['quotes']data['has_next']以网站自己的脚本消费的相同类型的Python值返回——没有从<small>标签重建的.author.name,没有从锚文本重建的标签列表。

降低级别:使用CDP网络域读取原始帧

Playwright的响应事件位于Chrome DevTools协议网络域之上,您可以通过CDPSession直接与该域进行通信。当您根本不使用Playwright时,这很重要——在另一种语言中的裸CDP客户端,仅暴露协议事件的工具,或您想要响应头和特定绑定不呈现的时间的情况—因为Network.responseReceivedNetwork.getResponseBody的工作方式无关乎哪个客户端库位于其上:

python Copy
import os, json
from urllib.parse import urlencode
from playwright.sync_api import sync_playwright

API_KEY = os.environ["SCRAPELESS_API_KEY"]

def scraping_browser_url(proxy_country="US", session_ttl=180):
    params = urlencode({
        "token": API_KEY,
        "sessionTTL": session_ttl,
        "proxyCountry": proxy_country,
    })
    return f"wss://browser.scrapeless.com/api/v2/browser?{params}"

request_ids, bodies = {}, []

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(scraping_browser_url())
    page = browser.new_page()
    cdp = page.context.new_cdp_session(page)
    cdp.send("Network.enable")

    def on_response_received(event):
        if "/api/quotes" in event["response"]["url"]:
            request_ids[event["requestId"]] = event["response"]["url"]

    def on_loading_finished(event):
        rid = event["requestId"]
        if rid in request_ids:
            raw = cdp.send("Network.getResponseBody", {"requestId": rid})
            data = json.loads(raw["body"])
            bodies.append((request_ids[rid], data["page"], len(data["quotes"]), data["quotes"][0]["author"]["name"]))

    cdp.on("Network.responseReceived", on_response_received)
    cdp.on("Network.loadingFinished", on_loading_finished)

    page.goto("https://quotes.toscrape.com/scroll", wait_until="domcontentloaded")
    page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
    for _ in range(20):
        if len(bodies) >= 2:
            break
        page.wait_for_timeout(300)

    for url, page_no, count, author in bodies:
        print(f"CDP Network.getResponseBody on {url}: page={page_no}, quotes={count}, first_author={author}")

    browser.close()
文本 Copy
CDP Network.getResponseBody on https://quotes.toscrape.com/api/quotes?page=1: page=1, quotes=10, first_author=Albert Einstein
CDP Network.getResponseBody on https://quotes.toscrape.com/api/quotes?page=2: page=2, quotes=10, first_author=Marilyn Monroe

Network.responseReceived在响应开始时就会触发,带有标题和requestId; 直到Network.loadingFinished确认传输完成,主体本身才可用,这就是为什么处理程序分成两个事件而不是从第一个事件读取主体的原因。getResponseBody返回浏览器接收到的确切字节,外加用于二进制负载的base64Encoded标志—Playwright的response.json()构建其上并隐藏的信息。

通过在app.scrapeless.com注册获得免费的爬虫浏览器运行时,并对自己的目标运行上述两个脚本。

一旦您了解了形状,跳过浏览器

以下两个拦截证明了同一件事:/api/quotes?page=N 是一个公开、未认证的 GET 请求,返回 quotespagehas_next。一旦你了解了这个结构,就不再需要浏览器来访问它——一个普通的 HTTP 客户端可以直接分页浏览整个集合:

python Copy
import json
import urllib.error
import urllib.request

def fetch_page(n):
    url = f"https://quotes.toscrape.com/api/quotes?page={n}"
    req = urllib.request.Request(url, headers={"User-Agent": "network-interception-demo/1.0"})
    with urllib.request.urlopen(req, timeout=10) as resp:
        if resp.status != 200:
            raise urllib.error.HTTPError(url, resp.status, "unexpected status", resp.headers, None)
        return json.loads(resp.read())

all_quotes, page = [], 1
while True:
    data = fetch_page(page)
    all_quotes.extend(data["quotes"])
    if not data["has_next"]:
        break
    page += 1

print("获取的页面数:", page)
print("总引用数:", len(all_quotes))
print("第一条引用作者:", all_quotes[0]["author"]["name"])
print("最后一条引用作者:", all_quotes[-1]["author"]["name"])
text Copy
获取的页面数: 10
总引用数: 100
第一条引用作者: 阿尔伯特·爱因斯坦
最后一条引用作者: 乔治·R·R·马丁

十个普通的 HTTP 请求通过已确认的同一 JSON 合同拉取了全部 100 条引用集合,而完全不需要运行浏览器进程。这就是拦截的实际收益:在这里,浏览器唯一的任务是揭示该终端。一旦你有了它,分页浏览集合的最快方式通常是停止渲染并直接调用该终端,遵循 Fetch标准,该标准最终被浏览器和普通 HTTP 客户端实现。

何时仍然需要浏览器

并非每个隐藏的终端都是如此合作。许多需要服务器在早期页面加载期间设置的会话 cookie、嵌入在页面 JavaScript 包中的 CSRF 或签名请求令牌,或者从仅存在于客户端的状态中组装的请求主体,还有一些通过 WebSocket 帧或 GraphQL POST 而不是 REST 形状的 GET 发送,如 XMLHttpRequest标准 所描述。在这些情况下,前一节中直接重放的步骤不适用——你无法恢复你从未捕获的认证头——但拦截步骤仍然适用。page.expect_response() 和 CDP Network 域读取页面的流量,而不管是什么验证它或负载采取什么形状,因为它们观察浏览器实际发送和接收的内容,而不是提前假设特定的请求格式。在这些网站上,让浏览器始终处于循环中:让它创建会话,驱动导航,并在每次响应到达时将其交给你。

有关将相同的渲染然后阅读网络模式应用于整个网站而不是练习目标的实例,请参阅 TikTok 数据抓取指南,它在滚动真实信息流时以相同的方式捕获评论和帖子 XHR。

拦截不仅仅是一个提取步骤,也是一个发现步骤。第一次接触一个不熟悉的网站时,打开其开发者工具网络面板,通过 Fetch/XHR 进行筛选,并在与页面交互时观察触发的内容——这种手动操作能够告诉你在编写脚本之前将 page.expect_response() 绑定到哪个终端。一旦终端确定,以上所有内容——响应监听器、原始 CDP 会话和直接重放——都是同一发现以代码的形式编码,该代码可以自我运行。

app.scrapeless.com 注册以获取免费的抓取浏览器运行时,或查看 抓取浏览器产品页面定价 以获取规模运行的信息。

常见问题解答

问:在网络爬虫中,网络请求拦截是什么?
它是读取页面自身 JavaScript 生成的 XHR/fetch 流量——通常是一个 JSON API 调用——而不是等待该流量渲染为 HTML 然后解析渲染的标记。

问:拦截网站自己的 API 调用是否合法?
读取浏览器在访问公共页面时接收到的响应,与访问需要身份验证的内容或获取非公共数据有不同的考虑。在任何工作流程中,限制在公共页面,尊重目标的服务条款和爬虫指令,并保持请求量的控制——将拦截视为更精确读取流量的手段,而不是忽视访问规则的许可证。

问:一旦知道隐藏端点,你还需要浏览器吗?
只有当端点需要浏览器提供的内容时——会话cookie、签名令牌、JavaScript计算的状态。像本指南中所述的公共、未验证的端点可以用普通HTTP客户端重放,正如直接重放示例所示。

问:page.expect_response()与监听原始CDP Network域有什么区别?
page.expect_response()是Playwright的高级封装:将其绑定到URL模式,触发操作,获取解析后的Response对象。CDP Network域是其底层协议——Network.responseReceivedNetwork.getResponseBody——当你根本不使用Playwright绑定,或需要特定客户端库未公开的协议级细节时,它非常有用。

问:这是否适用于返回JSON以外内容的端点?
这两种拦截模式可以读取响应携带的任何字节——在Playwright中为response.text()response.body(),在CDP中为Network.getResponseBody的原始body字段——因此HTML片段、XML或任何其他有效负载以相同方式传递。JSON只是页面自身内部API的常见情况。

问:page.expect_response()能捕捉在你观看的页面加载之前发出的请求吗?
不能——它必须在触发操作运行之前开始监听,这就是它封装特定page.goto()或导致请求的交互的原因,而不是事后附加。

问:拦截是否需要特定于Scrapeless Scraping Browser,还是可以与任何可通过CDP访问的Chromium一起使用?
该技术本身是通用的CDP行为,可以在任何可以通过connect_over_cdp访问的Chromium上使用,局部或远程。在Scrapeless Scraping Browser上运行会增加反检测的Chromium和住宅出口,这在你拦截的目标具有足够激进的指纹时很重要,因为普通本地浏览器无法渲染以产生流量。

问:如果网站更改其端点或响应结构会发生什么?
只要URL模式仍然匹配,拦截代码就能继续工作;重命名字段或重构有效负载会破坏读取data['quotes']的代码,就像CSS选择器在类名更改时会失效一样。没有一种方法能够抵御重新设计——读取API只是意味着你在跟踪JSON合同,而不是标记结构,后者通常变化得较少。

在Scrapeless,我们仅访问公开可用的数据,并严格遵循适用的法律、法规和网站隐私政策。本博客中的内容仅供演示之用,不涉及任何非法或侵权活动。我们对使用本博客或第三方链接中的信息不做任何保证,并免除所有责任。在进行任何抓取活动之前,请咨询您的法律顾问,并审查目标网站的服务条款或获取必要的许可。

最受欢迎的文章

目录