返回博客
指南
Ștefan RăcilăLast updated on Apr 29, 20264 min read

Scrapy Splash 教程:渲染 JavaScript 页面

Scrapy Splash 教程:渲染 JavaScript 页面
简而言之:Scrapy Splash 将 Scrapy 的快速爬取引擎与 Splash 无头浏览器相结合,用于渲染大量使用 JavaScript 的网页。本 Scrapy Splash 教程将带您逐步了解 Docker 环境搭建、Scrapy 项目配置、SplashRequest 基础知识、用于滚动和点击的 Lua 脚本、代理集成,以及如何解决您可能会遇到的常见错误。

Scrapy 是 Python 生态系统中最高效的 Web 爬取框架之一,但它有一个众所周知的盲点:无法执行 JavaScript。对于通过客户端渲染、AJAX 调用或单页应用框架加载数据的网站,原生 Scrapy 爬虫是无法识别的。这正是本 Scrapy Splash 教程要解决的问题。

Scrapy Splash 是 Scrapy 与 Splash 无头浏览器之间的集成层。Splash 是由 Zyte(即 Scrapy 的开发团队)开发的一款基于 Qt 的轻量级渲染服务,它提供了一个 HTTP API。与运行完整的桌面浏览器不同,Splash 会在精简版的 WebKit 引擎中加载页面,执行 JavaScript,并将完全渲染好的 HTML 返回给您的爬虫。 您的解析方法仍可照常使用标准的 CSS 和 XPath 选择器,仿佛一切如常。

在本指南中,您将从零开始搭建 Docker 和 Splash,配置 Scrapy 项目,编写可渲染动态页面的爬虫,创建用于高级交互的 Lua 脚本,配置代理,并解决让大多数新手感到困扰的常见错误。

什么是 Scrapy Splash 以及何时应该使用它?

Splash 是一款带有 HTTP API 的无头浏览器,用于渲染通过 JavaScript 加载的网页。与功能齐全的浏览器不同,Splash 设计上更轻量级:它在 Docker 容器内启动,监听特定端口,并通过 HTTP 返回渲染后的 HTML(或 PNG 截图、HAR 日志)。Scrapy Splash 是连接 Scrapy 与该渲染服务的官方库。

当目标网站通过 JavaScript 或 AJAX 加载关键内容,且您需要 Scrapy 的内置管道、中间件和爬取管理功能时,请选用 Scrapy Splash。Zyte 专门为爬取工作流构建了 Splash,它能无缝集成到 Scrapy 的请求/响应生命周期中。不过需要注意的是,Splash 采用的是较旧的 WebKit 引擎,因此其 JavaScript 支持不如基于 Chromium 的替代方案那么现代化。 如果目标网站依赖前沿的浏览器 API,请评估采用 Chromium 后端的无头浏览器工具。

对于大规模的简单 JS 渲染(产品页面、目录列表、分页结果),Splash 依然是一个实用且资源高效的选项。

先决条件与环境配置

在深入学习本 Scrapy Splash 教程之前,请确认您已具备以下条件:

  • 系统已安装 Python 3.7 及以上版本
  • 一个 Scrapy 项目(或计划创建一个)
  • 用于运行 Splash 容器的 Docker Desktop(或 Linux 上的 Docker 引擎)
  • 用于运行 Docker 和 Scrapy CLI 命令的终端

以上就是全部要求。下一节将介绍 Docker 的安装。

安装 Docker 并运行 Splash 容器

Splash 在 Docker 容器内运行,因此需要先安装 Docker。请下载适用于 macOS 或 Windows 的 Docker Desktop,或在 Linux 上直接安装 Docker 引擎。Docker 运行后,拉取官方 Splash 镜像:

docker pull scrapinghub/splash

然后启动容器:

docker run -it -p 8050:8050 --rm scrapinghub/splash

这将容器的 8050 端口映射到主机的 8050 端口。 --rm 参数可在停止容器时将其移除。

http://localhost:8050/ 。若看到默认的 Splash 交互页面,则表示服务已启动。在编写任何 Scrapy 代码之前,请直接在此处测试一个 URL 以确认渲染功能正常。

对于面向生产环境的 Scrapy Splash Docker 配置,请考虑资源限制。 --max-timeout 标志可让您延长默认的 60 秒超时(除非您进行覆盖,否则最大值约为 90 秒,但您应根据当前 Splash 文档核实确切值,因为具体情况可能有所不同)。使用 Docker 的 --memory 参数限制内存,以防止失控的页面耗尽主机资源。

为 Splash 配置您的 Scrapy 项目

如果您还没有项目,请创建一个:

scrapy startproject myproject

安装 scrapy-splash 插件:

