跳至内容
返回博客

JavaScript 与 Scrapy:数据优先,仅在需要时渲染

Mihai Maxim最后更新于 2 min read
JavaScript 与 Scrapy:数据优先,仅在需要时渲染

开始提取

免费试用 WebScrapingAPI

领取 1,000 个免费 API 积分。无需信用卡。

开始使用
简而言之:在 Scrapy 中使用 JavaScript 并不总是需要浏览器。首先检查原始响应、网络请求和嵌入式页面状态;仅当目标数据确实依赖于浏览器执行时,才添加 scrapy-playwright,然后有针对性地控制等待、交互、清理和并发。

在 Scrapy 中使用 JavaScript,意味着对于那些所需数据只有在客户端代码运行后才会出现的页面,需要添加浏览器执行环节。实际目标并非渲染每个页面,而是找出获取数据的成本最低且最可靠的途径:原始 HTML、JSON 请求、嵌入的 JavaScript 对象,或者仅在必要时使用浏览器构建的 DOM。

这一区别至关重要,因为 Scrapy 的下载器接收的是服务器的响应,而 Chrome 或 Firefox 可能会在此之后发出额外请求并修改文档。因此,一个 React、Vue 或 Angular 页面在开发者工具中可能看起来已经完整,尽管 response.text 实际上仅包含一个应用程序外壳。

本指南首先从“浏览器最后”的诊断方法入手,随后针对真正需要执行的请求构建一个针对性的 scrapy-playwright 工作流。 您还将了解如何使用基于条件的等待、处理点击和会话状态、比较渲染选项,以及在生产环境中保持动态爬虫的稳定性。最终形成的工作流将在可能的情况下尽可能保留 Scrapy 高效的 HTTP 管道,同时不会假装每个现代网站仅凭静态标记就能被爬取。

在渲染前定位数据源

在尝试使用 Scrapy 执行 JavaScript 之前,请先检查服务器已返回的内容。Scrapy 关于动态内容的官方指南建议首先定位底层数据源。请按以下顺序操作:原始响应、网络请求、嵌入式状态,最后才是无头浏览器。

将 Scrapy 的响应与渲染后的 DOM 进行比较

保存或查找 response.text ,例如在浏览器中可见的独特值(如产品 ID,而非格式化的价格)。然后将其与开发者工具的“元素”面板进行对比。如果该值仅存在于渲染后的 DOM 中,很可能是 JavaScript 插入的,但这仍不能证明必须进行渲染。应用程序可能已从可访问的端点获取了相同的值。

一份更全面的《使用 Scrapy 进行网络爬取指南》可作为此处的内部参考,尤其适用于响应检查、选择器以及请求调试。

重放 XHR 或 fetch 请求

打开开发者工具,选择“网络”选项卡,刷新页面,并按 Fetch/XHR 进行过滤。检查候选响应,直到找到所需的字段。记录 URL、HTTP 方法、查询字符串或请求正文、相关头部,以及与该会话相关的任何 Cookie 或令牌。

在 Scrapy 中重现最简有效的请求,而非复制每个浏览器头:

def parse(self, response):
    yield scrapy.Request(
        "https://example.com/api/products?page=1",
        headers={"Accept": "application/json"},
        callback=self.parse_products,
    )

def parse_products(self, response):
    payload = response.json()
    for row in payload["results"]:
        yield {"id": row["id"], "name": row["name"]}

此方法可避免 DOM 时间消耗、浏览器内存占用以及视觉选择器的变更。如果端点使用分页、光标值或 POST 数据,请直接遵循这些参数。涉及身份验证时,请保留所需的 Cookie 或令牌流,而不是硬编码短效凭据。

解析 JSON 及脚本中嵌入的数据

有时初始 HTML 中的 <script> 元素中包含应用程序状态。使用 CSS 或 XPath 选择器提取该脚本。当内容为有效 JSON 时,请使用 json.loads() ;当内容为 chompjs 当内容采用 JavaScript 对象语法(包含未加引号的键、尾随逗号或类似差异)时使用。

import chompjs

