首页
/ Scrapling Spider 爬虫架构解析:从请求到结果集的异步爬取流水线

Scrapling Spider 爬虫架构解析:从请求到结果集的异步爬取流水线

2026-09-06 16:07:48作者:董灵辛Dennis

本篇技术文章以 docs/spiders/architecture.md 为蓝本,系统讲解 Scrapling 蜘蛛(Spider)系统的整体架构:Spider、Crawler Engine、Scheduler、Session Manager、Checkpoint 与 Response Cache 六大组件如何协同工作,并深入 scrapling/spiders/ 的源码实现。读完后,你将理解一个请求从入队到产出数据项的完整生命周期,掌握并发、限速、阻塞重试与断点续爬的实现机制,并知道如何与 Scrapy 的概念体系做对照迁移。

一、总体数据流:一个爬取任务是如何运转的

Scrapling 的蜘蛛系统是一个受 Scrapy 启发的异步爬取框架,面向并发、多会话的爬取场景,内置暂停/恢复(pause/resume)能力。它将 Scrapling 自身的解析引擎与 Fetchers 组合为统一的爬取 API,并在此之上增加了调度(scheduling)、并发控制和检查点(checkpointing)。熟悉 Scrapy 的读者会感到非常亲切;不熟悉也没关系,系统设计本身很直接。

整体数据流如下图所示(图源 docs/assets/spider_architecture.png):

Spider 系统架构数据流图,展示 Spider、Scheduler、Crawler Engine、Session、回调与 Checkpoint 之间的请求与响应流向

当爬虫运行时,数据按以下步骤流动(对应 scrapling/spiders/engine.pyCrawlerEngine.crawl() 的主循环):

  1. Spider 产出第一批 Request 对象。默认情况下,start_urls 中每个 URL 生成一个请求;你也可以重写 start_requests() 实现自定义逻辑。
  2. Scheduler 接收请求并放入优先级队列,同时为它们计算指纹(fingerprint)。高优先级请求先出队。
  3. Crawler Engine 向 Scheduler 请求下一个请求,出队时遵守并发上限(全局与每域名)和下载延迟。若开启了 robots_txt_obey,引擎会先检查该域名的 robots.txt 规则——被禁止的请求会被静默丢弃。引擎拿到请求后交给 Session Manager,按请求的 sid(session ID)路由到正确的会话。
  4. Session 抓取页面,把 Response 对象返回给引擎。引擎记录统计信息并检查是否为被阻塞(blocked)响应;若被阻塞,引擎最多重试 max_blocked_retries 次。阻塞检测和重试逻辑都可以自定义。
  5. 引擎把 Response 交给请求的回调(callback)。回调要么 yield 一个字典(被当作抓取到的 item),要么 yield 一个后续请求(送回调度器排队)。
  6. 从第 2 步开始循环,直到调度器为空且没有活跃任务,或者蜘蛛被暂停。
  7. 如果启动时设置了 crawldir,引擎会周期性地保存检查点(待处理请求 + 已见 URL 集合)到磁盘;优雅退出(Ctrl+C)时再保存最后一次。下次用同一个 crawldir 运行时,蜘蛛从上次的位置恢复,跳过 start_requests(),还原调度器状态。

二、核心组件逐一拆解

2.1 Spider:你直接交互的中心类

Spider 是你继承并配置的抽象基类,定义在 scrapling/spiders/spider.py。你需要子类化 Spider,定义 start_urlsparse() 方法,可选地配置会话并覆写生命周期钩子:

from scrapling.spiders import Spider, Response, Request

class MySpider(Spider):
    name = "my_spider"
    start_urls = ["https://example.com"]

    async def parse(self, response: Response):
        for link in response.css("a::attr(href)").getall():
            yield response.follow(link, callback=self.parse_page)

    async def parse_page(self, response: Response):
        yield {"title": response.css("h1::text").get("")}

