返回博客
指南
Raluca PenciucLast updated on Apr 27, 20264 min read

Scrapy Playwright 教程:大规模抓取 JavaScript 负担沉重的网站

简而言之:Scrapy-Playwright 允许您通过 Playwright 控制真实的 Chromium、Firefox 或 WebKit 浏览器,从而在 Scrapy 爬虫内部直接渲染大量使用 JavaScript 的网页。本教程将带您逐步了解安装、配置、页面交互、AJAX 拦截、反检测以及生产就绪的项目结构,使您无需离开 Scrapy 生态系统即可抓取动态网站。

Scrapy 擅长高速爬取静态 HTML,但一旦目标网站通过 JavaScript 加载内容,标准的 Scrapy 请求就会返回一个空壳。这正是 Scrapy Playwright 解决的问题。它是一个 Scrapy 下载处理程序,将渲染任务委托给微软的浏览器自动化库 Playwright,因此您的爬虫收到的每个响应都包含完全渲染的 DOM。 如果您一直想在自己的项目中集成 Scrapy Playwright,却不确定各部分如何协同工作,本指南将涵盖每个步骤:从 pip install 到一个集成了项目、管道和反检测机制、可投入生产使用的蜘蛛。在此过程中,您将学习等待策略、AJAX拦截、无限滚动处理、代理配置,以及确保长时间爬取稳定的故障排除模式。

什么是 Scrapy-Playwright 以及为何使用它?

Scrapy-Playwright(PyPI 包 scrapy-playwright)是一款 Scrapy 下载处理器,它将默认的 HTTP 后端替换为由 Playwright 驱动的完整浏览器。当您在 Scrapy 请求中添加 "playwright": True 标记时, meta 字典中添加标签时,该处理器会启动浏览器页面,导航至该 URL,等待 JavaScript 执行完毕,然后将渲染后的 HTML 传递回您的 parse 回调函数,作为普通的 Scrapy Response.

这为何重要?越来越多的网页内容在客户端渲染:React 仪表盘、Vue 商店前端、受同意弹窗限制的页面,以及通过后台 API 调用加载产品数据的网站。标准的 Scrapy 仅获取初始 HTML 文档,其中通常包含占位符 <div> 标签和 JavaScript 包,却不包含您真正需要的数据。借助 Scrapy Playwright 的 JavaScript 渲染功能,您可以在不脱离 Scrapy 熟悉的请求/响应管道的情况下,获得与真实浏览器显示完全一致的输出。

何时应在请求中启用 Playwright?并非每个 URL 都需要全功能浏览器。一个实用的经验法则是:

  • 当所需数据存在于原始 HTML 中,或可通过已知的直接 API 端点获取时,请使用标准 Scrapy 请求
  • 当内容在页面加载后注入、需要点击或滚动才能显示数据,或者页面依赖于难以通过普通 HTTP 复现的 Cookie 和 JavaScript 重定向时,请使用 Playwright 请求

在单个爬虫中混合使用这两种模式非常简单(且值得推荐)。您只需为真正需要浏览器处理的请求支付额外开销,从而确保对其他页面的爬取速度保持高效。

Scrapy-Playwright 与 Scrapy-Splash 与 Scrapy-Selenium

为 Scrapy 选择浏览器渲染后端时,关键在于维护负担、浏览器还原度以及团队现有的工具链。以下是简要对比:

评估标准

Scrapy-Playwright

Scrapy-Splash

Scrapy-Selenium

浏览器引擎

Chromium、Firefox 或 WebKit

基于 Qt 的自定义渲染器

通过 WebDriver 调用 Chrome 或 Firefox

异步支持

原生(asyncio)

需要单独的 Splash 服务器

默认同步;提供异步封装

维护

积极维护,社区正在壮大

Splash的开发进度已放缓

稳定但依赖于 WebDriver 协议

JS 兼容性

支持所有现代浏览器

表现良好,但部分特殊情况会失败

支持所有现代浏览器

易于配置

pip install + playwright install

需要 Docker 容器

WebDriver 二进制文件管理

页面交互