script = response.css("script[data-page-state]::text").get()
state = chompjs.parse_js_object(script) if script else {}
items = state.get("products", [])

建议优先使用结构化解析器,而非泛用的正则表达式。正则表达式虽能隔离边界明确的赋值语句,但在处理嵌套对象和转义字符串时容易出现问题。将直接的 JSON 请求与 script 标签解析相结合,通常可在不启动浏览器的情况下解决 Scrapy 中的动态内容问题。

使用 scrapy-playwright 添加 JavaScript 渲染

当数据依赖于仅在浏览器中才有的行为(例如应用程序代码、Web API 或交互驱动的状态)时,请有选择地添加渲染功能。此“Scrapy 与 JavaScript 结合”示例将 scrapy-playwright 作为主要集成路径,同时保留 Scrapy 标准下载器处理的常规请求。

安装并配置集成

一种常见的配置模式是将插件和 Playwright 浏览器二进制文件安装在同一项目环境中:

python -m pip install scrapy-playwright
python -m playwright install chromium

将 asyncio 反应堆和 Playwright 下载处理程序添加到 settings.py:

TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

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

PLAYWRIGHT_BROWSER_TYPE = "chromium"

在可部署项目中锁定兼容的依赖版本,并在开发和生产环境中使用相同的锁定文件。提供的示例无法验证插件当前的配置范围,因此在将此模板视为可发布版本之前,请先查阅项目的最新文档。

渲染一个请求并使用 Scrapy 选择器进行解析

仅标记需要浏览器的请求。通过有意义的选择器等待,可确保在目标组件出现后,回调函数才能接收到 HTML:

import scrapy
from scrapy_playwright.page import PageMethod

class ProductsSpider(scrapy.Spider):
    name = "products"

    def start_requests(self):
        yield scrapy.Request(
            "https://example.com/products",
            meta={
                "playwright": True,
                "playwright_page_methods": [
                    PageMethod(
                        "wait_for_selector",
                        "[data-product-card]",
                    )
                ],
            },
        )

    def parse(self, response):
        for card in response.css("[data-product-card]"):
            yield {
                "name": card.css("[data-name]::text").get(),
                "price": card.css("[data-price]::text").get(),
            }

回调函数仍会收到 Scrapy 响应,因此现有的 CSS 和 XPath 提取模式依然适用。请将静态分类页面、API 调用和资源保留在常规请求中。当您需要处理多个上下文、页面事件或更深入的项目配置时,针对 JavaScript 密集型网站的专用 Scrapy-Playwright 教程将是您自然而然的下一份内部参考资料。

等待并交互动态内容

导航事件的完成并不意味着应用程序数据已准备就绪。只有当爬虫等待与计划提取的字段相关的状态时,Scrapy 的 JavaScript 渲染才会变得可靠。

优先采用基于状态的等待而非固定延迟

等待稳定的选择器、已知的 API 响应、URL 跳转或应用程序标志。例如,选择器 [data-results-loaded="true"] 比“休眠三秒”更能准确表达意图。固定延迟在运行缓慢时可能过短,而在运行快速时则会浪费时间。

请使用有上限的超时设置,并记录失败的具体条件。如果不存在稳定的信号,短暂的固定等待可以作为备选方案,但需为整个浏览器请求设置上限。这样,Scrapy 因 JavaScript 失败而产生的等待就会变得可观察,而不是让页面无限期保持打开状态。

点击、滚动、分页及保持会话状态

页面方法可以在响应返回之前执行受控交互:

bplaywright_page_methods": [
    PageMethod("click", "button.load-more"),
    PageMethod("wait_for_selector", "[data-page='2']"),
]

对于无限滚动,仅在项目计数增加时重复滚动操作,并在经过预定义次数的未变化循环后停止。对于分页,若网站提供数据请求则优先使用;否则点击“下一页”控件,并等待页码标记或首行 ID 发生变化。

将相关的已认证请求保留在同一命名浏览器上下文中,以便 Cookie 和本地存储能够持久化。使用 playwright_include_page=True,应通过 finally 块中关闭当前活动页面。专用上下文应在最后一次会话请求后关闭,而非在其他请求仍依赖于它时关闭:

