能,Playwright通过page.route()或context.route()直接拦截XHR/Fetch请求,需精准定位目标API URL;推荐用page.on("response")监听并解析JSON响应,比Selenium更原生、灵活且稳定。

Playwright能直接拦截并读取XHR/Fetch请求吗
能,但不是靠“监听DOM变化”或“等页面加载完再找元素”,而是通过 page.route() 或 context.route() 拦截网络请求本身。关键在于:你得知道目标数据是通过哪个URL发起的(比如 /api/items 或 /graphql),否则拦截了也拿不到结构化数据。
常见错误是盲目启用 page.wait_for_load_state("networkidle") 后去抓 document.querySelectorAll,结果发现目标内容根本没渲染——因为数据压根没走HTML,而是由JS在运行时用 fetch 拉的JSON。
- 优先用浏览器开发者工具的 Network 面板,筛选
XHR或Fetch,找到返回真实数据的请求(看 Response 是 JSON 且字段匹配) - 拦截时建议用
context.route()而非page.route(),避免漏掉页面初始化阶段的预请求(如认证token获取) - 拦截后别只调
route.continue(),要用route.response()或route.fulfill()提前捕获响应体
如何在拦截中提取并保存JSON响应体
Playwright 的 route 对象不直接暴露响应体,必须主动调用 route.fetch_response()(需 Playwright ≥ 1.40)或改用 page.on("response") 监听。后者更稳定,尤其对重定向、压缩响应等场景。
示例逻辑:监听所有以 /api/ 开头的响应,检查 response.status() 和 response.headers()["content-type"] 是否含 application/json,再用 response.json() 解析:
立即学习“Java免费学习笔记(深入)”;
page.on("response", lambda r: handle_api_response(r) if r.url.startswith("https://example.com/api/") else None)
<p>def handle_api_response(response):
if response.status != 200:
return
try:
data = response.json()</p><h1>保存到列表或文件</h1><pre class='brush:python;toolbar:false;'> api_results.append(data)
except Exception:
pass</pre>- 注意
response.json()是异步方法,若在同步回调里用会报错;应确保回调函数为async,或改用response.text()+json.loads() - 部分API返回 gzip 响应,
response.text()会自动解压,但response.body()返回的是原始 bytes,需手动处理 - 不要在
handle_api_response里做耗时操作(如写文件),容易拖慢页面加载;先存内存,结束后统一处理
为什么用Playwright比Selenium更适合这类场景
核心差异不在“能不能跑JS”,而在网络层控制粒度:Playwright 的 route 和 response 事件是原生支持的,而 Selenium 必须依赖 CDP 协议手动调用 Network.getResponseBody,代码冗长且易因CDP版本变动失效。
-
Playwright支持在请求发出前修改 headers、query、甚至伪造响应(route.fulfill()),适合绕过简单反爬(如缺失Authorization) -
Selenium的driver.get_log("performance")只能拿到请求元信息,拿不到响应体,还得额外发一次请求去捞数据 - Playwright 的
browser.new_context(ignore_https_errors=True)对自签名证书兼容更好,企业内网调试更省事
拦截失败的三个高频原因
不是代码写错,而是环境或时机问题:
- 目标请求是
preflight OPTIONS请求,被自动忽略——实际数据在后续的POST/GET中,需监听主请求而非预检 - 页面用 Service Worker 缓存了响应,导致
response事件不触发——加context.add_init_script("window.navigator.serviceWorker.ready.then(() => {});")清除或禁用 - 请求 URL 动态拼接(含时间戳、sign参数),每次都不一样,无法用固定字符串匹配——改用正则匹配路径部分,例如
re.match(r"https://.*/api/items\?.*", url)
真正难的从来不是写几行拦截代码,而是确认那个“关键请求”到底长什么样、在什么条件下发出、有没有前置依赖。花十分钟看清楚 Network 面板,比写一小时重试逻辑更有效。


