丰富(click, fill, evaluate)

Lua 脚本功能有限

完整的 WebDriver API

如果您今天要启动一个新项目,Scrapy Playwright 通常是最佳选择。它提供了现代化的异步支持、一流的页面交互方法,并避免了运行独立渲染服务带来的运维开销。若想深入了解 Scrapy 与 Selenium 之间的权衡取舍,请参阅《Scrapy 与 Selenium 对比指南,该指南对此主题进行了详细阐述。

安装与项目设置

只需几条终端命令即可让 Scrapy Playwright 项目运行起来。以下是分步操作流程。

先决条件:您需要 Python 3.8 或更高版本,以及 pip。强烈建议使用虚拟环境以隔离依赖项。

# Create and activate a virtual environment
python -m venv venv
source venv/bin/activate  # Windows: venv\Scripts\activate

# Install Scrapy and scrapy-playwright
pip install scrapy scrapy-playwright

# Install browser binaries (Chromium is the default)
playwright install chromium

playwright install chromium 命令将下载 Playwright 内部管理的特定 Chromium 构建版本。您也可以安装 firefoxwebkit ,如果您的使用场景需要其他引擎。

接下来,创建一个新的 Scrapy 项目:

scrapy startproject myproject
cd myproject
scrapy genspider example example.com

这将生成标准的 Scrapy 目录结构: settings.py, items.py, pipelines.py, middlewares.py,以及一个 spiders/ 文件夹。剩下的唯一与 Playwright 相关的步骤是更新 settings.py,我们将在下一节中介绍。

有一点值得注意: scrapy-playwright 依赖于 Playwright 的异步 API,而该 API 又需要基于 asyncio基于 Twisted 的反应器。Scrapy 支持这一点,但你必须在 Scrapy 尝试使用其默认设置之前显式设置反应器。忘记这一步是开发者最常犯的安装错误。

为 Playwright 配置 Scrapy 设置

打开项目的 settings.py 并添加以下内容:

# settings.py

DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}

TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

# Optional: choose browser type (chromium, firefox, webkit)
PLAYWRIGHT_BROWSER_TYPE = "chromium"

# Optional: global navigation timeout in milliseconds
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000

DOWNLOAD_HANDLERS 字典告诉 Scrapy 将所有 HTTP 和 HTTPS 请求路由到 Playwright 处理程序。 TWISTED_REACTOR 行将 Scrapy 的事件循环切换为 asyncio,这是 Playwright 所必需的。

PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT 设置浏览器等待页面加载的最大时间(以毫秒为单位)。默认值为 30 秒,这对大多数网站来说已经足够。如果你正在抓取特别慢的页面,可以将其调高。如果你希望对无效 URL 快速失败,则将其调低。

另外两个值得了解的设置:

  • PLAYWRIGHT_LAUNCH_OPTIONS:直接传递给 playwright.chromium.launch()。可用于切换无头模式、指定可执行文件路径或配置全局代理。
  • PLAYWRIGHT_MAX_PAGES_PER_CONTEXT:限制在创建新上下文之前,有多少个页面共享同一个浏览器上下文。这有助于在大规模爬取时的内存管理。

配置好这些设置后,每个包含 "playwright": Truemeta 的每个 Scrapy 请求都将由 Playwright 渲染。未包含该标志的请求仍通过 Scrapy 的标准下载器处理,从而兼顾两者的优势。

渲染 JavaScript 密集型页面

现在让我们编写你的第一个 Scrapy Playwright 爬虫。目标是:访问一个通过 JavaScript 加载内容的页面,并从完全渲染的 DOM 中提取数据。

import scrapy

class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = ["https://quotes.toscrape.com/js/"]

    def start_requests(self):
        for url in self.start_urls:
            yield scrapy.Request(
                url,
                meta={"playwright": True},
                callback=self.parse,
            )

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
            }

关键代码行是 meta={"playwright": True}。这一标志会指示下载处理程序启动浏览器页面,导航至指定 URL,等待 load 事件,并将渲染后的 HTML 作为 TextResponse。在 parse中,你可以使用与任何 Scrapy 爬虫相同的 CSS 选择器(或 XPath)。解析方面没有任何变化。

