Scrapling Spider 爬虫架构解析:从请求到结果集的异步爬取流水线
本篇技术文章以 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):
当爬虫运行时,数据按以下步骤流动(对应 scrapling/spiders/engine.py 中 CrawlerEngine.crawl() 的主循环):
- Spider 产出第一批
Request对象。默认情况下,start_urls中每个 URL 生成一个请求;你也可以重写start_requests()实现自定义逻辑。 - Scheduler 接收请求并放入优先级队列,同时为它们计算指纹(fingerprint)。高优先级请求先出队。
- Crawler Engine 向 Scheduler 请求下一个请求,出队时遵守并发上限(全局与每域名)和下载延迟。若开启了
robots_txt_obey,引擎会先检查该域名的 robots.txt 规则——被禁止的请求会被静默丢弃。引擎拿到请求后交给 Session Manager,按请求的sid(session ID)路由到正确的会话。 - Session 抓取页面,把
Response对象返回给引擎。引擎记录统计信息并检查是否为被阻塞(blocked)响应;若被阻塞,引擎最多重试max_blocked_retries次。阻塞检测和重试逻辑都可以自定义。 - 引擎把
Response交给请求的回调(callback)。回调要么yield一个字典(被当作抓取到的 item),要么yield一个后续请求(送回调度器排队)。 - 从第 2 步开始循环,直到调度器为空且没有活跃任务,或者蜘蛛被暂停。
- 如果启动时设置了
crawldir,引擎会周期性地保存检查点(待处理请求 + 已见 URL 集合)到磁盘;优雅退出(Ctrl+C)时再保存最后一次。下次用同一个crawldir运行时,蜘蛛从上次的位置恢复,跳过start_requests(),还原调度器状态。
二、核心组件逐一拆解
2.1 Spider:你直接交互的中心类
Spider 是你继承并配置的抽象基类,定义在 scrapling/spiders/spider.py。你需要子类化 Spider,定义 start_urls 和 parse() 方法,可选地配置会话并覆写生命周期钩子:
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 在内部处理异步执行,返回CrawlResult;stream()则是异步生成器,边爬边逐条产出 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.py 的 Request.update_fingerprint() 计算:以 sid、请求体(data/json 序列化后的 hex)、HTTP method、经 w3lib.url.canonicalize_url 规范化的 URL 为基础,再按蜘蛛配置可选纳入额外 kwargs 与请求头,最后用 SHA-1 摘要。蜘蛛上的 fp_include_kwargs、fp_include_headers、fp_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.meta与response.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。
运行结果封装在 CrawlResult(stats + items + paused),其中 result.completed 即"非暂停的正常完成"。统计由 CrawlStats dataclass 承载,字段包括:requests_count、failed_requests_count、offsite_requests_count、robots_disallowed_count、blocked_requests_count、cache_hits/misses、items_scraped/items_dropped、response_bytes(及按域名分列)、response_status_count、sessions_requests_count(按会话分列)、proxies、autothrottle_delays(各域名的最终延迟)、custom_stats 与 log_count(各日志级别计数),并派生 elapsed_seconds 与 requests_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_match、test_subdomain_match、test_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)是:
- 若
_retry_count < max_blocked_retries,复制请求并递增重试计数,同时做三处调整:priority -= 1(避免重试请求立即插队)、dont_filter = True(绕过去重)、清空_session_kwargs中的proxy/proxies(换掉可能失效的代理); - 交给
retry_blocked_request()钩子做最后的定制,再入队; - 超过上限则记 warning 并放弃,该 URL 计入
blocked_requests_count。
上述行为分别由 tests/spiders/test_engine.py 中的 test_blocked_response_triggers_retry、test_blocked_response_max_retries_exceeded、test_retry_request_has_dont_filter、test_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 dict 或 yield 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/ 下对引擎、调度器、检查点、会话、模板等的成套测试,是一个可读性很高的异步爬取参考实现。
想继续深入,建议按以下路径阅读:
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
