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

在 Playwright 中阻止资源:它实际上节省了什么

05-Aug-2026

TL;DR:

  • “阻止图片以节省带宽”完全取决于页面。 在三页的测试中,阻止图片、媒体和字体使一个图像密集的目录减少了67%的传输量,在文章页面减少了约25%,而在没有图片的纯文本列表页面几乎没有影响。
  • 在纯文本页面,样式表是主要负载。stylesheet 添加到阻止列表后,传输量降至2,121字节——仅HTML文档——因为其四个请求中的三个是CSS。
  • 字节节省是可靠的;时间节省则不然。 每个被阻止的配置文件传输的字节更少,但真实时间在不同运行中呈现出两种变化,因为拦截每个请求都有其自身的成本。
  • 阻止不消耗数据。 在所有九次测量中提取的元素数量是相同的——20个产品,10个报价,36个段落——因此没有丢弃任何有用的信息。
  • 测量绝对字节,而不是百分比,并多次测量。 这里有一页具有双峰基线,在不改变内容的情况下,它的显著节省在1%和65%之间波动。
  • 从免费开始。 Scraping Browser提供免费的服务,本文中的整个测量只涉及九次页面加载。

Playwright通过WebSocket CDP端点连接到Scrapeless Scraping Browser,这意味着云浏览器会话的行为类似于本地会话——而计费则像远程会话。页面提取的每个图像、字体和样式表都是你支付的流量。

标准建议是阻止图片。这个建议四处被重复,但没有附上数字,因此本文附上了一个:同样的脚本,三页,三个阻止配置,从协议中计数字节。

浏览器会话的实际成本

浏览器获取人类所能看到的一切。对于提取来说,大部分都是浪费——没有人去解析网络字体。网页重量通过HTTP Archive页面重量报告公开跟踪,像这样的汇总数字就是阻止图片建议的基础。

汇总数据是该建议出错的地方。你并不是抓取中位数页面;你抓取一个目标,其资产组合可能与中位数完全不同。

前提条件

  • Python 3.9或更高版本,并运行 pip install playwright
  • SCRAPELESS_API_KEY 环境变量中设置Scrapeless API密钥

无需下载本地浏览器。connect_over_cdp 连接到远程会话,因此 playwright install 不是此工作流程的一部分。

连接并计数字节

Playwright通过CDP连接,打开同一页面的原始CDP会话会计算跨越网络的字节:

python Copy
browser = await playwright.chromium.connect_over_cdp(ENDPOINT)
page = await browser.new_page()
cdp = await page.context.new_cdp_session(page)
await cdp.send("Network.enable")

transferred = {"bytes": 0}
cdp.on(
    "Network.loadingFinished",
    lambda event: transferred.__setitem__(
        "bytes", transferred["bytes"] + event.get("encodedDataLength", 0)
    ),
)

encodedDataLength 是重要的测量值。Chrome DevTools Protocol Network域将其定义为请求接收的总字节数,因此它计算的是压缩传输而不是解压的文档大小。这就是代理测量的内容,也是带宽账单所反映的内容。

Network.loadingFinished 上求和会给出每次页面加载的一个数字,整个过程中没有任何估算。

添加路由层

阻止是每个请求的路由决策:

python Copy
BLOCKED = {"image", "media", "font"}

async def router(route):
    if route.request.resource_type in BLOCKED:
        await route.abort()
    else:
        await route.continue_()

await page.route("**/*", router)

现在每个请求都通过 router,该路由要么中止请求,要么允许其继续——这是Playwright页面路由API中的处理器契约。不执行任何操作的处理器会挂起请求,直到导航超时,因此每个分支必须以 abortcontinue_ 结束。

resource_type 来源于浏览器对请求发起原因的分类,Fetch标准称之为请求目标。根据其匹配比按扩展名匹配URL更具耐用性,因为从 /assets/a8f3c2 提供的字体即使没有扩展名仍然被分类为 font
这是读取流量的镜像,而不是停止它——要提取页面请求中的数据,请参见 拦截页面的隐藏 JSON API