使用 scrapy crawl quotes运行该蜘蛛,你应该能看到完整提取的引号,尽管该页面依赖 JavaScript 将其注入 DOM。如果你尝试使用标准 Scrapy 请求(不带 Playwright 标志)访问同一 URL, response.css("div.quote") 则会返回一个空列表。

此模式是本 Scrapy Playwright 教程中所有其他内容的基础。后续的每项技术都基于相同的 meta 字典,向浏览器传递额外指令。

页面交互:点击、滚动和表单提交

实际的网页抓取很少仅涉及加载页面。你通常需要点击按钮、填写搜索表单,或滚动页面以触发延迟加载的内容。Scrapy Playwright 的页面方法通过 playwright_page_methods request meta.

A PageMethod 是 Playwright 页面操作的封装。您传递一个操作列表,处理程序会在初始导航后按顺序执行每个操作。

点击按钮:

from scrapy_playwright.page import PageMethod

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("click", selector="button#load-more"),
            PageMethod("wait_for_selector", selector="div.new-content"),
        ],
    },
    callback=self.parse,
)

填写并提交表单:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("fill", selector="input#search", value="python scrapy"),
            PageMethod("click", selector="button[type=submit]"),
            PageMethod("wait_for_selector", selector="div.results"),
        ],
    },
    callback=self.parse,
)

滚动到页面底部:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod(
                "evaluate",
                "window.scrollTo(0, document.body.scrollHeight)",
            ),
            PageMethod("wait_for_timeout", 2000),
        ],
    },
    callback=self.parse,
)

请注意这种模式:您通过链式调用 PageMethod 调用以模拟真实的用户会话。处理程序会按顺序处理这些操作,因此顺序至关重要。在触发新内容的操作(如触发 API 调用的点击、加载更多项的滚动)之后,务必添加等待操作,以便在 Scrapy 捕获最终 HTML 之前,页面有时间完成更新。

一个实用技巧:尽量缩短 playwright_page_methods 列表尽可能简短。每次方法调用都会增加延迟。如果能通过更少的步骤实现相同结果(例如,直接导航至经过筛选的 URL 而不是填写表单),请优先选择更简单的方法。

动态内容的等待策略

选择正确的等待策略对于可靠地使用 Scrapy Playwright 抓取动态内容至关重要。等待时间过短会导致数据不完整;等待时间过长则会使爬取过程陷入停滞。

以下是主要方法:

wait_for_selector 这是最精确的选项。它会暂停执行,直到特定的 CSS 选择器出现在 DOM 中。

PageMethod("wait_for_selector", selector="div.product-list")

当您确切知道哪个元素标志着数据已加载时,请使用此方法。它之所以快速,是因为它会在元素出现的那一刻立即响应,而不是等待一个任意的时间段。

wait_for_load_state 等待特定的页面生命周期事件:

  • "load": 在初始 HTML 及所有资源(图片、样式表)加载完成后触发。
  • "domcontentloaded":在 HTML 解析完成时触发,此时图片尚未加载完毕。
  • "networkidle": 当至少 500 毫秒内没有网络连接时触发。
PageMethod("wait_for_load_state", "networkidle")

networkidle 虽然它能捕获大多数 AJAX 请求,但对于存在持久 WebSocket 连接、分析 ping 或广告追踪器(这些会持续占用网络资源)的页面,其可靠性可能较低。此外,它的响应速度通常比 wait_for_selector.

wait_for_timeout 是硬睡眠,以毫秒为单位指定。

PageMethod("wait_for_timeout", 3000)

这是最粗糙的工具。仅将其作为最后手段使用,例如在没有稳定选择器且 networkidle 表现不稳定时。硬等待在加载速度快的页面上会浪费时间,而在加载缓慢的页面上时长可能仍不足。

建议:尽可能默认使用 wait_for_selector 。当无法确定确切选择器时,再回退到 networkidle 。针对无法确定精确选择器的页面,请保留 wait_for_timeout 仅用于真正不可预测的页面,并尽可能将该值设得尽可能低。