从源码可以看到 Spider 提供了一整套类属性(scrapling/spiders/spider.py#L71-L104),这些就是你在子类里直接覆盖的"配置项":

类属性 默认值 作用
name None(必填,否则构造时报错) 蜘蛛名称,也用于日志与开发缓存目录
start_urls [] 起始 URL 列表
allowed_domains set() 域名白名单,空表示不限制
robots_txt_obey False 是否遵守 robots.txt
development_mode False 是否启用开发模式(响应缓存回放)
development_cache_dir None 开发缓存目录,默认 .scrapling_cache/{name}
concurrent_requests 4 全局并发请求数上限
concurrent_requests_per_domain 0(关闭) 每域名并发上限
download_delay 0.0 下载延迟(秒)
max_blocked_retries 3 被阻塞请求的最大重试次数
autothrottle_enabled False 是否启用自动限速
autothrottle_start_delay / max_delay 5.0 / 60.0 自动限速的起始/最大延迟
autothrottle_target_concurrency None 目标并发,缺省回退到 concurrent_requests_per_domain 或 1.0
autothrottle_block_backoff True 检测到阻塞时是否加倍退避
fp_include_kwargs / fp_keep_fragments / fp_include_headers False 指纹计算时是否纳入额外 kwargs、URL fragment、请求头
logging_level / logging_format / log_file DEBUG / 带时间戳格式 / None 日志级别、格式与文件输出

几个值得注意的实现细节:

  • 构造参数Spider.__init__(crawldir=None, interval=300.0)crawldir 提供后即启用暂停/恢复;interval 是周期性检查点保存的间隔(秒),默认 300 秒。检查点间隔的合法性在 scrapling/spiders/checkpoint.py 中校验:必须是非负整数或浮点数。
  • 会话配置configure_sessions(manager) 是注入会话的入口,默认实现注册一个 FetcherSession(ID 为 default);manager.add() 时第一个加入的会话成为 start_requests() 的默认会话,也可以显式用 default=True 指定。若没有添加任何会话会抛出 SessionConfigurationError
  • 生命周期钩子on_start(resuming)resuming=True 表示从检查点恢复)、on_close()on_error(request, error)on_scraped_item(item)(返回 None 可静默丢弃该 item)、is_blocked(response)(默认按状态码集合 {401, 403, 407, 429, 444, 500, 502, 503, 504} 判定)、retry_blocked_request(request, response)(重试前定制请求)。
  • 运行入口start(use_uvloop=False, **backend_options) 通过 anyio 在内部处理异步执行,返回 CrawlResultstream() 则是异步生成器,边爬边逐条产出 item,适合长时间运行的蜘蛛或在其上构建应用,迭代期间可用 spider.stats 读取实时统计。注意源码注释明确说明:stream() 模式下不提供 SIGINT 的暂停/恢复处理。
  • 优雅暂停start() 会注册 SIGINT 处理器——第一次 Ctrl+C 请求优雅暂停(等待在途请求完成),第二次 Ctrl+C 强制立即停止;这对应引擎里的 request_pause() 状态机(第一次置 _pause_requested,第二次置 _force_stop)。

2.2 Crawler Engine:编排整个爬取过程

引擎(scrapling/spiders/engine.py 中的 CrawlerEngine)管理主循环、强制并发限制、通过 Session Manager 分发请求、处理回调结果。你不直接和它交互——Spider.start()Spider.stream() 替你处理。

