Crawlee BasicCrawler 能力全景:从配置参数到源码实现的分层解析
【免费下载链接】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
BasicCrawler是 Crawlee 中负责“抓取调度”的底层核心:它不负责页面解析或浏览器渲染,而是把静态 URL 列表、动态请求队列乃至 sitemap 统一抽象为请求来源,并以并发受控的方式逐个派发requestHandler。本文以@crawlee/basic包的 CHANGELOG.md 为主线,串联其演进历史与核心源码,梳理BasicCrawler的完整能力清单,帮助读者理解每个选项背后的实现原理,并掌握停止、暂停、批量入队、robots.txt 遵守等关键操作的正确姿势。
一、BasicCrawler 是什么:定位与适用场景
BasicCrawler是 Crawlee 所有爬虫的最底层抽象,位于 basic-crawler/src/index.ts,其对外导出由@crawlee/core的公共类型与 internals/basic-crawler.ts 中的类实现共同构成。官方示例 docs/examples/basic_crawler.mdx 将其定位为“最底层的示例”——它把每个 URL 抽象成一个Request对象,通过requestHandler回调执行用户逻辑(例如用sendRequest下载 HTML 存入 Dataset),而把页面解析、浏览器控制等留给了更高层的CheerioCrawler、PuppeteerCrawler、PlaywrightCrawler。
在 Changelog 中,BasicCrawler的演进始终围绕几个核心主题:请求来源的多样化(RequestList、RequestQueue、sitemap、Tandem 组合)、请求调度与重试策略、防封锁(代理、会话、robots.txt)、以及运行生命周期控制。下面按这些主题展开。
二、请求来源:从 requestList/requestQueue 到 requestManager
2.1 传统双入口:requestList 与 requestQueue
最初版本的BasicCrawler通过两个构造选项提供请求来源:
requestList:静态 URL 列表(RequestList),适合确定性的批量抓取;requestQueue:动态请求队列(RequestQueue),支持爬取过程中递归入队新 URL。
Changelog 中多次修复围绕这一组合展开,例如 3.5.7 版本“add warning when we detect use of RL and RQ, but RQ is not provided explicitly”(检测到同时使用两种来源时给出警告),以及 3.15.0 引入的TandemRequestProvider——它把只读的RequestList与可写的RequestQueue组合成一个串联管理器,先消费列表中的 URL,再自动把它们入队,保证同一 URL 不会被重复抓取。
2.2 新一代统一入口:requestManager
从源码看,这两个传统选项目前已被标记为@deprecated:
/** * @deprecated Use the `requestManager` option instead. ... */ requestList?: IRequestLoader; /** * @deprecated Use the `requestManager` option instead. ... */ requestQueue?: RequestQueue; requestManager?: IRequestManager;构造函数中会把两者“折叠”进统一的requestManager(两者同时给出时组合成RequestManagerTandem)。RequestQueue本身就是请求管理器,可以直接传入;只读来源(RequestList、SitemapRequestLoader)则通过requestLoader.toTandem(requestQueue)组合。当什么也没传时,爬虫会在首次需要时打开默认的RequestQueue——注意实现细节:第一个爬虫实例使用默认队列(null标识),后续实例通过唯一别名__default_<id>__各自获得独立队列以避免冲突,见openOwnedRequestQueue()。
关于 sitemap,3.11.0 引入了“Sitemap-based request list implementation”,也就是SitemapRequestLoader,它让爬虫可以直接从sitemap.xml生成请求列表,相关用法可参考 docs/guides/request_loaders.mdx。
2.3 批量入队:addRequests 与 addRequestsBatched
3.9.2 修复了“don't call notify in addRequests()”,3.13.9 起addRequests开始接受 (Async)Iterable。crawler.addRequests()内部走RequestQueue.addRequestsBatched()的批量逻辑,测试 test/batch-add-requests.test.ts 验证了其行为:默认每批 1000 条,返回{ addedRequests, waitForAllRequestsToBeAdded }——首批返回即解析,剩余批次在后台继续入队;只有显式传入waitForAllRequestsToBeAdded: true才会一次性全部入队。
关键选项(来自addRequestsOptionsSchema):
| 选项 | 说明 |
|---|---|
forefront | 是否插入队列前端(优先处理) |
batchSize | 每批入队数量,默认 1000 |
waitBetweenBatchesMillis | 批次间等待时间 |
waitForAllRequestsToBeAdded | 是否等待全部入队完成 |
maxNewRequests/limit | 本次入队的请求上限 |
include/exclude | glob 或正则 URL 过滤模式 |
strategy | 入队策略(All/SameHostname/SameDomain/SameOrigin),此处默认All |
transformRequestFunction | 入队前改写请求对象 |
userData/label | 为整批请求统一附加数据与路由标签 |
include/exclude与strategy是“与”关系(URL 必须同时满足两者),这与 crawlee-python 的行为保持一致。入队时会同步执行maxRequestsPerCrawl预算计算(#calculateEnqueuedRequestLimit),避免入队超过爬虫实际处理能力的链接数量。
三、请求处理、重试与错误处理
3.1 requestHandler 与超时
requestHandler是每个 URL 的处理逻辑,默认超时requestHandlerTimeoutSecs = 60秒(见optionsShape与构造函数的60_000毫秒兜底)。源码中runRequestHandler()用addTimeoutToPromise包裹用户回调,超时抛出带请求 ID 的TimeoutError。
3.4.2 修复了“limit internalTimeoutMillis in addition to requestHandlerTimeoutMillis”的问题,引入internalTimeoutMillis作为整个请求生命周期的兜底超时:它至少是requestHandlerTimeoutMillis * 2,且不小于 300 秒(也可通过配置internalTimeoutMillis覆盖)。该超时通过raceWithTimeout(request-timeout.ts)实现——它特意使用“裸定时器”而非嵌套的addTimeoutToPromise帧,避免内部超时触发时把请求处理中的AbortController一并取消、导致随后的错误处理无法执行。请求各阶段(导航、导航钩子、requestHandler)有自己的独立超时,context.extendTimeout(secs)可同时向后推延这三个预算。
3.2 重试机制:maxRequestRetries 与错误分类
maxRequestRetries默认 3。重试判定逻辑canRequestBeRetried()遵循以下规则:
request.noRetry === true或错误为NonRetryableError:不重试;- 错误为
RetryRequestError(用户显式要求重试):强制重试,忽略次数; - 否则比较
request.maxRetries ?? maxRequestRetries与request.retryCount。3.3.3 引入了Request.maxRetries,允许对单个请求覆盖全局重试次数。
错误处理有三个层级,对应 Changelog 中的演进:
errorHandler:每次重试前的钩子(3.5.1 起,会话轮换次数超限也会触发failedRequestHandler);failedRequestHandler:所有重试耗尽后调用;SessionError/ 会话相关错误:错误处理过程中若判定为会话问题会调用session.retire()。
Changelog 3.11.5 修复“trigger errorHandler for session errors”,确保会话类错误也会走errorHandler。请求成功后会session.markGood(),失败则markBad()降低会话信誉分。
3.3 阻塞检测与重试:blockedStatusCodes / retryOnBlocked
blockedStatusCodes:默认[401, 403, 429],命中即抛SessionError并轮换会话;retryOnBlocked:默认false,开启后自动尝试绕过 Cloudflare Bot Management 与 Google Search 的限流检测。3.7.0 增加了“log cause with retryOnBlocked”,3.4.2 则让其在探测到被封锁页面时自动重试;- HTTP 状态码处理:默认
>= 500视为错误,可用ignoreHttpErrorStatusCodes豁免、additionalHttpErrorStatusCodes追加(见isErrorStatusCode())。
此外,429 响应会被recordDomainRateLimit()记录到请求管理器,由ThrottlingRequestManager按Retry-After头与指数退避策略延迟重试,并抛出RequestThrottledError而非记失败。
四、并发控制与任务循环
4.1 并发快捷选项
BasicCrawler提供四个并发快捷参数:
| 选项 | 默认 | 说明 |
|---|---|---|
minConcurrency | 自动伸缩下限 | 设置过低会拖慢爬虫,过高可能耗尽内存/CPU |
maxConcurrency | 自动伸缩上限 | 与 min 一起决定并发区间 |
initialConcurrency | 取minConcurrency | 启动时的初始并发度 |
maxRequestsPerMinute | Infinity | 每分钟最大请求数,须为>= 1的整数 |
这些快捷选项最终折叠进爬虫自建的ConcurrencySystem(createDefaultConcurrencySystem),实现“有足够空闲 CPU/内存才派发新任务”的自动伸缩。若同时显式传入concurrencySystem与上述快捷选项,构造函数会直接抛错,因为两者互相排斥。
4.2 任务循环与 keepAlive
爬虫的核心循环由CrawlerRun(crawler-run.ts)承载,它封装AutoscaledPool与runTaskFunction、isTaskReadyFunction、isFinishedFunction三个谓词。keepAlive: true会强制isFinishedFunction恒为false,使爬虫在队列清空后仍保持运行等待新请求,此时必须通过stop()或teardown()退出。taskLoopOptions允许用户覆盖 ready/finished 谓词,但任务本身(取请求、跑管道)归爬虫所有、不可覆写。
3.13.3 修复了“respect autoscaledPoolOptions.isTaskReadyFunction option”,确保用户自定义的 ready 谓词真正生效。
五、会话、代理与 robots.txt 遵守
5.1 会话与代理
爬虫自带SessionPool,可通过sessionPool选项注入共享实例(注入的池生命周期由调用方负责,爬虫不会销毁它)。同时传入sessionPool与proxyConfiguration时,proxyConfiguration会被忽略并给出警告——代理应配置在会话池上(如addSession({ proxyInfo }))。3.9.0 为代理配置引入了tieredProxyUrls(分层代理 URL),3.5.0 支持“retire session on proxy error”。
5.2 respectRobotsTxtFile:从布尔到自定义 userAgent
这一功能的演进在 Changelog 中非常清晰:
- 3.13.1:重命名
RobotsFile为RobotsTxtFile; - 3.13.1:新增
respectRobotsTxtFile爬虫选项; - 3.15.3:支持自定义
userAgent; - 3.17.0:修复
enqueueLinks中自定义 userAgent 不生效的问题。
当前选项签名:
respectRobotsTxtFile?: boolean | { userAgent?: string };开启后,爬虫在请求派发前调用isAllowedBasedOnRobotsTxtFile()检查对应源的 robots.txt(按 origin 缓存于LruCache,容量 1000),不合法则跳过并触发onSkippedRequest(reason 为robotsTxt);若 robots.txt 声明了Crawl-delay,该延迟会通过applyCrawlDelay()交给请求管理器执行。enqueueLinks/addRequests入队时同样会过滤被 robots.txt 禁止的 URL。默认 userAgent 为*,可通过{ userAgent: 'MyBot' }指定。
5.3 onSkippedRequest:感知被跳过的请求
3.13.2 引入onSkippedRequest回调,当前覆盖四类跳过原因:
- robots.txt 不允许(
robotsTxt); - 不匹配
enqueueLinks的 include/exclude 过滤(filters); - 重定向后不再匹配入队策略(
redirect); - 达到
maxRequestsPerCrawl上限(limit)。
爬虫还会用logOnce保证“达到 maxRequestsPerCrawl / maxCrawlDepth 上限”这类提示只打印一次(对应 3.16.0 的“maxCrawlDepth warning is logged only once”)。
六、爬取深度与总请求上限
6.1 maxRequestsPerCrawl
maxRequestsPerCrawl是防止死循环的关键保护:达到上限后isTaskReadyFunction返回false,不再派发新请求,正在进行的请求允许完成(3.17.0 修复了它可能导致请求被意外丢弃的问题)。入队侧同步受限:#calculateEnqueuedRequestLimit()会用“上限 − 已处理数 − 待处理数”计算出剩余预算,作为addRequestsBatched的maxNewRequests。
6.2 maxCrawlDepth
3.14.0 新增maxCrawlDepth选项。语义为:
0:只处理初始请求,跳过所有入队的新链接;1:处理初始请求及初始请求处理器中入队的链接;- 未设置:不限制深度。
深度由context.addRequests/context.enqueueLinks通过addCrawlDepthRequestGenerator注入(crawlDepth = request.crawlDepth + 1),入队时检查crawlDepth > maxCrawlDepth即跳过。3.15.0 修复了自定义transformRequestFunction场景下深度校验不生效的问题。
七、数据输出与状态管理
7.1 数据集操作
BasicCrawler内置了默认数据集助手(3.5.3 的“default dataset helpers”):pushData()、getData()、getDataset()以及exportData()(3.6.0 引入)。exportData(path, format)支持json与csv两种格式,格式可从路径后缀自动推断;3.15.0 为其加入collectAllKeys选项,导出 CSV 时汇总所有记录的全部键而非仅首条记录的键。3.15.0 还修复了空数据集下exportData报错的问题。
7.2 状态持久化:useState 与 id
useState()基于 KeyValueStore 的getAutoSavedValue实现,键为CRAWLEE_STATE。多个爬虫实例共用该键会互相覆盖,因此 3.18.0 版本配套了id选项:指定id后状态键变为CRAWLEE_STATE_<id>,每个实例状态隔离且跨重启持久化;多个无id实例同时调用useState()会收到警告。该id同时也用于状态消息事件与队列别名隔离。
7.3 状态消息与统计
setStatusMessage(message, options):按level(默认DEBUG)记录日志,并通过EventType.STATUS_MESSAGE事件广播(3.3.0 引入,3.3.0 将实现从存储层移入 Crawlee);3.10.4 起不再 await 该调用,避免拖慢爬虫;statusMessageLoggingInterval:默认 10 秒(3.3.3 曾固定为 5 秒并以 debug 级别输出);statusMessageCallback:3.5.0 起可自定义状态消息内容,回调需显式调用crawler.setStatusMessage();默认消息包含成功数/总数、失败数及期望并发度(3.10.3 加入 desired concurrency);statistics:可注入自定义统计实例(3.7.0 起支持配置),stateExtension可扩展自定义字段;3.13.3 修复统计中记录真实request.retryCount的问题;3.10.0 修复迁移/复活/续跑时统计丢失的问题;- 3.17.0 修复了周期日志中失败请求增量计数错误的问题。
八、运行生命周期:run / stop / pause / resume / teardown
8.1 run() 的多次运行
3.3.2 起允许同一爬虫实例多次调用run():重复运行时继续消费同一个请求管理器,上次已处理(含失败)的请求不会被再次处理;爬虫自建的统计与会话池会在新一次运行前重置,而注入的实例保持原状。若请求管理器中的请求全部已处理,新一次run()会处理 0 个请求,爬虫会给出“queue 不会自动清空,请 purge 或换新队列”的警告。
8.2 优雅停止:stop()
3.12.2 引入BasicCrawler.stop():停止接收新请求,允许在途请求完成后再结束(内部由CrawlerRun.stop(reason)置位stopRequested)。3.16.0 修复了多次调用stop()的处理——重复调用是幂等的,只会记录一次停止原因日志。run()会保持 pending 直到所有在途请求收尾。
8.3 暂停与恢复:pause() / resume()
pause(timeoutSecs?)停止派发新请求、等待在途请求收尾(超时可拒),但不结束运行——run()仍挂起,需resume()恢复派发。这常用于迁移(migration)场景:pauseOnMigration()在收到MIGRATING/ABORTING事件时暂停派发,等待至多 20 秒(SAFE_MIGRATION_WAIT_MILLIS)让健康请求完成,再持久化RequestList状态(3.1.3 修复了迁移事件处理中的内存泄漏)。
8.4 立即终止:teardown() / destroy()
teardown():立即结束当前运行,不等待在途请求;每次run()结束都会调用,释放的是“单次运行”级别的资源(会话池、事件管理器、任务循环);destroy():释放跨运行存活的资源(如浏览器爬虫的浏览器池),适用于彻底弃用爬虫实例前调用;Symbol.asyncDispose委托给destroy(),因此支持await using语法。
3.12.2 修复了CriticalError下的优雅清理,3.18.0 修复了“avoid duplicate final crawler persistence”,确保最终持久化只执行一次,且最终状态消息(isStatusMessageTerminal: true)可靠送达(3.18.0)。
九、事务化存储与 HTTP 客户端扩展
9.1 transactionalStorage
3.18.0 时代的源码中,transactionalStorage默认开启(true):请求处理期间的存储写入被记录在横跨整个请求生命周期的StorageTransaction中,仅当 requestHandler 成功时才提交,因此异常抛出不会留下部分写入、重试也不会重复写入。可通过false关闭,或用对象按存储类型覆盖策略(如{ requestQueue: 'deferred' });withDirectStorageAccess是单点绕过事务的出口,useState()则刻意不参与事务。
9.2 可插拔 HTTP 客户端
3.12.0 起允许使用其他 HTTP 客户端。爬虫默认使用LazyDefaultHttpClient:优先动态加载@crawlee/impit-client的ImpitHttpClient(提供代理支持与浏览器指纹),未安装时回退到原生FetchHttpClient(此时代理与指纹能力不可用,并给出警告)。可通过httpClient选项注入自定义客户端。sendRequest上下文助手即经由该客户端执行,见 send-request.ts。
十、实战示例:组装一个完整的 BasicCrawler
结合上述能力,一个兼顾入口过滤、深度限制、robots.txt 与状态消息的典型配置如下:
import { BasicCrawler, Dataset, createBasicRouter } from 'crawlee'; const router = createBasicRouter(); router.addHandler('detail', async ({ request, sendRequest, log }) => { const { body } = await sendRequest({ url: request.url }); await Dataset.pushData({ url: request.url, html: body }); log.info(`Processed ${request.url}`); }); router.addDefaultHandler(async ({ request, log }) => { log.info(`Unlabelled request: ${request.url}`); }); const crawler = new BasicCrawler({ id: 'my-crawler', requestHandler: router, maxRequestsPerCrawl: 100, maxCrawlDepth: 2, sameDomainDelaySecs: 1, respectRobotsTxtFile: { userAgent: 'MyCrawlerBot/1.0' }, requestHandlerTimeoutSecs: 30, maxRequestRetries: 3, onSkippedRequest({ request, reason }) { console.log(`Skipped ${request.url}: ${reason}`); }, async requestHandler() {}, // 实际由 router 承担 }); await crawler.run([ { url: 'https://example.com/', label: 'detail', userData: { source: 'home' } }, ]); await crawler.exportData('./out/result.json'); // 导出默认数据集十一、版本演进速查表
| 版本 | 关键变化 |
|---|---|
| 3.3.0 | setStatusMessage基础支持;状态消息实现移入 Crawlee |
| 3.3.3 | Request.maxRetries支持单请求覆盖全局重试上限 |
| 3.4.2 | internalTimeoutMillis兜底;retryOnBlocked检测被封锁页面 |
| 3.5.0 | 状态消息可配置;sameDomainDelay支持;会话因代理错误轮换 |
| 3.5.3 | 默认数据集助手 |
| 3.6.0 | crawler.exportData() |
| 3.7.0 | 可配置统计;retryOnBlocked记录原因 |
| 3.8.0 | 上下文访问状态/KVS/数据集;自适应 Playwright 爬虫 |
| 3.9.0 | tieredProxyUrls;入队后通知自动伸缩池 |
| 3.10.0 | ErrorSnapshotter;RequestQueue v2 成为默认 |
| 3.11.0 | sitemap 请求列表 |
| 3.12.0 | 可插拔 HTTP 客户端;BasicCrawler.stop()优雅停止 |
| 3.13.0 | 简化 RequestQueueV2 实现 |
| 3.13.1 | respectRobotsTxtFile选项;RobotsTxtFile重命名 |
| 3.13.2 | onSkippedRequest |
| 3.14.0 | maxCrawlDepth |
| 3.15.0 | TandemRequestProvider;collectAllKeys导出选项 |
| 3.15.3 | 自定义 userAgent + robots.txt |
| 3.16.0 | 多次stop()安全;maxCrawlDepth警告只打一次 |
| 3.17.0 | 修复maxRequestsPerCrawl意外丢请求;失败增量计数修正 |
| 3.18.0 | 按路由 label 的类型安全 userData 映射与 opt-in schema 校验;修复最终持久化重复;终端状态消息可靠送达 |
十二、总结
从 packages/basic-crawler/CHANGELOG.md 可以看出,BasicCrawler的演进主线是把“可靠抓取”拆解为一系列可组合、可观测的机制:请求来源统一到requestManager、并发交给ConcurrencySystem、超时分层(handler 级 + internal 级 + 导航窗口级)、防封锁由会话池与 robots.txt 协作、生命周期由CrawlerRun统一管理。理解这些分层,既能帮助排查“请求为什么被跳过”“为什么没有重试”“为什么队列空了还在运行”等常见问题,也能在需要时通过taskLoopOptions、concurrencySystem、statistics、httpClient等注入点把BasicCrawler深度定制为适合自己业务的抓取引擎。
【免费下载链接】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),仅供参考