处理无限滚动与分页

许多现代网站采用 Scrapy Playwright 的无限滚动模式或分页导航,将内容拆分为多个视图。在 Scrapy 中处理这两种情况需要略有不同的策略。

无限滚动通常的工作原理是滚动到页面底部,等待新项目加载,并重复此过程直到不再出现新项目。由于 playwright_page_methods 会在返回响应前运行一次,因此你需要在 page.evaluate 调用内部处理,或通过直接访问 Playwright 页面对象来处理。

最简洁的方法是使用 playwright_page meta键获取原始的Playwright页面,并自行编写循环脚本:

async def parse(self, response):
    page = response.meta["playwright_page"]
    previous_height = 0

    while True:
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        await page.wait_for_timeout(1500)
        current_height = await page.evaluate("document.body.scrollHeight")
        if current_height == previous_height:
            break
        previous_height = current_height

    # Re-read the fully scrolled page content
    content = await page.content()
    await page.close()

    sel = scrapy.Selector(text=content)
    for item in sel.css("div.item"):
        yield {
            "title": item.css("h3::text").get(),
        }

请注意,我们使用 await page.close()。这对内存管理至关重要;否则,浏览器页面会不断累积,导致进程内存激增。

分页(点击“下一页”或基于 URL)更为简单。如果网站使用查询参数(?page=2),只需生成 URL 递增的新 Scrapy 请求即可。如果依赖“下一页”按钮,则使用 PageMethod click:

def parse(self, response):
    # Extract data from current page
    for product in response.css("div.product"):
        yield {"name": product.css("h2::text").get()}

    # Follow next page if it exists
    next_button = response.css("a.next-page::attr(href)").get()
    if next_button:
        yield response.follow(
            next_button,
            meta={"playwright": True},
            callback=self.parse,
        )

对于仅使用 JavaScript 的“加载更多”按钮且不更改 URL 的网站,请将页面交互部分中的点击模式与 wait_for_selector ,以确认新项目已加载完毕再进行数据提取。

拦截 AJAX 请求

有时,最干净的数据源并非渲染后的 DOM,而是页面为填充内容而发出的后台 API 调用。Scrapy Playwright 的 AJAX 拦截功能允许您直接捕获这些响应,通常能直接获取结构化的 JSON 数据,无需进行任何 HTML 解析。

要拦截响应,您需要访问 Playwright 页面对象及其 response 事件:

import json

class AjaxSpider(scrapy.Spider):
    name = "ajax_products"
    captured_data = []

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/products",
            meta={
                "playwright": True,
                "playwright_include_page": True,
            },
            callback=self.parse,
        )

    async def parse(self, response):
        page = response.meta["playwright_page"]

        async def handle_response(resp):
            if "/api/products" in resp.url:
                body = await resp.json()
                self.captured_data.extend(body.get("items", []))

        page.on("response", handle_response)

        # Trigger the AJAX call (e.g., scroll or click)
        await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
        await page.wait_for_timeout(3000)
        await page.close()

        for product in self.captured_data:
            yield product

page.on("response", ...) 监听器会在每次网络响应时触发。您可以通过 URL 模式进行过滤,仅捕获您关心的 API 调用。响应正文已解析完毕(.json().text()),因此完全无需进行 DOM 遍历。

对于单页应用程序,此技术尤为强大——当您滚动页面时,前端会发起多次分页 API 请求。您无需解析复杂的 HTML,而是直接从源头获取干净、结构化的数据。

运行自定义 JavaScript 和截屏

Scrapy Playwright 提供了两项轻量级但实用的功能:自定义 JavaScript 执行和截图捕获。它们虽服务于不同目的,但共享相同的机制:直接访问 Playwright 页面对象。

使用 page.evaluate ,可在 Scrapy 读取 HTML 之前,提取深埋在 JavaScript 变量中的数据或操作页面状态:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod(
                "evaluate",
                "document.querySelectorAll('.popup-overlay')"
                ".forEach(el => el.remove())",
            ),
        ],
    },
    callback=self.parse,
)