测量三页

完整的脚本在三个配置文件下加载每个目标,并打印字节、节省、时间和仍可提取的元素数量。两个对比的设置足以说明问题;将自己的目标添加到 TARGETS 中以相同方式进行测量:

python Copy
import asyncio
import os
import time

from playwright.async_api import async_playwright

ENDPOINT = (
    "wss://browser.scrapeless.com/api/v2/browser"
    f"?token={os.environ['SCRAPELESS_API_KEY']}&session_ttl=300&proxy_country=US"
)

TARGETS = [
    ("books.toscrape.com", "https://books.toscrape.com/", "article.product_pod"),
    ("quotes.toscrape.com", "https://quotes.toscrape.com/", "div.quote"),
]

PROFILES = {
    "baseline": set(),
    "media-only": {"image", "media", "font"},
    "media+css": {"image", "media", "font", "stylesheet"},
}

PAUSE_SECONDS = 5


async def measure(playwright, url, selector, blocked):
    """加载一个页面并返回(传输字节,秒,匹配的元素)。

    每个测量都有自己的连接,因此在配置文件之间不共享 HTTP 缓存;一个温暖的缓存会低估冷加载真正的成本。
    """
    browser = await playwright.chromium.connect_over_cdp(ENDPOINT)
    try:
        page = await browser.new_page()
        cdp = await page.context.new_cdp_session(page)
        await cdp.send("Network.enable")

        transferred = {"bytes": 0}
        cdp.on(
            "Network.loadingFinished",
            lambda event: transferred.__setitem__(
                "bytes", transferred["bytes"] + event.get("encodedDataLength", 0)
            ),
        )

        if blocked:
            async def router(route):
                if route.request.resource_type in blocked:
                    await route.abort()
                else:
                    await route.continue_()

            await page.route("**/*", router)

        started = time.monotonic()
        await page.goto(url, wait_until="load", timeout=120_000)
        elapsed = time.monotonic() - started
        matched = await page.locator(selector).count()
        return transferred["bytes"], elapsed, matched
    finally:
        await browser.close()


async def main():
    print(f"{'target':22}{'profile':12}{'bytes':>10}{'saved':>7}{'time':>8}{'matched':>9}")
    async with async_playwright() as playwright:
        for name, url, selector in TARGETS:
            baseline_bytes = None
            for profile, blocked in PROFILES.items():
                # 使加载间隔开:这将为每次测量打开一个新的会话,
                # 并且连续加载九个页面对目标来说并不礼貌。
                await asyncio.sleep(PAUSE_SECONDS)
                transferred, elapsed, matched = await measure(playwright, url, selector, blocked)
                if baseline_bytes is None:
                    baseline_bytes = transferred
                saved = round((1 - transferred / baseline_bytes) * 100)
                print(f"{name:22}{profile:12}{transferred:>10,}{saved:>6}%{elapsed:>7.2f}s{matched:>9}")


if __name__ == "__main__":
    asyncio.run(main())

它的一次运行:

text Copy
target                profile          bytes  saved    time  matched
books.toscrape.com    baseline       337,691     0%   4.22s       20
books.toscrape.com    media-only     111,939    67%   5.62s       20
books.toscrape.com    media+css       76,329    77%   2.56s       20
quotes.toscrape.com   baseline        26,731     0%   5.86s       10
quotes.toscrape.com   media-only      26,739     0%   5.22s       10
quotes.toscrape.com   media+css        2,125    92%   3.12s       10

请记住这两行 quotes.toscrape.com 被阻止的行:阻止图像、媒体和字体并没有节省任何东西,而在此基础上阻止样式表几乎节省了所有内容。下一部分将解释为什么——以及为什么同一页面有时会从同一媒体配置文件中报告 65% 的节省。

数字所说的