pip install scrapy scrapy-splash

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

SPLASH_URL = 'http://localhost:8050'

DOWNLOADER_MIDDLEWARES = {
    'scrapy_splash.SplashCookiesMiddleware': 723,
    'scrapy_splash.SplashMiddleware': 725,
    'scrapy.downloadermiddlewares.httpcompression.HttpCompressionMiddleware': 810,
}

SPIDER_MIDDLEWARES = {
    'scrapy_splash.SplashDeduplicateArgsMiddleware': 100,
}

DUPEFILTER_CLASS = 'scrapy_splash.SplashAwareDupeFilter'

SPLASH_URL 用于告知 scrapy-splash 渲染服务的位置。 SplashCookiesMiddleware 负责处理 Scrapy 与 Splash 之间的 Cookie 转发。 SplashMiddleware 拦截 SplashRequest 对象,并通过 Splash HTTP API 进行路由。自定义 DUPEFILTER_CLASS 确保 Scrapy 的重复请求过滤器能识别 Splash 特有的参数,从而避免因渲染参数不同而意外过滤请求。

完成这些配置后,您的项目便已准备就绪,可用于构建后续的任何 Scrapy Splash 教程蜘蛛。

您的第一个 Scrapy Splash 教程蜘蛛:SplashRequest 实战

生成蜘蛛骨架:

scrapy genspider quotes_js quotes.toscrape.com

替换默认的 start_urls 模式替换为 start_requests,因为 Scrapy 的默认请求类不会通过 Splash 进行路由:

import scrapy
from scrapy_splash import SplashRequest

class QuotesJsSpider(scrapy.Spider):
    name = 'quotes_js'

    def start_requests(self):
        yield SplashRequest(
            url='http://quotes.toscrape.com/js/',
            callback=self.parse,
            args={'wait': 2}
        )

    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(),
            }

与标准 scrapy.Request 的关键区别在于,SplashRequest 会先将 URL 发送给 Splash。Splash 会渲染页面,等待指定秒数以执行 JavaScript,并返回完全渲染的 HTML。在 parse中,您可以像往常一样处理响应:CSS 选择器、XPath 等均可正常工作,因为 Splash 的响应包含所有标准的响应属性。

使用 scrapy crawl quotes_js ,你应该会在输出中看到已渲染的引用数据。

通过 SplashRequest 参数控制页面渲染

SplashRequest 支持多个参数,用于控制 Splash 的页面渲染方式:

参数

类型

用途

wait

float

页面加载完成后等待返回 HTML 的秒数

timeout

float

最大渲染时间(秒)。默认值为 60,上限约为 90,除非被覆盖

images

int (0/1)

设置为 0 以禁用图片加载,从而加快渲染速度

resource_timeout

float

每个资源(CSS、JS 文件、图片)的超时时间

http_method

字符串

使用 POST 用于表单提交

body

字符串

POST 请求正文内容,需配合 http_method='POST'

例如,要发送一个 POST 请求(适用于表单提交):

yield SplashRequest(
    url='https://example.com/search',
    args={
        'wait': 1,
        'http_method': 'POST',
        'body': 'query=scrapy+splash',
    },
    callback=self.parse_results,
)

http_methodbody 参数对于在服务器端处理搜索表单或登录操作的网站非常实用。这涵盖了 scrapy splash JavaScript 渲染的基础知识,但若要进行页面交互(点击、滚动、等待动态元素),则需要 Lua 脚本。

编写 Lua 脚本以实现高级交互