这能在 Scrapy 解析页面之前移除弹出覆盖层,对于首次访问时会弹出模态框的网站非常实用。

使用 Scrapy Playwright 截屏功能有助于调试渲染问题。如果您的爬虫提取到空数据,截屏能准确展示浏览器所见的内容:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_page_methods": [
            PageMethod("screenshot", path="debug.png", full_page=True),
        ],
    },
    callback=self.parse,
)

full_page=True 参数会捕获整个可滚动区域,而不仅仅是视口。在开发过程中,您可以条件性地启用截图功能(例如,仅当解析回调发现零项时),以避免在生产环境爬取时占满磁盘空间。

中止非必要请求以加快爬取速度

每个浏览器页面默认都会加载图片、字体、CSS、分析脚本和广告追踪器。对于爬取而言,这些资源大多是冗余负担。屏蔽它们可以显著减少带宽消耗并加快页面加载速度。

Scrapy-Playwright 通过 PLAYWRIGHT_ABORT_REQUEST 设置支持请求拦截。您只需定义一个异步函数,该函数会检查每个请求,并返回 True 来中止请求:

# settings.py
PLAYWRIGHT_ABORT_REQUEST = "myproject.utils.should_abort"
# myproject/utils.py
from playwright.async_api import Request as PlaywrightRequest

async def should_abort(request: PlaywrightRequest) -> bool:
    blocked_types = {"image", "font", "stylesheet", "media"}
    if request.resource_type in blocked_types:
        return True
    blocked_domains = ["google-analytics.com", "doubleclick.net"]
    if any(domain in request.url for domain in blocked_domains):
        return True
    return False

仅屏蔽图片和字体就能显著缩短页面加载时间,在媒体资源密集的电商网站上效果尤为明显。但请务必注意,不要屏蔽负责渲染所需内容的 JavaScript 文件。如果启用请求屏蔽后数据消失,请将 "script" 重新加入允许的类型,并将过滤器范围缩小至特定域名。

在 Scrapy-Playwright 中使用代理

在大规模抓取时,轮换代理对于避免IP封禁至关重要。Scrapy Playwright的代理配置分为两个层级:全局配置和按请求配置。

全局代理适用于每个 Playwright 请求。请在 settings.py:

PLAYWRIGHT_LAUNCH_OPTIONS = {
    "proxy": {
        "server": "http://proxy-server:8080",
        "username": "user",
        "password": "pass",
    },
}

这会将代理配置传递给浏览器启动调用,因此该浏览器实例打开的每个页面都会通过该代理路由。

按请求配置的代理可提供更精细的控制。请在 playwright_context_kwargs 在请求中 meta 中,为单个请求分配不同的代理:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context_kwargs": {
            "proxy": {
                "server": "http://different-proxy:9090",
            },
        },
        "playwright_context": "proxy_context_1",
    },
    callback=self.parse,
)

每个唯一的 playwright_context 名称都会创建一个独立的浏览器上下文,拥有专属的代理、Cookie 和存储状态。这就是在代理池中轮换时隔离会话的方式。

对于生产环境中的爬取任务,建议使用通过单一接口管理代理轮换和验证码破解的服务,从而保持您的爬虫逻辑简洁。关键在于,Scrapy-Playwright 的代理支持足够灵活,能够与您选择的任何轮换策略集成。

反检测与隐身最佳实践

仅靠代理是不够的。现代反机器人系统会检查浏览器指纹、用户代理字符串和行为模式。以下是您应为 Scrapy Playwright 爬虫考虑的反检测层。

User-agent 轮换:为每个上下文设置一个真实且轮换的 User-agent:

import random

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 ...",
    # Add more real browser UA strings
]

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context_kwargs": {
            "user_agent": random.choice(USER_AGENTS),
        },
        "playwright_context": f"ctx_{random.randint(1, 100)}",
    },
    callback=self.parse,
)