async def parse_last_session_page(self, response):
    page = response.meta["playwright_page"]
    context = page.context
    try:
        return {"title": await page.title()}
    finally:
        await page.close()
        await context.close()

由于包含和清理 API 可能会发生变化,请在部署前确认当前的请求元数据和生命周期规则。页面泄漏最终会耗尽浏览器的页面限制,导致原本正确的 Scrapy JavaScript 爬虫看似卡死。

比较 Scrapy JavaScript 选项

最佳的集成方案取决于您是否需要真实浏览器、流程所需的交互程度,以及由谁来运营渲染基础设施。请勿仅凭首次演示看起来多么简便就做出选择。

Playwright、Selenium、Splash 及托管式渲染 API

选项

最适合

主要权衡

scrapy-playwright

在 Scrapy 项目中实现有选择性的浏览器渲染,包括现代交互功能

浏览器CPU和内存开销、异步生命周期管理以及受版本影响的集成细节

基于 Selenium 的集成

已建立 WebDriver 自动化或测试代码的团队

驱动程序和浏览器管理、围绕 Scrapy 的更多衔接工作,以及必须针对所选包进行兼容性检查

基于 Splash 的集成

HTTP 渲染服务或现有的基于 Lua 的工作流

需要运营一个独立的服务,且 JavaScript 行为可能与当前完整版浏览器存在差异

托管式渲染API

将浏览器、代理和重试基础设施卸载

供应商特有的限制、超时机制、并发性、定价以及本地控制权较少

若您已维护 WebDriver 代码,针对 Scrapy 与 Selenium 的专项对比分析将有所帮助。当您的架构倾向于采用独立渲染服务时,Scrapy Splash 教程则更为实用。对于结合 Scrapy 使用的 JavaScript,请选择既能满足最低交互要求,又能让简单请求无需依赖浏览器的方案。

渲染并不能保证完全防止机器人。完整浏览器仍可能收到 403 响应、验证码、速率限制或账户验证,因此请将访问控制与 DOM 执行分开评估。

在生产环境中可靠地运行渲染型爬虫

Scrapy 配合无头浏览器运行时,速度比普通 HTTP 请求慢,且资源消耗更大。生产环境的可靠性取决于限制浏览器工作量、控制故障范围以及监测资源使用情况。

控制成本、并发量和浏览器资源

仅对已证实需要渲染的 URL 进行渲染。在解析器开发期间缓存具有代表性的页面,谨慎屏蔽不必要的媒体内容,并对浏览器上下文和页面设置保守的限制。在监控内存、CPU、超时和打开的页面同时,逐步提高并发度。

为导航和加载后等待分别设置超时预算。对暂时的网络或浏览器进程故障进行重试,但需限制重试次数,并避免对确定性故障(如被移除的选择器)进行无限次重试。 在成功或发生异常时关闭页面,并回收状态异常的浏览器工作线程。这些控制措施可防止使用 Scrapy 的 JavaScript 将单个缓慢目标演变为影响整个爬虫的背压。

《避免被封的网络爬取指南》是一份有用的内部参考资料,涵盖了标头、请求速率、代理和访问诊断等内容,这些与渲染正确性是分开的。

排查内容缺失、加载缓慢或被封锁的问题

症状

下一步检查

该字段在浏览器中存在但 response.text

在添加渲染之前,查找 Fetch/XHR 请求或嵌入的脚本

渲染后的响应仍缺失数据

等待特定字段的选择器或网络条件,然后根据最终的DOM验证该选择器

首页正常,后续页面为空

保留 Cookie、令牌、上下文状态和分页参数

爬虫速度随时间推移逐渐变慢

统计打开的页面和上下文数量,确认清理操作,并降低渲染并发度

反复出现超时

将导航与应用程序等待分离,捕获诊断信息,并限制重试次数

403 错误或 CAPTCHA 验证

将其视为访问控制问题,而非 JavaScript 失败的证据