` render.html 端点可处理简单场景,但一旦需要与页面交互,就需转用 execute 端点并配合 Lua 脚本。Scrapy Splash 的 Lua 脚本可让你逐步控制浏览器:

function main(splash, args)
  splash:go(args.url)
  splash:wait(1)
  return splash:html()
end

通过 SplashRequest 发送此请求,使用 endpoint='execute' ,并将脚本传递至 args={'lua_source': script}。在此基础上,可逐步添加元素等待、滚动循环及点击操作。

等待特定元素加载

当您知道页面加载时间时,固定 wait 在已知页面加载时间时有效,但这种方法不稳定。建议改为轮询特定 DOM 元素:

function main(splash, args)
  splash:go(args.url)
  while not splash:select('.target-element') do
    splash:wait(0.5)
  end
  splash:wait(0.5)
  return splash:html()
end

该脚本将循环执行,直到 splash:select() 找到符合 .target-element,每次重试之间间隔半秒。一旦元素出现,最后一次短暂的等待将处理剩余的渲染。这种模式比猜测静态延迟要可靠得多。

滚动无限滚动页面

Splash 没有内置的滚动命令。取而代之的是,通过注入 JavaScript 来控制滚动位置。以下是一个用于 scrapy splash 无限滚动的 Lua 脚本:

function main(splash, args)
  splash:go(args.url)
  splash:wait(2)
  local scroll_count = 5
  for i = 1, scroll_count do
    splash:runjs("window.scrollTo(0, document.body.scrollHeight)")
    splash:wait(2)
  end
  return splash:html()
end

该脚本滚动至页面底部,等待新内容加载,然后循环重复。请根据网站特性调整 scroll_count 和等待时长以适应目标网站。比较 document.body.scrollHeight 每次滚动前后的状态,以判断何时不再有新内容出现。

点击按钮与页面导航

“加载更多”按钮和分页链接需要鼠标交互。使用 splash:select() 定位元素并触发点击:

function main(splash, args)
  splash:go(args.url)
  splash:wait(2)
  local btn = splash:select('.load-more-btn')
  if btn then
    btn:mouse_click()
    splash:wait(2)
  end
  return splash:html()
end

对于包含多个触发点的页面,请将此操作封装在循环中。对于分页,请选择“下一页”链接,点击它,等待新页面加载,并在每个步骤中收集 HTML。

在 Splash 中运行自定义 JavaScript

有时您并不需要完整的 Lua 工作流。Splash 允许您通过两种方法注入任意 JavaScript: splash:evaljs() (返回值) 和 splash:runjs() (执行但不返回)。

function main(splash, args)
  splash:go(args.url)
  splash:wait(1)
  local title = splash:evaljs("document.title")
  splash:runjs("document.querySelector('.popup-close').click()")
  splash:wait(0.5)
  return {html = splash:html(), title = title}
end

这对于关闭 Cookie 提示横幅、关闭模态框,或在抓取页面 HTML 之前提取计算值非常有用。您还可以通过 js_source 参数传递 JavaScript(无需 Lua),该脚本将在页面加载后、HTML 快照生成前执行。

在 Scrapy Splash 中使用代理

轮换 IP 地址有助于防止在任何规模的爬取过程中被封禁。通过在 SplashRequest 参数中传递详细信息,将请求路由至 Scrapy Splash 代理:

yield SplashRequest(
    url='https://example.com',
    callback=self.parse,
    args={
        'wait': 2,
        'proxy': 'http://user:pass@proxyhost:port',
    },
)

您还可以通过 Lua 脚本配置代理,使用 splash:on_request():

function main(splash, args)
  splash:on_request(function(request)
    request:set_proxy{
      host = "proxyhost",
      port = 8080,
      username = "user",
      password = "pass",
    }
  end)
  splash:go(args.url)
  splash:wait(2)
  return splash:html()
end

Lua 方法允许您在同一页面加载过程中,为不同的子请求应用不同的代理。请注意,Splash 本身并不能绕过反机器人系统;它仅负责渲染页面。您仍需使用经过适当轮换的住宅或数据中心代理,以避免 IP 级别的封禁。

常见错误与故障排除

大多数 Scrapy Splash 教程在此处搁浅。以下是您最常遇到的错误:

连接到 localhost:8050 被拒绝。Splash Docker 容器未运行。请通过 docker ps。如果容器正在运行但无法访问,请检查端口 8050 是否被防火墙阻挡或被其他进程占用。

504 网关超时。页面渲染时间超过了允许的超时限制。请在 timeout 参数。默认上限约为 90 秒。若需更长的渲染时间,请使用更高的 --max-timeout (请对照当前 Splash 文档进行验证,因不同版本细节可能有所差异)。

Lua 脚本错误(“参数错误”、“尝试对 nil 值进行索引”)。这通常意味着 splash:select() 返回 nil ,因为该元素尚未进入 DOM。在与其交互前请添加等待或轮询循环。

Docker 容器被终止(OOM)。在资源密集型页面上,Splash 可能消耗大量内存。请通过 --memory 2g 并禁用图片加载(images=0)。若需运行多个实例,请使用 Docker Compose 并为每个容器设置资源限制。

返回空白或不完整的 HTML。该页面的 JavaScript 可能需要更多时间。请增加 wait。若第三方资源加载缓慢,请设置 resource_timeout 以跳过它们。

Scrapy Splash 与 Scrapy-Playwright 与 Selenium

选择合适的渲染工具取决于您的项目。以下是 Scrapy Splash 替代方案中三种最常见选项的对比:

功能

Scrapy Splash

Scrapy-Playwright

Selenium

浏览器引擎

WebKit(基于 Qt)

Chromium、Firefox、WebKit

Chrome、Firefox、Edge

Scrapy 集成

原生(scrapy-splash)

原生(scrapy-playwright)

需要自定义中间件

异步支持

有限(HTTP API)

完全异步(基于 Playwright)

默认同步

资源使用

低至中等

中等

对现代 JavaScript 的支持

部分支持(旧版 WebKit)

完全支持(Chromium)

完全

反机器人绕过

无内置功能

无内置

无内置

最适合

大规模轻量级 JS 渲染

复杂的单页应用(SPA)、现代 JavaScript 网站

非 Scrapy 项目、测试

当您希望将开销降至最低,且目标页面不依赖最前沿的浏览器 API 时,Splash 是理想之选。对于现代单页应用程序,采用 Chromium 后端的 Scrapy-Playwright 可能更合适。 Selenium 虽然可用,但缺乏与 Scrapy 的原生集成。这些工具均无法独立处理反机器人防护,因此在生产环境抓取时仍需使用代理层。请以本 Scrapy Splash 教程为基础,并在项目需要时探索其他替代方案。

关键要点

  • Splash 在 Docker 中运行,并通过 HTTP API 连接到 Scrapy。一旦容器监听 8050 端口且 settings.py 完成配置后,您的爬虫只需调用一次 SplashRequest 即可渲染 JavaScript 页面。
  • 当需要交互操作时,请使用 Lua 脚本。固定等待时间适用于简单场景,但元素轮询、滚动循环和点击操作则需要 execute 通过 Lua 脚本实现的端点。
  • 代理对生产环境中的抓取至关重要。Splash 负责渲染页面,但无法绕过反机器人防护。请通过 SplashRequest 参数或 splash:on_request() 在 Lua 中实现,将请求路由至轮换代理。
  • Splash 虽轻量但已显陈旧。与 Scrapy 集成良好,但其 WebKit 引擎不支持某些现代 JavaScript API。对于需要 Chromium 后端的网站,请考虑使用 Scrapy-Playwright。
  • 请系统化地排查故障。大多数 Splash 问题归根结底是超时、元素缺失或 Docker 资源限制所致。

常见问题

Scrapy Splash 能否处理基于 React 或 Vue 构建的单页应用?

它能渲染许多 React 和 Vue 应用,但结果取决于应用使用的 JavaScript API。由于 Splash 运行在旧版 WebKit 引擎上,因此依赖现代浏览器功能(如 IntersectionObserver 或 ES2020+ 语法)的应用可能无法正确渲染。在构建完整爬虫之前,请先通过 Splash 网页界面 localhost:8050 测试目标 URL。

Splash Docker 容器在生产环境中进行爬取需要多少内存?

对于典型工作负载,建议每个实例预留至少 1 至 2 GB 内存。包含大量图片或复杂 JavaScript 的页面可能会消耗更多内存。可通过 images=0 来减少内存消耗,并设置 Docker 的 --memory 参数,以防止单个容器耗尽主机资源。

Scrapy Splash 是否仍在维护?有哪些活跃的替代方案?

Splash 更新频率较低,且已不再进行积极的功能开发。它仍适用于许多用例,但社区在新项目中已普遍转向 Scrapy-Playwright。Selenium 仍是 Scrapy 生态系统之外的一个选项。每种工具在浏览器引擎支持、异步能力和资源使用方面都有各自的取舍。

cookies 字典中添加一个 args 字典中添加key,或使用 headers 参数设置头部。在 Lua 脚本中,请在 splash:set_custom_headers() ,然后调用 splash:go()。当 SplashCookiesMiddleware 在您的设置中启用时,会自动转发来自 Scrapy cookie 存储库的 Cookie。

结论

本篇 Scrapy Splash 教程已带您完整走过整个工作流:搭建 Splash 容器、配置 Scrapy 项目、使用 SplashRequest 编写爬虫、编写用于滚动和点击的 Lua 脚本,以及配置代理。本文涵盖的故障排除模式应能为您节省数小时的调试时间。

Splash 虽能很好地处理简单的渲染任务,但目前已属于较旧的技术。如果您的目标网站对现代 JavaScript 提出了更高要求,建议考虑基于 Chromium 的替代方案。无论您选择哪种渲染工具,生产环境中爬取的真正瓶颈很少是浏览器本身,而是如何突破反机器人防御机制以及如何大规模管理代理基础设施。

若您希望完全避免基础设施带来的烦恼,WebScrapingAPI 通过单一 API 接口即可处理代理轮换、验证码破解和 JavaScript 渲染,让您能够专注于数据解析,而非与基础架构的繁琐工作周旋。

关于作者
Ștefan Răcilă, 全栈开发工程师 @ WebScrapingAPI
Ștefan Răcilă全栈开发工程师

Stefan Racila 是 WebScrapingAPI 的 DevOps 及全栈工程师,负责开发产品功能并维护确保平台稳定运行的基础设施。

开始构建

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

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