指纹缩减:Playwright 的 Chromium 内核包含默认的 WebDriver 标志,这些标志会被反机器人脚本检测到。您可以通过以下方式降低指纹:

  • 传递 "args": ["--disable-blink-features=AutomationControlled"] 传入 PLAYWRIGHT_LAUNCH_OPTIONS.
  • 使用 page.evaluate 来删除 navigator.webdriver 属性。
  • 设置一个符合实际的视口大小,而不是默认的无头模式尺寸。

随机延迟:在请求之间添加抖动,可避免您的流量看起来像是以机器速度猛攻服务器的机器人。使用 Scrapy 的 DOWNLOAD_DELAY 设置并结合 RANDOMIZE_DOWNLOAD_DELAY:

DOWNLOAD_DELAY = 2
RANDOMIZE_DOWNLOAD_DELAY = True

隐身上下文配置:将上述所有内容整合为可复用的上下文配置。关于避免被封锁的全面指南,文章《网络爬虫资源:避免被封锁或IP封禁的技巧》涵盖了适用于Scrapy-Playwright之外的其他策略。

核心要点:将反检测视为多层防护体系,而非单一解决方案。代理服务器负责IP信誉管理;User-Agent轮换应对头部级检测;指纹缩减应对JavaScript级检测;延迟则应对行为级检测。所有这些措施必须协同工作。

浏览器上下文、会话与资源管理

在 Playwright 中,浏览器上下文browser context)是一个独立的浏览器会话,拥有专属的 Cookie、本地存储和缓存。Scrapy-Playwright 大量使用上下文,理解它们是管理大规模爬取资源的关键。

默认情况下,每个未指定 playwright_context name 的 Scrapy-Playwright 请求都会共享默认上下文。这意味着 Cookie 会在不同请求间保留,对于需要保持登录状态的网站这没有问题,但若希望每个请求都有独立的会话,则会引发问题。

命名上下文可实现会话隔离:

yield scrapy.Request(
    url,
    meta={
        "playwright": True,
        "playwright_context": "session_a",
    },
    callback=self.parse,
)

所有标记为 "session_a" 的请求将共享 Cookie 和状态。标记为 "session_b" 的请求则拥有完全独立的会话。这对于需要模拟多个独立用户的并行抓取工作流非常有用。

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT 控制单个上下文中可同时打开的页面数量。当达到限制时,将创建一个新的上下文。调整此设置有助于防止内存膨胀:

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT = 4

内存管理提示:

  • 使用 playwright_include_page。若您忘记 await page.close()parse 方法中忘记,页面会不断累积,内存使用量将随请求数量呈线性增长。
  • 请使用 CONCURRENT_REQUESTS 来限制并行度。浏览器对资源消耗较大;在配备 8 GB 内存的机器上,8 到 16 个并发 Playwright 请求是一个合理的起点。
  • 在测试运行期间监控爬虫的 RSS 内存。如果内存持续攀升,请检查是否有未关闭的页面或过多的上下文创建。

对于更普遍的无头浏览器抓取工作流,关于使用 Python 运行无头浏览器的指南中讨论了资源模式,这些内容是对本文所涵盖内容的补充。

故障排除与错误处理

即使是配置良好的 Scrapy Playwright 爬虫,在大规模运行时也可能失败。以下是最常见的问题及可行的解决方案。

TimeoutError:这是您最常遇到的错误。它表示浏览器未能在允许的时间内完成导航或等待操作。

  • 对于加载缓慢的网站,请增加 PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT
  • 对于 networkidle wait_for_selector 以避免在持久连接上卡住。
  • 检查目标网站是否正在阻止您(超时页面的截图通常会显示验证码或阻止页面)。

浏览器断开连接:如果浏览器进程在爬取过程中崩溃,您将看到 BrowserErrorConnection closed 异常。

  • 减少 CONCURRENT_REQUESTS。并行页面过多会耗尽系统内存并导致浏览器崩溃。
  • PLAYWRIGHT_MAX_PAGES_PER_CONTEXT 设置为较低的数值。
  • 添加 "args": ["--disable-dev-shm-usage"]PLAYWRIGHT_LAUNCH_OPTIONS 在 Docker 中运行时, /dev/shm 通常过小。

