Crawlee(Apify SDK v1)升级指南:从 PuppeteerPool 到 BrowserPool、Crawling Context 与 launchContext 的全面迁移
【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee
导读
本文基于 Crawlee 仓库 website/versioned_docs/version-3.13/upgrading/upgrading_v1.md 中的官方迁移文档,系统讲解 Apify SDK v1 引入的破坏性变更与对应的迁移路径。读者将掌握:如何安装 v1 并选择 Puppeteer/Playwright、handler 参数如何统一为 Crawling Context、PuppeteerPool如何被BrowserPool取代、gotoFunction/launchPuppeteerOptions/launchPuppeteerFunction等旧选项如何迁移,以及新版 launch 函数的正确用法。文末会结合当前仓库中 packages/browser-pool 与 packages/puppeteer-crawler 的源码实现,佐证这些设计背后的底层原理。
为什么会有 v1:稳定性与多浏览器支持
经过 3.5 年的快速迭代和大量破坏性变更、弃用之后,Apify SDK v1 正式发布。这次大版本有两个核心目标:
- 稳定性:SDK 已被大量网页抓取和自动化项目使用,团队承诺从 v1 开始每年最多只通过一次新的大版本发布引入破坏性变更,为开发者提供稳定的运行环境。
- 更多浏览器支持:通过用新库
browser-pool)替换PuppeteerPool,将原本仅支持 Puppeteer 的能力扩展到 Playwright(Firefox、WebKit/Safari 等所有知名浏览器)。
browser-pool 继承了PuppeteerPool的核心思想,并将其扩展为可管理多种浏览器自动化库。它的 API 与PuppeteerPool相似但不完全相同。Playwright 与 Puppeteer 接口几乎一致,同时增加了实用特性并简化了常见任务;即使如此,你仍然可以在新的BrowserPool下继续使用 Puppeteer。
从当前仓库的源码可以看到,
browser-pool已经独立演进为一个通用浏览器管理库,通过PuppeteerPlugin和PlaywrightPlugin两个插件封装底层库,BrowserPool负责启动、回收、关闭浏览器以及管理整个浏览器/页面生命周期(见 browser-pool.ts)。
一个重要的破坏性变更:不再捆绑浏览器库
v1 之前,SDK 直接捆绑了puppeteer包,用户无需自行安装。v1 同时支持playwright,为了不强迫用户同时安装两者,也为了让安装更快、让用户自由选择库的版本,v1 起puppeteer和playwright都不再随 SDK 捆绑,需要用户自行安装。这也为未来支持更多库留出了空间。
这一设计在当前仓库中依然延续:@crawlee/browser-pool的 README 明确说明它不预装任何浏览器自动化库,由用户自行选择库及其版本(见 packages/browser-pool/README.md)。
安装:选择 Puppeteer 还是 Playwright
按需安装。使用 Puppeteer(与旧版本行为一致):
npm install apify puppeteer使用 Playwright:
npm install apify playwright需要注意:v1 初始版本虽然覆盖了大部分核心功能,但仍有一些工具函数或选项仅支持 Puppeteer 而不支持 Playwright。
在 Apify Platform 上运行
如果要在 Apify Platform 上使用 Playwright,需要选择支持 Playwright 的 Docker 镜像(官方已提供,详见仓库 docs/deployment 目录下的相关部署文档)。同时请务必注意:
你的
package.json必须将puppeteer和/或playwright列为依赖。如果没有列出,构建 actor 时这些库会被从node_modules中卸载。
Handler 参数统一为 Crawling Context
旧版 SDK 中,用户提供的各个 handler 函数的参数是各自独立创建的对象。这导致同一个请求在不同函数中拿到的参数对象不是同一个引用,跨函数追踪状态非常困难:
const handlePageFunction = async (args1) => { args1.hasOwnProperty('proxyInfo') // true } const handleFailedRequestFunction = async (args2) => { args2.hasOwnProperty('proxyInfo') // false } args1 === args2 // falseSDK v1 引入了一个统一的单一对象:Crawling Context。所有 handler 共享同一个上下文对象:
const handlePageFunction = async (crawlingContext1) => { crawlingContext1.hasOwnProperty('proxyInfo') // true } const handleFailedRequestFunction = async (crawlingContext2) => { crawlingContext2.hasOwnProperty('proxyInfo') // true } // 所有 context 都是同一个对象。 crawlingContext1 === crawlingContext2 // trueCrawling Context 的id与跨上下文访问
既然所有对象都是同一个,SDK 就能追踪所有正在运行的 crawling context,通过新增的id属性实现跨上下文访问:
let masterContextId; const handlePageFunction = async ({ id, page, request, crawler }) => { if (request.userData.masterPage) { masterContextId = id; // 准备 master 页面。 } else { const masterContext = crawler.crawlingContexts.get(masterContextId); const masterPage = masterContext.page; const masterRequest = masterContext.request; // 现在可以在另一个 handlePageFunction 中操作 master 页面的数据。 } }autoscaledPool移至crawlingContext.crawler下
为避免对象臃肿并让关键对象更容易访问,v1 在 handler 参数上暴露了crawler属性:
const handlePageFunction = async ({ request, page, crawler }) => { await crawler.requestQueue.addRequest({ url: 'https://example.com' }); await crawler.autoscaledPool.pause(); }这也意味着puppeteerPool、autoscaledPool等旧版简写不再需要:
const handlePageFunction = async (crawlingContext) => { crawlingContext.autoscaledPool // 不再存在 crawlingContext.crawler.autoscaledPool // <= 这才是正确用法 }PuppeteerPool被BrowserPool取代
BrowserPool在PuppeteerPool的基础上扩展出管理其他浏览器自动化库的能力。只有PuppeteerCrawler和PlaywrightCrawler使用它,可以通过crawler对象访问:
const crawler = new Apify.PlaywrightCrawler({ handlePageFunction: async ({ page, crawler }) => { crawler.browserPool // <----- } }); crawler.browserPool // <-----页面现在有 ID
页面 ID 与crawlingContext.id相同,这让你在 hooks 中可以访问完整的crawlingContext(详见下文生命周期 hooks 小节):
const pageId = browserPool.getPageId这一设计在当前仓库中仍然存在:BrowserPool的getPageId(page)方法通过内部pageIdsWeakMap 返回页面 ID,页面 ID 在浏览器启动前就已创建,并伴随页面直到关闭(见 browser-pool.ts)。
配置与生命周期 hooks
BrowserPool最重要的新增能力是生命周期 hooks,可通过两个 crawler 的browserPoolOptions访问。完整的browserPoolOptions列表定义在browser-pool中(当前仓库对应 BrowserPoolOptions):
const crawler = new Apify.PuppeteerCrawler({ browserPoolOptions: { retireBrowserAfterPageCount: 10, preLaunchHooks: [ async (pageId, launchContext) => { const { request } = crawler.crawlingContexts.get(pageId); if (request.userData.useHeadful === true) { launchContext.launchOptions.headless = false; } } ] } })从当前仓库源码可以补充BrowserPoolOptions的默认值(见 browser-pool.ts 的 zod schema):
| 选项 | 默认值 | 说明 |
|---|---|---|
maxOpenPagesPerBrowser | 20 | 单个浏览器同时打开的最大页面数,超过后启动新浏览器 |
retireBrowserAfterPageCount | 100 | 浏览器处理多少页后自动退役关闭 |
operationTimeoutSecs | 15 | 底层库异步操作(启动浏览器/打开页面)的超时秒数 |
closeInactiveBrowserAfterSecs | 300 | 非活动浏览器多久后被关闭以释放资源 |
retireInactiveBrowserAfterSecs | 10 | 非活动浏览器多久后被标记为退役 |
useFingerprints | true | 是否启用浏览器指纹(v3 起默认开启) |
hooks 共分六类:preLaunchHooks、postLaunchHooks、prePageCreateHooks、postPageCreateHooks、prePageCloseHooks、postPageCloseHooks。从源码看,hooks 按声明顺序依次await执行,且 hook 的第一个参数是pageId(页面创建前)或page(页面创建后),用于追踪 hook 由哪次newPage()触发(见 browser-pool.ts)。
BrowserController的引入
BrowserController是browser-pool中负责浏览器管理的类,为 Puppeteer 和 Playwright 提供统一的 API。它在后台自动工作,但如果你需要正确地关闭浏览器,应该通过它来做。它出现在 handler 参数中:
const handlePageFunction = async ({ page, browserController }) => { // 错误用法。会绕过 BrowserPool,可能引发问题。 await page.browser().close(); // 正确用法。允许优雅关闭。 await browserController.close(); const cookies = [/* some cookie objects */]; // 错误用法。只在 Puppeteer 中有效,Playwright 中无效。 await page.setCookies(...cookies); // 正确用法。两种库都有效。 await browserController.setCookies(page, cookies); }当前仓库中BrowserController的抽象基类完整实现了这套统一 API:getCookies(page)、setCookies(page, cookies)、close(),以及kill()、newPage()等内部方法(见 abstract-classes/browser-controller.ts)。close()会优雅关闭浏览器并确保不残留进程,同时通过PROCESS_KILL_TIMEOUT_MILLIS = 5000的超时兜底强制终止卡住的进程(见 browser-controller.ts)。专门的PuppeteerController与PlaywrightController分别继承它实现底层差异。
BrowserController还携带浏览器的重要信息,例如启动它的上下文——这在 v1 之前很难获取:
const handlePageFunction = async ({ browserController }) => { // 浏览器使用的代理信息 browserController.launchContext.proxyInfo // 浏览器使用的会话 browserController.launchContext.session }对应的LaunchContext类在当前仓库中实现,它持有launchOptions、proxyUrl、useIncognitoPages、userDataDir等信息,并提供extend()方法安全地附加浏览器作用域的任意字段(如 session ID),见 packages/browser-pool/src/launch-context.ts。
BrowserPool方法与PuppeteerPool的对照
部分函数被移除(与更早的弃用一致),部分发生了变更:
// 旧 await puppeteerPool.recyclePage(page); // 新 await page.close();// 旧 await puppeteerPool.retire(page.browser()); // 新 browserPool.retireBrowserByPage(page);// 旧 await puppeteerPool.serveLiveViewSnapshot(); // 新 // BrowserPool 中不再有 LiveView其中retireBrowserByPage(page)在当前仓库中依然存在:它通过getBrowserControllerByPage(page)找到页面所属的浏览器控制器并对其执行退役(见 browser-pool.ts)。退役不同于立即关闭——浏览器不再开新页面,但会等待已打开的页面关闭后再优雅退出,避免运行中的任务被打断。
更新的PuppeteerCrawlerOptions
为了让PuppeteerCrawler和PlaywrightCrawler保持一致,v1 更新了选项体系。
移除gotoFunction,改用导航前后 hooks
可配置的gotoFunction概念并不理想,尤其是底层使用经过修改的gotoExtended——用户重写gotoFunction想要扩展默认行为时,必须了解这些内部细节。v1 用preNavigationHooks和postNavigationHooks取代了它。
下面这个例子展示了旧gotoFunction有多么繁琐:
const gotoFunction = async ({ request, page }) => { // 预处理 await makePageStealthy(page); // 必须记住怎么做: const response = await gotoExtended(page, request, {/* 必须记住默认参数 */}); // 后处理 await page.evaluate(() => { window.foo = 'bar'; }); // 不能忘记! return response; } const crawler = new Apify.PuppeteerCrawler({ gotoFunction, // ... })使用preNavigationHooks和postNavigationHooks则简单得多。preNavigationHooks接收两个参数:crawlingContext和gotoOptions;postNavigationHooks只接收crawlingContext:
const preNavigationHooks = [ async ({ page }) => makePageStealthy(page) ]; const postNavigationHooks = [ async ({ page }) => page.evaluate(() => { window.foo = 'bar' }) ] const crawler = new Apify.PuppeteerCrawler({ preNavigationHooks, postNavigationHooks, // ... })这两个选项在当前仓库的PuppeteerCrawlerOptions中依然是标准配置(见 packages/puppeteer-crawler/src/internals/puppeteer-crawler.ts)。仓库还特别提醒:在preNavigationHooks中使用injectJQuery()会导致结果不稳定,应将其放在postNavigationHook或requestHandler中使用。
launchPuppeteerOptions=>launchContext
旧launchPuppeteerOptions一直令人困惑,因为它把 Apify 自定义选项与 Puppeteer 的launchOptions混在了一起:
const launchPuppeteerOptions = { useChrome: true, // Apify 选项 headless: false, // Puppeteer 选项 }新launchContext对象显式定义了launchOptions。launchPuppeteerOptions已被移除:
const crawler = new Apify.PuppeteerCrawler({ launchContext: { useChrome: true, // Apify 选项 launchOptions: { headless: false // Puppeteer 选项 } } })
LaunchContext同时也是browser-pool的类型,两边的结构完全一致,SDK 只是额外增加了一些选项。
当前仓库中PuppeteerLaunchContext的结构与此一脉相承:launchOptions对应 Puppeteer 的puppeteer.launch选项,另外提供useChrome(为 true 且未指定executablePath时启动完整版 Chrome 而非捆绑的 Chromium)、proxyUrl、launcher(支持puppeteer-extra等包装库)、useIncognitoPages(每页独立上下文、互不共享 cookie 与缓存)等扩展选项(见 packages/puppeteer-crawler/src/internals/puppeteer-launcher.ts)。
移除launchPuppeteerFunction
browser-pool引入了生命周期 hooks 的概念——在浏览器生命周期中特定事件发生时执行的函数:
const launchPuppeteerFunction = async (launchPuppeteerOptions) => { if (someVariable === 'chrome') { launchPuppeteerOptions.useChrome = true; } return Apify.launchPuppeteer(launchPuppeteerOptions); } const crawler = new Apify.PuppeteerCrawler({ launchPuppeteerFunction, // ... })现在你可以用preLaunchHook实现同样的功能:
const maybeLaunchChrome = (pageId, launchContext) => { if (someVariable === 'chrome') { launchContext.useChrome = true; } } const crawler = new Apify.PuppeteerCrawler({ browserPoolOptions: { preLaunchHooks: [maybeLaunchChrome] }, // ... })这种方式更好:它在 Puppeteer 和 Playwright 间保持一致,并且允许你用预定义的行为轻松组合浏览器:
const preLaunchHooks = [ maybeLaunchChrome, useHeadfulIfNeeded, injectNewFingerprint, ]借助crawler.crawlingContexts,这些 hook 函数还能访问触发启动的request所对应的crawlingContext:
const preLaunchHooks = [ async function maybeLaunchChrome(pageId, launchContext) { const { request } = crawler.crawlingContexts.get(pageId); if (request.userData.useHeadful === true) { launchContext.launchOptions.headless = false; } } ]Launch 函数
除了Apify.launchPuppeteer(),v1 新增了Apify.launchPlaywright()。
更新后的参数
launch 选项对象也因同样原因(新旧选项混用易混淆)做了更新:
// 旧 await Apify.launchPuppeteer({ useChrome: true, headless: true, }) // 新 await Apify.launchPuppeteer({ useChrome: true, launchOptions: { headless: true, } })当前仓库中launchPuppeteer(launchContext, configuration)的实现依然遵循这一签名:通过PuppeteerLauncher启动浏览器,并会读取CRAWLEE_HEADLESS环境变量、将proxyUrl校验后写入--proxy-server启动参数等(见 packages/puppeteer-crawler/src/internals/puppeteer-launcher.ts)。
自定义模块:puppeteerModule统一为launcher
Apify.launchPuppeteer原本支持puppeteerModule选项。引入 Playwright 后,名称被统一为launcher——因为playwright模块本身并不直接启动浏览器,需要指定具体的浏览器类型(chromium/firefox/webkit):
const puppeteer = require('puppeteer'); const playwright = require('playwright'); await Apify.launchPuppeteer(); // 等价于: await Apify.launchPuppeteer({ launcher: puppeteer }) await Apify.launchPlaywright(); // 等价于: await Apify.launchPlaywright({ launcher: playwright.chromium })从当前仓库browser-pool的 README 可以看到这一能力的完整形态:BrowserPool通过browserPlugins接收插件(如new PlaywrightPlugin(playwright.chromium)),支持在一个池中同时管理多个插件并按轮询(round-robin)方式分配页面,也支持newPageWithEachPlugin()同时用所有插件各开一个页面用于多环境测试(见 packages/browser-pool/README.md)。这正是文档中"未来可以支持更多库"承诺的实现基础。
迁移清单速查
| 旧 API(v1 之前) | 新 API(v1 起) |
|---|---|
SDK 捆绑puppeteer | 自行安装puppeteer或playwright |
| handler 各自独立的参数对象 | 统一的 Crawling Context(所有 handler 共享同一对象) |
参数中的puppeteerPool/autoscaledPool | crawler.browserPool/crawler.autoscaledPool |
PuppeteerPool | BrowserPool(经browserPoolOptions配置) |
puppeteerPool.recyclePage(page) | page.close() |
puppeteerPool.retire(browser) | browserPool.retireBrowserByPage(page) |
puppeteerPool.serveLiveViewSnapshot() | 移除,BrowserPool 无 LiveView |
gotoFunction | preNavigationHooks+postNavigationHooks |
launchPuppeteerOptions | launchContext(内含launchOptions) |
launchPuppeteerFunction | browserPoolOptions.preLaunchHooks |
puppeteerModule | launcher(Playwright 需指定如playwright.chromium) |
仅Apify.launchPuppeteer() | 另有Apify.launchPlaywright() |
小结
Apify SDK v1 通过"每年一次大版本破坏性变更"的承诺换取长期稳定,并通过BrowserPool让 Puppeteer 与 Playwright 站在同一套生命周期管理 API 之下。从本文可以看到,所有迁移的核心思路是一致的:把散落的、隐含的参数与行为收敛为显式的、统一的对象和 hooks——handler 参数收敛为 Crawling Context,启动参数收敛为launchContext,自定义行为收敛为各类 hooks。这一设计原则在如今 Crawlee 的browser-pool、puppeteer-crawler、playwright-crawler各包源码中依然清晰可见,也是理解后续 v2、v3 系列演进的钥匙。
【免费下载链接】crawleeCrawlee—A web scraping and browser automation library for Node.js to build reliable crawlers. In JavaScript and TypeScript. Extract data for AI, LLMs, RAG, or GPTs. Download HTML, PDF, JPG, PNG, and other files from websites. Works with Puppeteer, Playwright, Cheerio, JSDOM, and raw HTTP. Both headful and headless mode. With proxy rotation.项目地址: https://gitcode.com/GitHub_Trending/cr/crawlee
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考