crawl() 的主循环(engine.py#L351-L446)可以读出它的行为契约:

  • 启动时若启用了检查点,会先尝试 restore;恢复成功则跳过 start_requests()
  • 若开启 robots 合规,会先从 start_urls 提取域名预取 robots.txt(_prefetch_robots_txt);
  • 主循环里每轮先检查暂停标志、是否到了周期性检查点时间,再判断队列是否清空;只有当活跃任务数 < concurrent_requests 时才从调度器出队并派发新任务,避免生成数千个等待中的任务;
  • 单个请求的处理流程 _process_request 依次为:robots.txt 检查(被禁则计入 robots_disallowed_count 后跳过)→ 开发缓存命中检查 → 进入并发限流器 → 按 download_delay(或自动限速、robots 的 Crawl-delay/Request-rate 计算值)休眠 → 经 Session Manager 抓取 → 记录统计与延迟 → 阻塞检测与重试 → 执行回调。
  • 结束时的 finally 块会调用 spider.on_close(),并且仅在非暂停的正常完成时清理检查点文件——这保证了暂停后文件仍在盘上可供下次恢复。

2.3 Scheduler:带 URL 去重的优先级队列

实现见 scrapling/spiders/scheduler.py。它基于 asyncio.PriorityQueue,请求按"负优先级 + 入队计数"排序,保证高优先级先出队、同优先级 FIFO。去重逻辑在 enqueue() 中:除非 dont_filter=True,否则指纹已存在于 _seen 集合的请求会被丢弃。

指纹本身由 scrapling/spiders/request.pyRequest.update_fingerprint() 计算:以 sid、请求体(data/json 序列化后的 hex)、HTTP method、经 w3lib.url.canonicalize_url 规范化的 URL 为基础,再按蜘蛛配置可选纳入额外 kwargs 与请求头,最后用 SHA-1 摘要。蜘蛛上的 fp_include_kwargsfp_include_headersfp_keep_fragments 三个开关直接控制指纹的"严格程度"——例如默认丢弃 URL fragment,同一个页面的 #anchor 变体只会被抓取一次。

Scheduler 还提供 snapshot()(返回待处理请求列表 + 已见指纹集合)和 restore()(恢复两者并重建队列)两个方法,是检查点系统的状态源。dequeue() 出的请求会被记录到 _inflight 中,直到 complete() 被调用才从检查点跟踪中移除,确保"正在处理的请求"不会出现在检查点里。

2.4 Session Manager:多会话路由

实现见 scrapling/spiders/session.py。它管理一组具名的会话实例,每个会话是三选一:

请求进来时,fetch()request.sid 路由到对应会话;sid 为空则回退到默认会话。两个关键机制:

  • 懒加载会话manager.add(sid, session, lazy=True) 注册的会话不会随爬虫启动,而是在首次被请求命中时(在锁保护下)才 __aenter__() 启动。这对昂贵的浏览器会话很有用——不用的浏览器引擎永远不会被启动。
  • 统一响应形态:抓取后 response.request 被回指到该请求,且 request.metaresponse.meta 合并(response 侧优先),这使回调里可以同时拿到两者。

源码中还可以看到针对 FetcherSession 的特化路径:直接调用底层 _ASyncSessionLogic._make_request(),而浏览器类会话走 session.fetch(url, **kwargs),请求上的任意关键字参数(headers、proxy、method 等)都会透传给会话层。

2.5 Checkpoint System:断点续爬

可选系统,启用条件是构造 Spider 时传入 crawldir。它将爬虫状态(待处理请求 + 已见 URL 指纹集合,即 CheckpointData)序列化为 pickle 文件 checkpoint.pkl,实现见 scrapling/spiders/checkpoint.py

  • 原子写入:先写临时文件 checkpoint.pkl.tmp,再 replace() 为正式文件,防止写入中途崩溃导致损坏;失败时清理临时文件。
  • 保存时机:主循环里按 interval 周期保存;暂停时(在取消任务组之前)保存最后一次;加载失败或无检查点时直接全新启动。
  • 自动清理:爬取正常完成(非暂停)后删除检查点文件。
  • 回调的持久化Request 在 pickle 时不保存可调用对象本身,而是把回调记为方法名字符串(__getstate__/__setstate__);恢复时由引擎调用 request._restore_callback(spider) 从蜘蛛实例上按名字找回方法,找不到则回退到 parse

2.6 Response Cache:开发模式的响应回放

可选缓存,当 development_mode = True 时启用(见 scrapling/spiders/cache.py):每个抓到的响应按请求指纹落盘为 {fingerprint}.json,body 做 base64 编码以保证二进制内容可完整还原,同时保存 status、reason、encoding、headers、request_headers 与 cookies。缓存写入同样是"临时文件 + 替换"的原子方式。

它的设计意图是:在本地反复调试 parse() 逻辑时不再反复请求目标服务器,不用于生产环境。缓存命中/未命中分别计入统计里的 cache_hits / cache_misses,引擎在开发模式下会打印一条 warning 提示缓存已开启。

2.7 Output:ItemList 与 CrawlStats

抓取到的 item 收集在 ItemList 中——一个带导出能力的 list 子类(scrapling/spiders/result.py),提供四种导出方法:

  • to_json(path, indent=False):JSON 数组,支持 2 空格缩进;
  • to_jsonl(path):JSON Lines,每行一个对象;
  • to_csv(path, fields=None, delimiter=","):列取所有 item 中出现过的 key(可按出现顺序),缺格留空,嵌套的 dict/list 值会写成 JSON 字符串;
  • to_xml(path, root_tag="items", item_tag="item", indent=True):key 非法时改写标签名并保留原名在 name 属性中,非标量值写为 JSON。

运行结果封装在 CrawlResultstats + items + paused),其中 result.completed 即"非暂停的正常完成"。统计由 CrawlStats dataclass 承载,字段包括:requests_countfailed_requests_countoffsite_requests_countrobots_disallowed_countblocked_requests_countcache_hits/missesitems_scraped/items_droppedresponse_bytes(及按域名分列)、response_status_countsessions_requests_count(按会话分列)、proxiesautothrottle_delays(各域名的最终延迟)、custom_statslog_count(各日志级别计数),并派生 elapsed_secondsrequests_per_second。爬取结束时引擎会 log.info 打印完整统计 JSON。