内存泄漏:在长时间爬取过程中,爬虫的内存占用会逐渐增加。

  • 请确认您已关闭所有通过 playwright_include_page获取的所有页面。每个未关闭的页面都会在内存中保留完整的 DOM。
  • 限制 PLAYWRIGHT_MAX_PAGES_PER_CONTEXT 并定期重启上下文。
  • 使用 CLOSESPIDER_PAGECOUNT 或自定义扩展,在达到阈值后重启爬虫。

错误回调模式:使用 Scrapy 的 errback 来优雅地处理失败,而不是让它们导致蜘蛛崩溃:

yield scrapy.Request(
    url,
    meta={"playwright": True, "playwright_include_page": True},
    callback=self.parse,
    errback=self.handle_error,
)

async def handle_error(self, failure):
    page = failure.request.meta.get("playwright_page")
    if page:
        await page.close()
    self.logger.error(f"Request failed: {failure.request.url}")

关键细节:如果你请求了 playwright_include_page,则必须在回调和错误回调中都关闭该页面。否则,失败的请求会导致页面对象泄漏。将错误回调与 Scrapy 的内置 RETRY_TIMES 设置,可在放弃前自动重试瞬时故障。

使用跟踪信息进行调试:Playwright 支持跟踪记录功能,可捕获网络请求、DOM 快照和操作的完整时间线。在开发过程中通过 PLAYWRIGHT_LAUNCH_OPTIONS 在开发过程中启用此功能,可精确重现浏览器在问题页面上的操作。

构建生产就绪的爬虫

教程通常在展示如何提取数据后就结束了。但在生产环境中,你需要一个包含项、管道、中间件以及经过精细调优的设置的完整项目结构。以下是关于如何将所有组件整合到一个 Scrapy Playwright 项目中的方法。

定义您的 Item:

# items.py
import scrapy

class ProductItem(scrapy.Item):
    name = scrapy.Field()
    price = scrapy.Field()
    url = scrapy.Field()

使用 Item 类(或 dataclass 新版 Scrapy 中的 items) 不仅能进行模式验证,还能让管道代码比直接传递原始字典更简洁。

编写用于验证和存储的项管道:

# pipelines.py
class ValidateProductPipeline:
    def process_item(self, item, spider):
        if not item.get("name"):
            raise scrapy.exceptions.DropItem("Missing name")
        item["price"] = float(item["price"].replace("$", "").strip())
        return item

class JsonWriterPipeline:
    def open_spider(self, spider):
        import json
        self.file = open("products.jsonl", "w")

    def close_spider(self, spider):
        self.file.close()

    def process_item(self, item, spider):
        import json
        self.file.write(json.dumps(dict(item)) + "\n")
        return item

生产环境配置检查清单:

# settings.py (additions for production)
ITEM_PIPELINES = {
    "myproject.pipelines.ValidateProductPipeline": 100,
    "myproject.pipelines.JsonWriterPipeline": 200,
}

CONCURRENT_REQUESTS = 8
DOWNLOAD_DELAY = 1.5
RANDOMIZE_DOWNLOAD_DELAY = True
RETRY_TIMES = 3
LOG_LEVEL = "INFO"

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT = 4
PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 30000

生产就绪的模式是:结构化项流经验证管道,配置将并发数限制在您的机器和目标网站可处理的范围内,重试逻辑配合错误回调可捕获瞬时故障。Scrapy 内置的统计收集器无需额外代码即可提供每次爬取的指标(已抓取项、错误、重试次数)。

对于希望先掌握 Scrapy 网页抓取基础知识,再叠加 Playwright 的团队,Scrapy 网页抓取指南提供了坚实的基础。