百分比是读取这一点的自然方式,它们也是其中最不稳定的东西。被阻止的配置文件非常可重复;基线则不然,百分比是两者的比率。
目录页面几乎可以完全重复——其三个基准测量相互之间的误差为0.004%。百科全书文章的运行间差异约为7%。而文本列表则表现出两种模式:大多数加载约为26,700字节,但大约每三次加载中就有一次加载约为75,820字节,因为浏览器有时会获取样式表引用的字体二进制文件,有时则不会。基于这一点,其在阻止媒体时的“节省”波动在1%到65%之间,页面的实际内容没有变化。

因此,首先要查看绝对列。三个运行的中位数,加上以相同方式测量的实际百科全书文章作为第三个数据点:

页面 基准 阻止媒体 阻止媒体 + CSS 提取的元素
books.toscrape.com — 图像目录 337,692 B 111,970 B 76,237 B 20 / 20 / 20
quotes.toscrape.com — 文本列表 27,004 B 26,725 B 2,121 B 10 / 10 / 10
en.wikipedia.org — 文章 473,437 B 359,075 B 358,770 B 36 / 36 / 36

目录页面的重量有三分之二丢失在三类阻止列表中,进一步减少十个点给样式表——这是67%和77%的减少,对这样一个基准稳定的数字足以信任。

文本列表在阻止媒体时几乎没有变化,然后在样式表也被阻止时崩溃到2,121字节。这个数字仅是HTML文档本身,在所有阻止CSS的每个运行中都是相同的。请求的详细拆分解释了原因:页面发起了四个请求,其中三个是样式表,总计约24,500字节,相较于2,121字节的标记。这上面完全没有图像。

文章页面在媒体阻止时放弃四分之一,而在CSS上则没有可测量的变化——中位数移动305字节,而media+css运行的范围为29,000。在这个页面上,阻止样式表并没有带来节省。

提取在所有地方存活下来。20个产品、10个报价和36段文本在每个配置文件下都回来了,因此这些节省没有对任何字段造成损失。

时机值得单独提醒。每个配置文件在上面打印的运行中都比其基准快,这正是会让你承诺加速的结果。在中位数集上并未能保持:目录页面在media+css下耗时4.66秒,而基准为3.90秒,维基百科在media-only下耗时6.57秒,对比基准5.86秒——在传输更少时速度更慢。通过Python回调路由每个请求会增加每个请求的往返时间,而该开销与被阻止资产本应承载的字节数无关。将字节减少视为可靠的胜利,将延迟视为每个目标的副作用进行测量。

开始不需要卡——免费计划涵盖像这样的九次加载测量。

根据目标选择配置文件

该测量需要每个目标大约一分钟,并替代猜测工作:

  • 对一个代表性页面运行基准和一个阻止配置——应选择类别列表,而不是主页,因为资产组合因网站而异。
  • 默认阻止imagemediafont 这些从不被解析,缺点是有限的。
  • 单独测试stylesheet而不是假设。 这在某个页面上是最大的收获,而在另一个页面上则无关紧要。依赖布局的提取是需要检查的事项:与类和结构相关的选择器不受影响,但任何依赖计算几何或可见性的内容则不然。
  • 在需要渲染的页面上,绝不要阻止scriptdocument 客户端渲染的页面使用的是你将要丢弃的脚本构建内容。
  • 当目标重新设计时重新测量。 配置文件是针对资产组合进行调整的,而资产组合是会变化的。

在一个页面需要渲染但不需要交互的情况下,HTTP渲染调用完全避免了浏览器会话——抓取浏览器产品页面和连接文档涵盖了何时完整会话可以赚取其成本。

故障排除

启用路由后导航会超时。 处理程序中的代码路径结束时没有调用abortcontinue_。每个分支,包括异常路径,都必须解析路由。

被阻止的请求仍然出现在字节计数中。 Network.enable在注册page.route后发送,或在与被测量页面不同的CDP会话上。创建会话时需要从页面自己的上下文中进行,并在导航之前启用该域。
页面呈现为空,选择器不匹配任何内容。 scriptdocument 在阻止列表中。两者在客户端渲染的页面上都是必需的。