三、并发、限速与域名控制

引擎层的限流机制在 engine.py#L138-L143 实现:全局用一个 CapacityLimiter(concurrent_requests);当 concurrent_requests_per_domain > 0 时,每个域名拥有独立的 CapacityLimiter,主循环层面还额外用"活跃任务数 < 全局并发数"来封顶,防止堆积等待任务。

域名白名单 _is_domain_allowed() 的匹配规则值得注意:精确匹配或子域名后缀匹配(domain == allowed or domain.endswith("." + allowed)),部分名称匹配会被拒绝——这一点在 tests/spiders/test_engine.py 中有对应的 test_exact_domain_matchtest_subdomain_matchtest_partial_name_not_matched 用例。回调产出的请求若不在白名单内,不会入队,而是计入 offsite_requests_count 统计。

延迟方面有三个来源,最终取最大值生效:蜘蛛的 download_delay;robots.txt 的 Crawl-delay / Request-rate 指令(_get_domain_delay() 按域名缓存结果);自动限速器 AutoThrottle(开启 autothrottle_enabled 后按各域名记录延迟与成功率动态调整,并支持从 Retry-After 头解析退避时间)。

四、阻塞检测与自动重试

默认 is_blocked() 依据 BLOCKED_CODES = {401, 403, 407, 429, 444, 500, 502, 503, 504} 判定(spider.py#L16)。当响应被判定为 blocked 时,引擎的处理(engine.py#L245-L263)是:

  1. _retry_count < max_blocked_retries,复制请求并递增重试计数,同时做三处调整:priority -= 1(避免重试请求立即插队)、dont_filter = True(绕过去重)、清空 _session_kwargs 中的 proxy/proxies(换掉可能失效的代理);
  2. 交给 retry_blocked_request() 钩子做最后的定制,再入队;
  3. 超过上限则记 warning 并放弃,该 URL 计入 blocked_requests_count

上述行为分别由 tests/spiders/test_engine.py 中的 test_blocked_response_triggers_retrytest_blocked_response_max_retries_exceededtest_retry_request_has_dont_filtertest_retry_clears_proxy_kwargs 等用例验证。

五、与 Scrapy 的概念对照表

如果你是 Scrapy 用户,以下是 Scrapling 蜘蛛系统的对应关系(继承自原文档):

概念 Scrapy Scrapling
Spider 定义 scrapy.Spider 子类 scrapling.spiders.Spider 子类
初始请求 start_requests() async start_requests()
回调 def parse(self, response) async def parse(self, response)
跟进链接 response.follow(url) response.follow(url)
Item 输出 yield dictyield Item yield dict
请求调度 Scheduler + Dupefilter 内置去重的 Scheduler
下载层 Downloader + Middlewares 支持多会话的 Session Manager
Item 处理 Item Pipelines on_scraped_item() 钩子
阻塞检测 自定义中间件 内置 is_blocked() + retry_blocked_request() 钩子
并发 CONCURRENT_REQUESTS 设置 concurrent_requests 类属性
域名过滤 allowed_domains allowed_domains
Robots.txt ROBOTSTXT_OBEY 设置 robots_txt_obey 类属性
暂停/恢复 JOBDIR 设置 crawldir 构造参数
导出 Feed exports result.items.to_json() / to_jsonl() / to_csv() / to_xml() 或自定义钩子
运行方式 scrapy crawl spider_name MySpider().start()
流式输出 N/A async for item in spider.stream()
多会话 N/A 每个蜘蛛可挂多个不同类型的会话

两个结构性差异值得强调:Scrapy 用全局 settings 字典配置,Scrapling 把这些全部下沉为蜘蛛类属性,配置即代码,随类走;Scrapy 的下载中间件链被"多类型会话 + 每会话 sid 路由"取代——同一个蜘蛛里既可以用 FetcherSession 抓静态页,又可以用 AsyncStealthySession 抓反爬严格的页面,按 sid 分流。

六、小结与延伸阅读

Scrapling 蜘蛛系统的架构可以概括为一句话:Spider 声明意图,Engine 驱动循环,Scheduler 负责排队去重,Session Manager 负责按 sid 分流,Checkpoint 负责状态持久化,ItemList/CrawlStats 负责产出与度量。所有组件都集中在 scrapling/spiders/ 目录,核心逻辑约两千行,配合 tests/spiders/ 下对引擎、调度器、检查点、会话、模板等的成套测试,是一个可读性很高的异步爬取参考实现。

想继续深入,建议按以下路径阅读:

登录后查看全文
热门项目推荐
相关项目推荐