当选择器失效时,保存渲染后的 HTML 并将其与手动测试所用的浏览器会话进行对比。如果响应被阻塞,请在更改等待逻辑之前检查状态码和挑战页面。这种“症状到行动”的循环确保调试基于证据,而不是对每个请求都增加更长的等待时间。

关键要点

  • 在添加浏览器之前,先查找原始 HTML、JSON 端点和嵌入的应用程序状态。
  • 仅在需要执行的请求上启用 scrapy-playwright,随后继续对渲染后的响应使用 Scrapy 选择器。
  • 等待可观察的页面状态,限制超时时间,并关闭所有包含的页面或专用上下文。
  • 将渲染、缩放和反机器人访问视为需要不同诊断方法的独立工程问题。

常见问题

一个 Scrapy 爬虫能否将标准请求与 JavaScript 渲染的请求混合使用?

可以。将常规 scrapy.Request 对象保留在默认下载器中,仅对需要浏览器的 URL 添加渲染元数据。这两种请求类型均可 feeding 到同一项处理管道中。这种选择性模型在保持 Scrapy 对静态页面吞吐量的同时,允许较小的一部分请求使用浏览器执行。

如何使用 scrapy-playwright 运行自定义 JavaScript 或点击某个元素?

对于可在回调之前执行的操作(例如点击或 evaluate 调用。对于条件逻辑或自定义 JavaScript 返回的值,请将实时页面对象包含在响应元数据中,并使用异步回调。务必在 finally中关闭该页面,即使在数据提取过程中抛出异常时也不例外。

如何在调试时捕获屏幕截图或检查渲染后的 HTML?

将 Playwright 页面包含在响应中,然后在 page.screenshot(path="debug.png", full_page=True) 。当选择器行为不明确时,请将 await page.content() 与截图一同保存。使用包含请求 ID 的唯一文件名,并避免在大规模场景下持续启用这些诊断功能,因为截图会增加 I/O 和存储开销。

无头浏览器能否避免 403 响应、验证码或封禁?

不能。无头浏览器虽然能执行 JavaScript,但网站仍可评估 IP 信誉、请求频率、Cookie、账户行为、浏览器指纹以及导航模式。请分别诊断返回的状态码和验证内容。渲染页面虽能使其功能正常,但并不能使流量获得信任或授权。

scrapy-playwright 可以在 Docker 或 CI 环境中运行吗?

可以。容器必须包含所选浏览器的二进制文件及其操作系统依赖项,且 Playwright 版本应与您安装的 Python 包版本一致。请锁定依赖项,在不使用显示服务器的情况下测试镜像,提供足够的共享内存,并在启动完整爬虫之前运行一个小型渲染烟雾测试。

结论:Scrapy 与 JavaScript 的实际应用

处理动态页面的可靠方法是遵循从最简单到最复杂的步骤。将原始 Scrapy 响应与渲染后的 DOM 进行对比,定位提供缺失字段的请求或嵌入式状态,并在可能的情况下直接解析结构化数据。只有在此之后,才应添加浏览器执行。

当必须进行渲染时,请保持选择性。等待与数据相关的状态,而非任意延迟;仅在工作流需要时保留会话上下文;并以可预测的方式关闭页面和上下文。 在生产环境中,浏览器的并发性、超时限制、重试机制和资源指标与选择器同样重要。一个在笔记本电脑上能正确渲染的浏览器,在规模扩大时仍可能成为爬虫的瓶颈,或遭遇访问控制问题。

如果阻塞点出现在请求层(而非交互逻辑),WebScrapingAPI 提供了一个 Scraper API,该 API 在后台处理代理轮换和验证码识别的同时,返回原始 HTML。这使您能够保留 Scrapy 的解析和项处理管道,同时将不稳定的检索层卸载出去。 请从最简单且可验证的路径开始,对其进行监控,并仅在证据表明确实需要时才增加浏览器的复杂性。

关于作者

Mihai Maxim, 全栈开发工程师 @ WebScrapingAPI

Mihai Maxim

全栈开发工程师

米海·马克西姆(Mihai Maxim)是 WebScrapingAPI 的全栈开发工程师,他在产品各领域均有贡献,并协助为该平台构建可靠的工具和功能。

开始构建

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

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