关键要点

  • 有选择地启用 Playwright。仅当页面确实需要 JavaScript 渲染时,才使用 "playwright": True 标签;其余情况均使用标准 Scrapy 请求,以确保爬取速度。
  • 使用 wait_for_selector 而非 networkidle 或硬等待。对于大多数动态内容场景,基于选择器的等待速度更快且更可靠。
  • 尽可能拦截 AJAX 请求。捕获后台 API 响应可获得干净的 JSON 数据,并避免易受干扰的 DOM 选择器。
  • 分层反检测:代理、用户代理轮换、指纹缩减和随机延迟应协同工作,而非相互替代。
  • 关闭所有已打开的页面。未关闭的 Playwright 页面导致的内存泄漏,是长期运行的 Scrapy Playwright 爬取任务不稳定的最常见原因。

常见问题

Scrapy-Playwright 是否支持 Firefox 和 WebKit,还是仅支持 Chromium?

是的,这三种引擎均受支持。请设置 PLAYWRIGHT_BROWSER_TYPE"firefox""webkit" ,并运行 playwright install firefox (或 webkit) 即可下载对应的浏览器二进制文件。Chromium是默认选项且经过最广泛的测试,但对于那些专门检测Chromium指纹的网站,Firefox可能会派上用场。

如何修复 Scrapy-Playwright 中的 TimeoutError 异常?

首先将 PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT 默认的 30 秒。如果超时问题仍然存在,请将等待策略从 networkidle wait_for_selector ,以定位特定元素。同时请截取失败页面的屏幕截图,以确认网站是否返回了验证码或阻止页面,而非预期内容。

能否以 headful(可见浏览器)模式运行 Scrapy-Playwright 进行调试?

可以。添加 "headless": FalsePLAYWRIGHT_LAUNCH_OPTIONSsettings.py。浏览器窗口将可见地打开,让你能够实时观察每次导航和交互。这对调试页面-方法序列非常有价值。请记住,在运行生产环境爬取之前,务必切换回无头模式。

Scrapy-Playwright 占用多少内存?如何降低内存消耗?

每个 Chromium 页面根据页面复杂度不同,大约会消耗 50 到 150 MB 的内存。要减少内存占用,请降低 CONCURRENT_REQUESTS,将 PLAYWRIGHT_MAX_PAGES_PER_CONTEXT 为较小数值(3 至 5),停用不必要的资源类型(图片、字体、样式表),并在 callback 和 errback 方法中始终显式关闭页面。

Scrapy-Playwright、Scrapy-Splash 和 Scrapy-Selenium 之间有什么区别?

Scrapy-Playwright 采用 Playwright 的现代异步 API,支持 Chromium、Firefox 或 WebKit。Scrapy-Splash 依赖于一个独立的基于 Docker 的渲染服务,交互性有限。Scrapy-Selenium 封装了较旧的 WebDriver 协议。对于新项目,Scrapy-Playwright 通常在浏览器还原度、异步性能和积极维护方面提供了最佳组合。

结论

Scrapy Playwright 弥合了 Scrapy 强大的爬取引擎与当今由 JavaScript 驱动的 Web 现实之间的鸿沟。只需在请求中添加一个元标志,您即可获得完整的浏览器渲染效果,同时无需放弃 Scrapy 的管道、中间件和并发模型。本教程涵盖了从初始设置和配置,到页面交互、AJAX 拦截、反检测以及生产环境强化等全套内容。

本文介绍的技术应能应对绝大多数动态抓取场景。对于那些因大规模管理浏览器基础设施、代理轮换和反检测而导致瓶颈(而非抓取逻辑本身)的项目,我们的 Scraper API 通过单一端点处理这些底层工作,让您能够专注于数据本身,而非基础架构。

无论您选择哪种方法,核心原则始终如一:仅在必要时使用浏览器渲染,保持爬虫结构清晰,并关闭所有已打开的页面。

关于作者
Raluca Penciuc, 全栈开发工程师 @ WebScrapingAPI
Raluca Penciuc全栈开发工程师

Raluca Penciuc 是 WebScrapingAPI 的全栈开发工程师,主要负责开发爬虫、优化规避机制,并探索可靠的方法以降低在目标网站上的被检测概率。

开始构建

准备好扩展您的数据收集规模了吗?

加入2,000多家企业,使用WebScrapingAPI在无需任何基础设施开销的情况下,以企业级规模提取网络数据。