在不同的运行之间字节计数变化超过几个百分点。 页面在每次加载时未获取相同的资源。在这里测量的文本列表中,由样式表引用的字体二进制文件在某些加载中被提取,而在另一些加载中则没有,使基线从约26,700字节变化到约75,820字节。取多个运行的中位数,并比较绝对字节而非百分比,因为移动的基线扭曲比率。

看似节省的开支很大,但记录却丢失。 在信任配置文件之前,将提取的计数与基线进行比较,如上面的脚本所做的。一个减少字节和记录的配置文件不是优化。

结论

每个页面的带宽都会增加,与大多数成本不同,它可以在大约一分钟内进行测量。两行CDP生成一个数字,能够为特定目标的阻止列表提供理由,或不提供。

这里的数字反对的是一个默认设置。在某一页面阻止图像是决定性的,在另一页面中是适度的,而在第三个没有图像的页面中则无关紧要,而在这第三个页面上阻止样式表带来了最大的单一收益。在采用任何人的阻止列表之前,包括这个,先对自己的目标运行基线——并且要多次运行,因为这三页中的其中一页不会给出相同的答案两次。

准备好测量自己的目标了吗?创建一个免费的Scrapeless账号,导出你的密钥,并对你已经收集的页面运行脚本。计划限制在定价页面上,和代理配置涵盖了相同账单的出口部分。

常见问题解答

问:在抓取时哪个资源类型是安全的?

imagemediafont在几乎任何提取工作中都是安全的,因为没有解析器读取它们。stylesheet在你的选择器依赖于类和结构而非计算布局时是安全的,这涵盖了大多数抓取,并且在这些测量中是最大的单一节省。对任何构建客户端内容的页面保持scriptdocumentxhrfetch不变。

问:阻止图像实际节省了多少带宽?

这完全取决于页面,这就是为什么单一数字具有误导性。在这里测量的相同阻止列表在一个图像目录上节省了67%,在一篇百科全书文章上节省了大约四分之一,而在一个没有图像的单纯文本列表上基本没有节省。对自己的目标运行基线并比较绝对字节——一分钟的测量胜过任何发布的百分比。

问:阻止资源会加快抓取速度吗?

比减少字节的可靠性差。这里的每个配置文件传输的字节都少于其基线,但实际消耗的时间方向都在变化,因为每个请求都经过处理程序成本需要往返传输。在资源较少的页面上,这种开销可能会超过节省的部分,因此应将延迟视为需要验证的内容,而不是假设的内容。

问:我应该使用 page.route 还是 CDP Network.setBlockedURLs

page.route基于资源类型进行匹配,这通常是你想要的,并且保持逻辑在Python中,容易根据目标进行更改。CDP阻止命令匹配URL模式,因此更适合阻止特定已知主机——例如,一个分析端点——而不是整个资产类别。如果你同时需要两者,两个可以组合使用。

问:阻止资源会使页面的行为与真实浏览器不同吗?

是的,从某种意义上来说,永远不请求图像的会话与浏览器通常的行为不同。将阻止列表保持为提取实际所需的内容,如果目标在启用阻止后行为发生变化,请缩小列表并重新测量,而不是扩大它。

问:如何在没有CDP会话的情况下计数字节?
您可以通过在Playwright response 处理程序中求和响应体长度来近似,但这些是解压后的大小,而不是传输的字节,并且它们遗漏了失败的请求或从缓存中提供的请求。在页面内的替代方案是由W3C资源计时规范定义的 transferSize 属性,通过 performance.getEntriesByType("resource") 读取,这更接近正确的数量,但受到跨源限制,使其对于第三方资产为零。来自 Network.loadingFinishedencodedDataLength 是浏览器在网络上为每个请求记录的内容,无论其来源如何,因此值得多写这两行代码。

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

最受欢迎的文章

目录