集成阅读约 7 分钟

在 Playwright 中屏蔽图片、字体和媒体以减少代理流量

在 Python 和 Node.js 中用 Playwright 的 route() 屏蔽图片、字体和媒体请求,可作用于整个浏览器上下文或按主机屏蔽,并说明 HTTP 缓存与 Service Worker 的注意事项。

本页内容

起点

在浏览器上下文的每个页面上中止抓取任务不需要的大体积请求,然后测量这次运行是否真的少传输了字节。

先测量任务,再屏蔽任何内容

图片、字体和视频可能占页面下载量的很大一部分,而许多抓取任务根本不读取它们。通过请求拦截屏蔽它们,是减少代理流量的常见做法。它对你的任务是否有帮助取决于你的页面,所以先做一次测量:按原样运行一次任务,记录字节都去了哪里。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

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

javascript
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 · 一条路由覆盖所有页面

javascript
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 · 中止指定主机

python
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。
  • 确认屏蔽之后提取到的值没有变化。
  • 在依赖这项改动之前,通过同一家服务商再做一次实测运行并比较。

参考来源与延伸阅读

本指南参考的技术资料。请以你所安装版本的文档以及代理服务商支持的配置为准。