起点
在浏览器上下文的每个页面上中止抓取任务不需要的大体积请求,然后测量这次运行是否真的少传输了字节。
先测量任务,再屏蔽任何内容
图片、字体和视频可能占页面下载量的很大一部分,而许多抓取任务根本不读取它们。通过请求拦截屏蔽它们,是减少代理流量的常见做法。它对你的任务是否有帮助取决于你的页面,所以先做一次测量:按原样运行一次任务,记录字节都去了哪里。scrapescope 是一款免费、开源的本地流量计,可按主机和资源类型报告一次 Playwright 运行的字节数,并模拟屏蔽图片、媒体和字体的效果。
下面的示例使用 Playwright 1.63 和占位地址 https://example.com/。请把 TARGET_URL 指向你获准自动化访问的页面,并把代理设置和凭据放在你自己的启动选项或上下文选项里;仅仅试用屏蔽功能并不需要它们。
在 Python 中按资源类型屏蔽
Playwright 会给每个请求标上渲染引擎识别出的资源类型:document、stylesheet、image、media、font、script、texttrack、xhr、fetch、eventsource、websocket、manifest 或 other。在浏览器上下文上为所有 URL 注册一条路由,中止这三种大体积类型;其他请求照常继续。
保存为 block_resources.py,在执行 pip install playwright 和 playwright install chromium 之后运行 python block_resources.py。该上下文还屏蔽了 Service Worker,原因见下文。
block_resources.py · Playwright for Python
import os
from playwright.sync_api import Route, sync_playwright
TARGET_URL = os.environ.get("TARGET_URL", "https://example.com/")
BLOCKED_TYPES = {"image", "media", "font"}
def block_heavy(route: Route) -> None:
if route.request.resource_type in BLOCKED_TYPES:
route.abort()
else:
route.continue_()
with sync_playwright() as p:
browser = p.chromium.launch()
try:
context = browser.new_context(service_workers="block")
context.route("**/*", block_heavy)
page = context.new_page()
response = page.goto(TARGET_URL, wait_until="load")
print({"status": response.status if response else None})
context.close()
finally:
browser.close()在 Node.js 中按资源类型屏蔽
Node.js 版本是同一条路由、同样的三种类型。route.request().resourceType() 返回类型,route.abort() 让请求在离开浏览器之前失败,route.continue() 则原样发送其余所有请求。
保存为 block-resources.mjs,在执行 npm install playwright 和 npx playwright install chromium 之后运行 node block-resources.mjs。
block-resources.mjs · Playwright Chromium
import { chromium } from 'playwright';
const TARGET_URL = process.env.TARGET_URL ?? 'https://example.com/';
const BLOCKED_TYPES = new Set(['image', 'media', 'font']);
const browser = await chromium.launch();
try {
const context = await browser.newContext({ serviceWorkers: 'block' });
await context.route('**/*', route =>
BLOCKED_TYPES.has(route.request().resourceType()) ? route.abort() : route.continue());
const page = await context.newPage();
const response = await page.goto(TARGET_URL, { waitUntil: 'load' });
console.log({ status: response?.status() });
await context.close();
} finally {
await browser.close();
}应用到浏览器上下文中的每个页面
浏览器上下文上的路由对该上下文中任何页面发出的请求都生效:之后打开的页面、第二个标签页,以及页面通过 window.open() 打开的弹出窗口。单个页面上的路由只对该页面生效,因此弹出窗口或新标签页会重新加载自己的图片和字体。请在第一个页面加载之前注册上下文路由,并把它放进一个辅助函数,让每个任务都使用同一条规则。
当一个请求同时匹配两者时,页面路由优先于浏览器上下文路由;当多条页面路由都匹配时,最近注册的那条生效。每个上下文只保留一条屏蔽路由,以免出现意外。
context-wide.mjs · 一条路由覆盖所有页面
import { chromium } from 'playwright';
const TARGET_URL = process.env.TARGET_URL ?? 'https://example.com/';
const BLOCKED_TYPES = new Set(['image', 'media', 'font']);
// One route on the context covers every page the context opens, including
// tabs opened later and popups. Register it before the first page loads.
async function blockHeavyResources(context) {
await context.route('**/*', route =>
BLOCKED_TYPES.has(route.request().resourceType()) ? route.abort() : route.continue());
}
const browser = await chromium.launch();
try {
const context = await browser.newContext({ serviceWorkers: 'block' });
await blockHeavyResources(context);
const first = await context.newPage();
const second = await context.newPage();
await Promise.all([
first.goto(TARGET_URL, { waitUntil: 'load' }),
second.goto(TARGET_URL, { waitUntil: 'load' }),
]);
console.log({ pages: context.pages().length });
await context.close();
} finally {
await browser.close();
}按 URL 模式屏蔽特定的大流量主机
有时问题出在某一个主机上:视频 CDN、广告服务器或字体服务。route() 也接受 URL 模式,因此你可以中止来自这些主机的所有请求,而不去动资源类型。像 https://media.example.net/** 这样的 glob 匹配一个主机上的所有 URL;正则表达式则可以列出多个主机。下面的主机只是占位符;真实的主机名应来自你的测量结果,而不是通用的屏蔽列表。
block_hosts.py · 中止指定主机
import os
import re
from playwright.sync_api import sync_playwright
TARGET_URL = os.environ.get("TARGET_URL", "https://example.com/")
# A glob covers every URL on one host; a regular expression can cover several.
HEAVY_HOST = "https://media.example.net/**"
HEAVY_HOSTS = re.compile(r"^https://(video\.example\.org|fonts\.example\.com)/")
with sync_playwright() as p:
browser = p.chromium.launch()
try:
context = browser.new_context(service_workers="block")
context.route(HEAVY_HOST, lambda route: route.abort())
context.route(HEAVY_HOSTS, lambda route: route.abort())
page = context.new_page()
response = page.goto(TARGET_URL, wait_until="load")
print({"status": response.status if response else None})
context.close()
finally:
browser.close()启用路由会关闭 HTTP 缓存
Playwright 的 API 参考对 page.route() 和 browserContext.route() 都写明:"Enabling routing disables http cache."(启用路由会禁用 HTTP 缓存。)一旦注册了任何路由,样式表、脚本以及其他本来会被浏览器从缓存中复用的重复资源,就会在每个页面重新下载。对于在同一个上下文中加载许多页面的任务,这部分额外传输可能抵消屏蔽所省去的一部分流量,甚至全部。这就是只测一个页面不够的原因:要测量整次运行。
拦截请求时屏蔽 Service Worker
由页面的 Service Worker 处理的请求可能绕过你的路由:API 参考指出,browserContext.route() 不会拦截已被 Service Worker 拦截的请求,并建议在使用请求拦截时把 serviceWorkers 设为 'block' 来禁用 Service Worker。这就是上面每个示例中的上下文选项 service_workers="block"(Python)和 serviceWorkers: 'block'(Node.js)。依赖 Service Worker 的网站(例如用它缓存内容以便离线使用)在无法注册时,行为可能会有所不同。
屏蔽可能改变页面的行为
屏蔽图片、媒体和字体可能破坏数据提取、改变页面的行为,或引来反爬虫系统的关注。页面可能要等某张图片加载后才渲染你要读取的数据,可能只在媒体事件之后才懒加载内容,也可能把从不下载图片的浏览器视为异常。启用路由后,请确认提取到的值没有变化,并让屏蔽行为保持在网站条款允许的范围内。
再次测量并比较
通过同一家服务商把同一个任务运行两次,一次不带路由,一次带路由,然后比较这两次运行。第二次实测运行才是证据;模型或估算只是预测。scrapescope 可以在其查看器中并排比较两份报告,包括按主机和按资源类型统计的字节数,让你看清路由去掉了什么,以及失去 HTTP 缓存又加回了什么。
上线之前
- 在做任何改动之前,先在不屏蔽的情况下测量一次任务。
- 在第一个页面加载之前,把路由注册到浏览器上下文上。
- 路由生效期间,在上下文选项中屏蔽 Service Worker。
- 确认屏蔽之后提取到的值没有变化。
- 在依赖这项改动之前,通过同一家服务商再做一次实测运行并比较。
参考来源与延伸阅读
本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。