ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Scrapling Spider 进阶实战:并发控制、AutoThrottle 自适应限速、断点续爬、开发模式与结果统计

Scrapling Spider 进阶实战:并发控制、AutoThrottle 自适应限速、断点续爬、开发模式与结果统计 Scrapling Spider 进阶实战并发控制、AutoThrottle 自适应限速、断点续爬、开发模式与结果统计【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling本文围绕 Scrapling 爬虫框架Spider的进阶能力展开覆盖并发控制全局/按域限流与下载延迟、AutoThrottle 自适应延迟及其被封禁回退机制、基于 checkpoint 的暂停与恢复、用于本地调试的 Development Mode 响应缓存、stream()实时流式输出、生命周期钩子、CrawlResult/CrawlStats统计与日志配置。读完本文后你可以为生产级爬虫写出既礼貌又高效、可中断可恢复、可离线调试、可嵌入应用实时取数的完整方案并能看懂每一项配置在 scrapling/spiders/engine.py、scrapling/spiders/throttle.py 等源码中的真实落地方式。1. 并发控制全局与按域的双重限流Scrapling 的 Spider 基类通过几个类属性控制爬虫的激进程度全部定义在 scrapling/spiders/spider.py属性默认值说明concurrent_requests4同一时刻最多处理多少个请求全局上限concurrent_requests_per_domain0每个域名的最大并发数0 表示不限制单域并发download_delay0.0每个请求发出前固定等待的秒数robots_txt_obeyFalse是否遵守 robots.txt 规则Disallow、Crawl-delay、Request-rateclass PoliteSpider(Spider): name polite start_urls [https://example.com] # Be gentle with the server concurrent_requests 4 concurrent_requests_per_domain 2 download_delay 1.0 # Wait 1 second between requests async def parse(self, response: Response): yield {title: response.css(title::text).get()}双层限流器如何工作当设置了concurrent_requests_per_domain时引擎会在全局限流器之外为每个域名单独创建一个限流器。在 scrapling/spiders/engine.py 的_rate_limiter()中可以看到若未启用按域限流直接复用全局CapacityLimiter启用后则按域名懒加载各自的CapacityLimiter每个域独立的 anyio 容量槽位。def _rate_limiter(self, domain: str) - CapacityLimiter: Get or create a per-domain concurrency limiter if enabled, otherwise use the global limiter. if self.spider.concurrent_requests_per_domain: self._domain_limiters.setdefault(domain, CapacityLimiter(self.spider.concurrent_requests_per_domain)) return self._domain_limiters[domain] return self._global_limiter这意味着当你同时爬多个域名时可以在保持较高全局并发的同时对每个具体域名保持礼貌——某个慢站点占满它的按域额度不会拖垮其他域名的吞吐。此外主循环还会用concurrent_requests作为任务生成上限避免一次性 spawn 出成千上万个等待中的任务见 scrapling/spiders/engine.py。提示download_delay会为每个请求不分域名增加固定等待适合做最简单的速率限制。它对所有请求生效因此适合全站统一慢速的场景更细粒度的按域调整请结合下一节的 AutoThrottle。2. AutoThrottle用真实响应延迟替代拍脑袋的固定延迟固定download_delay本质上是猜猜低了被封猜高了爬虫跑到半夜。AutoThrottle 用观察代替猜测——它监控每个域名的真实响应耗时并独立调整每个域的延迟遇到快服务器就提速遇到慢服务器或敌对站点就退避。开关与参数同样以类属性配置默认值见 scrapling/spiders/spider.py属性默认值说明autothrottle_enabledFalse开启自适应延迟autothrottle_start_delay5.0首次请求某个域名时使用的延迟autothrottle_max_delay60.0节流允许达到的最高延迟autothrottle_target_concurrencyNone每个域名希望保持的在途请求数见下文autothrottle_block_backoffTrue每次被该域名封禁就将延迟翻倍class AdaptiveSpider(Spider): name adaptive start_urls [https://example.com] concurrent_requests 8 concurrent_requests_per_domain 1 autothrottle_enabled True autothrottle_start_delay 2.0 autothrottle_max_delay 30.0 async def parse(self, response: Response): yield {title: response.css(title::text).get()}延迟如何收敛到服务器真实速度核心算法在 scrapling/spiders/throttle.py 的record()中每次响应结束后目标延迟取latency / target_concurrency当前延迟按均值滑动靠近目标max((current target) / 2, target)。因此一个 0.5 秒响应的站点会收敛到约 0.5 秒的延迟——大致相当于任何时刻只有一个请求在途。延迟尖峰会立即反映取最大值而非平均所以服务器一旦开始变慢爬虫立刻退避而提速则是渐进的。目标并发数每个域在途多少请求的解析顺序在 scrapling/spiders/engine.pyautothrottle_target_concurrency若你显式设置→concurrent_requests_per_domain→1.0。延迟会再被该值除一次所以你通常完全不用配置目标并发——你已配置好的按域上限直接复用。tests/spiders/test_throttle.py中的test_explicit_target_concurrency_wins、test_target_concurrency_falls_back_to_the_per_domain_limit、test_target_concurrency_falls_back_to_one三个用例精确验证了这一优先级。另外两处约束floor地板值保证延迟永远不低于 spider 自身配置的download_delay或 robots.txt 的Crawl-delay见 scrapling/spiders/engine.py 的_get_domain_delay()它取 spider 延迟与Crawl-delay/Request-rate的最大值并缓存max_delay是天花板两者在record()末尾被钳制new_delay min(max(new_delay, floor), self.max_delay)。被封禁时的回退Backing off when blocked单靠延迟无法识别限流429、403或验证码页面往往比真实内容更快返回——若只看延迟反而像在邀请你加速。因此任何非健康响应状态码非 2xx或你的is_blocked()返回 True 的响应都会触发翻倍该域延迟的逻辑0.5s - 1s - 2s - 4s - 8s ... (up to autothrottle_max_delay)源码中该因子为BLOCK_BACKOFF_FACTOR 2.0scrapling/spiders/throttle.py且有硬性保证封禁永远不会让爬虫提速——new_delay max(new_delay, penalty, current_delay)即新延迟至少不低于被封禁前的值见 scrapling/spiders/throttle.py对应测试test_each_block_doubles_the_delay、test_backoff_stops_at_max_delay。当站点通过Retry-After头明确告知等待时长429或503上时该值优先于翻倍。数字形式Retry-After: 120和 HTTP-date 形式都被支持解析逻辑在parse_retry_after()scrapling/spiders/throttle.py且Retry-After同样受autothrottle_max_delay上限约束测试test_retry_after_is_capped_by_max_delay。爬虫会持续降速直到站点停止拒绝健康响应恢复后普通的均值滑动又会把延迟逐步拉回服务器的真实速度无需重启任何东西即可自愈测试test_healthy_responses_bring_the_delay_back_down验证了恢复过程。设置autothrottle_block_backoff False可关闭此机制回退到纯延迟驱动节流——此时封禁只会阻止提速不会主动翻倍。每域的最终延迟会出现在统计里result AdaptiveSpider().start() print(result.stats.autothrottle_delays) # {example.com: 0.62}注意事项download_delay和 robots.txt 的Crawl-delay构成地板值AutoThrottle 只会把它们往上调礼貌设置永不被击穿。concurrent_requests_per_domain身兼二职既限制在途请求数又充当 AutoThrottle 的目标并发通常无需单独配置目标值。建议始终设置它——因为被节流的域名在睡觉期间仍占用全局限流器的一个槽位若不限单域这些睡眠槽位会挤占其他域名共享的并发预算。测量的延迟是完整取回耗时包含内部重试对浏览器会话还包含页面渲染时间。autothrottle_max_delay是总上限包括Retry-After。若站点要求的等待超过你的天花板请调大autothrottle_max_delay以尊重它。学习到的延迟不做 checkpoint暂停后恢复每个域都会从autothrottle_start_delay重新学起引擎在每次crawl()开始时调用reset()见 scrapling/spiders/engine.py。使用 uvloop 提升事件循环吞吐start()方法接受use_uvloop参数在可用时使用更快的 uvloopLinux/macOS或 winloopWindows事件循环实现result MySpider().start(use_uvloopTrue)对 I/O 密集的爬取可改善吞吐。需要说明两点前提见 scrapling/spiders/spider.py一是需单独安装uvloop或winloop二是该参数最终通过backend_options传给anyio.run即if available——未安装相应库时不会报错但也不会真正生效。start()还支持透传其他backend_options例如MySpider().start(backend_options{loop: my_loop})。3. 暂停与恢复基于 Checkpoint 的优雅中断Spider 支持通过 checkpoint 实现优雅的暂停-恢复。启用方式是在 Spider 构造器中传入crawldir目录spider MySpider(crawldircrawl_data/my_spider) result spider.start() if result.paused: print(Crawl was paused. Run again to resume.) else: print(Crawl completed!)工作机制暂停爬取中按CtrlC。Spider 等待所有在途请求完成保存 checkpoint挂起请求队列 已见请求指纹集合然后退出。强制停止第二次按CtrlC立即停止不再等待活跃任务。恢复用相同crawldir再次运行。引擎检测到 checkpoint 后恢复队列与已见集合从中断处继续并跳过start_requests()scrapling/spiders/engine.py。清理爬取正常完成非暂停时checkpoint 文件自动删除scrapling/spiders/engine.py。checkpoint 默认每 5 分钟也会自动保存一次防止进程意外崩溃丢失过多进度。间隔可在构造器调整# Save checkpoint every 2 minutes spider MySpider(crawldircrawl_data/my_spider, interval120.0)写入是原子操作CheckpointManager.save()先写checkpoint.pkl.tmp再replace()为checkpoint.pklscrapling/spiders/checkpoint.pytests/spiders/test_checkpoint.py的test_save_is_atomic专门验证了这一点。加载失败如文件损坏时会记录错误并从头开始而不是崩溃。提示即使未启用 checkpoint 系统爬取中按CtrlC也总是触发优雅关闭等待在途任务结束再按一次则强制立即关闭。该行为由Spider._setup_signal_handler()安装的 SIGINT 处理器驱动scrapling/spiders/spider.pyrequest_pause()第一次调用进入暂停等待、第二次调用置位_force_stopscrapling/spiders/engine.pytests/spiders/test_force_stop_checkpoint.py覆盖了双击强停的场景。判断是否处于恢复模式on_start()钩子接收resuming标志async def on_start(self, resuming: bool False): if resuming: self.logger.info(Resuming from checkpoint!) else: self.logger.info(Starting fresh crawl)resuming来自引擎在crawl()开始时尝试load()checkpoint 的结果scrapling/spiders/engine.py随后await self.spider.on_start(resumingresuming)把它交给你的代码。4. Development Mode把响应缓存到磁盘离线调试解析逻辑反复调整parse()选择器时每次都打真实服务器既慢又吵。开发模式在第一次运行时把每个响应缓存到磁盘后续运行直接从磁盘回放让你可以无限制地重跑 spider 而不发任何一个网络请求。在 Spider 上设置development_mode True即可开启class MySpider(Spider): name my_spider start_urls [https://example.com] development_mode True async def parse(self, response: Response): yield {title: response.css(title::text).get()}第一次运行正常抓取并把每个响应落盘之后的每次运行对相同请求全部走缓存完全跳过网络。引擎在初始化时若发现该开关会实例化ResponseCacheManager并打印一条警告日志scrapling/spiders/engine.py。缓存位置默认缓存在当前工作目录你运行 spider 的地方不是spider 脚本所在目录下的.scrapling_cache/{spider.name}/可用development_cache_dir覆盖class MySpider(Spider): name my_spider start_urls [https://example.com] development_mode True development_cache_dir /tmp/my_spider_cache默认目录拼接逻辑在引擎构造函数中cache_dir self.spider.development_cache_dir or f.scrapling_cache/{self.spider.name}scrapling/spiders/engine.py。工作机制缓存键每个响应以请求指纹为键。任何影响指纹的属性变化fp_include_kwargs、fp_include_headers、fp_keep_fragments见 scrapling/spiders/spider.py都会产生一次全新抓取。存储格式每个响应一个 JSON 文件命名{fingerprint_hex}.json响应体 base64 编码以精确保留二进制内容写入采用临时文件 重命名的原子方式scrapling/spiders/cache.py。cookies 的存储会保留浏览器引擎tuple与静态引擎dict各自的形状避免回放时丢 cookie。回放命中缓存时引擎完全跳过网络——包括download_delay、限流和is_blocked()重试路径缓存响应直接进入你的回调scrapling/spiders/engine.py。统计缓存命中的请求同样计入requests_count、response_bytes和各状态码计数统计输出与正常爬取一致另有cache_hits/cache_misses两个计数器反映缓存表现缓存写入发生在未命中路径见 scrapling/spiders/engine.py。清空缓存缓存没有自动过期。要强制全新爬取删除缓存目录或调用ResponseCacheManager.clear()只清理目录内的.json文件见 scrapling/spiders/cache.py。tests/spiders/test_cache.py中的test_first_run_fetches_and_caches与test_second_run_uses_cache完整演示了首跑写缓存、二跑读缓存的行为test_preserves_binary_body验证了二进制响应体的精确回放。警告开发模式仅用于开发不要用于生产。缓存响应永不过期回放还会绕过速率限制和封禁重试——不要在development_mode True的状态下交付爬虫。5. 流式输出stream() 实时取数长时运行的 Spider 或需要实时获取抓取结果的应用应使用stream()方法代替start()import anyio async def main(): spider MySpider() async for item in spider.stream(): print(fGot item: {item}) # Access real-time stats print(fItems so far: {spider.stats.items_scraped}) print(fRequests made: {spider.stats.requests_count}) anyio.run(main)与start()的关键差异stream()必须从异步上下文调用条目是随抓取逐条 yield 出来的而不是收集进列表迭代期间可通过spider.stats访问实时统计。实现上引擎通过 anyio 内存对象流容量 100把on_scraped_item放行后的条目实时推给消费端scrapling/spiders/engine.py 的__aiter__/_stream()spider.stats属性在爬虫活跃时代理到引擎的CrawlStats否则抛错scrapling/spiders/spider.py。stream()也可以与 checkpoint 系统组合使用非常适合搭建实时数据 可暂停/恢复的 UIimport anyio async def main(): spider MySpider(crawldircrawl_data/my_spider) async for item in spider.stream(): print(fGot item: {item}) # Access real-time stats print(fItems so far: {spider.stats.items_scraped}) print(fRequests made: {spider.stats.requests_count}) anyio.run(main)也可以在代码中调用spider.pause()主动关停未启用 checkpoint 系统时它就是一次普通的关闭启用后则触发暂停保存。注意一个限制源码 docstring 明确说明stream 模式下不提供 SIGINT 暂停处理scrapling/spiders/spider.py所以流式场景下要靠pause()而非CtrlC来控制。6. 生命周期钩子在爬取各阶段注入自定义行为Spider 提供多个可覆写的钩子默认实现见 scrapling/spiders/spider.py。on_start爬取开始前调用适合加载种子数据、初始化资源resuming参数告知是否从 checkpoint 恢复async def on_start(self, resuming: bool False): self.logger.info(Spider starting up) # Load seed URLs from a database, initialize counters, etc.on_close爬取结束后调用无论正常完成还是暂停。它在引擎finally块中无条件触发scrapling/spiders/engine.py是关闭数据库连接、刷新缓冲等清理逻辑的可靠位置async def on_close(self): self.logger.info(Spider shutting down) # Close database connections, flush buffers, etc.on_error请求抛出异常时调用适合错误追踪或自定义恢复。引擎在取回失败或回调处理抛错时都会转入此钩子scrapling/spiders/engine.py、scrapling/spiders/engine.pyasync def on_error(self, request: Request, error: Exception): self.logger.error(fFailed: {request.url} - {error}) # Log to error tracker, save failed URL for later, etc.on_scraped_item每个 item 进入结果集之前调用返回 item可修改则保留返回None则丢弃计入items_dropped并打 warning 日志见 scrapling/spiders/engine.pyasync def on_scraped_item(self, item: dict) - dict | None: # Drop items without a title if not item.get(title): return None # Modify items (e.g., add timestamps) item[scraped_at] 2026-01-01 return item提示这个钩子还能把 item 导向你自己的管道同时在 Spider 结果中将其丢弃。start_requests覆写start_requests()可以完全自定义初始请求生成替代start_urls例如先登录再爬取async def start_requests(self): # POST request to log in first yield Request( https://example.com/login, methodPOST, data{user: admin, pass: secret}, callbackself.after_login, ) async def after_login(self, response: Response): # Now crawl the authenticated pages yield response.follow(/dashboard, callbackself.parse)7. 结果与统计CrawlResult 和 CrawlStatsstart()返回的CrawlResult同时包含抓取条目与详细统计定义见 scrapling/spiders/result.pyresult MySpider().start() # Items print(fTotal items: {len(result.items)}) result.items.to_json(output.json, indentTrue) # Did the crawl complete? print(fCompleted: {result.completed}) print(fPaused: {result.paused}) # Statistics stats result.stats print(fRequests: {stats.requests_count}) print(fFailed: {stats.failed_requests_count}) print(fBlocked: {stats.blocked_requests_count}) print(fOffsite filtered: {stats.offsite_requests_count}) print(fRobots.txt disallowed: {stats.robots_disallowed_count}) print(fCache hits: {stats.cache_hits}) print(fCache misses: {stats.cache_misses}) print(fItems scraped: {stats.items_scraped}) print(fItems dropped: {stats.items_dropped}) print(fResponse bytes: {stats.response_bytes}) print(fDuration: {stats.elapsed_seconds:.1f}s) print(fSpeed: {stats.requests_per_second:.1f} req/s)其中result.completed是not self.paused的派生属性items是ItemListlist的子类除to_json()外还支持to_jsonl()、to_csv()、to_xml()四种导出scrapling/spiders/result.py导出时会自动创建父目录CSV/XML 会把嵌套值序列化为 JSON、并自动清洗 XML 非法字符。更细粒度的统计CrawlStatsscrapling/spiders/result.py跟踪的细粒度信息stats result.stats # Status code distribution print(stats.response_status_count) # {status_200: 150, status_404: 3, status_403: 1} # Bytes downloaded per domain print(stats.domains_response_bytes) # {example.com: 1234567, api.example.com: 45678} # Requests per session print(stats.sessions_requests_count) # {http: 120, stealth: 34} # Proxies used during the crawl print(stats.proxies) # [http://proxy1:8080, http://proxy2:8080] # Log level counts print(stats.log_levels_counter) # {debug: 200, info: 50, warning: 3, error: 1, critical: 0} # Timing information print(stats.start_time) # Unix timestamp when crawl started print(stats.end_time) # Unix timestamp when crawl finished print(stats.download_delay) # The download delay used (seconds) # Concurrency settings used print(stats.concurrent_requests) # Global concurrency limit print(stats.concurrent_requests_per_domain) # Per-domain concurrency limit # AutoThrottle print(stats.autothrottle_enabled) # Whether the adaptive delay was on print(stats.autothrottle_delays) # Final delay per domain, {example.com: 0.62} # Custom stats (set by your spider code) print(stats.custom_stats) # {login_attempts: 3, pages_with_errors: 5} # Export everything as a dict print(stats.to_dict())两点值得注意自定义统计stats.custom_stats是一个普通 dict你可以在parse()里直接写self.stats.custom_stats[login_attempts] 1spider.stats在活跃爬取中可用to_dict()会把它一并导出。自动落日志每次crawl()结束时引擎会用log.info打印stats.to_dict()的完整 JSONscrapling/spiders/engine.py因此即使你不处理返回值统计也会进日志。proxies计数则是从请求的会话参数中累积的scrapling/spiders/engine.py。8. 日志配置Spider 内置一个以爬虫名字命名的 loggerscrapling.spiders.{name}支持以下类属性定制scrapling/spiders/spider.py属性默认值说明logging_levellogging.DEBUG最低日志级别logging_format[%(asctime)s]:({spider_name}) %(levelname)s: %(message)s日志消息格式{spider_name}会被替换为爬虫名logging_date_format%Y-%m-%d %H:%M:%S日志中的日期格式log_fileNone日志文件路径与控制台输出并存import logging class MySpider(Spider): name my_spider start_urls [https://example.com] logging_level logging.INFO log_file logs/my_spider.log async def parse(self, response: Response): self.logger.info(fProcessing {response.url}) yield {title: response.css(title::text).get()}从构造器实现scrapling/spiders/spider.py可以看出几个细节日志目录不存在时会自动创建mkdir(parentsTrue, exist_okTrue)控制台与文件共用同一个 formatterpropagate被置为False避免日志重复冒泡到父级 logger。此外还有一个隐藏的LogCounterHandler挂在 logger 上按级别累计消息数最终汇入stats.log_levels_counter——这就是上面统计里log_levels_counter数据的来源。爬取结束时文件 handler 会被显式关闭以释放句柄__run()的finally块。9. 进阶配置速查与源码索引把本文涉及的配置项汇总方便快速定位与自查类别配置项默认值源码位置并发concurrent_requests/concurrent_requests_per_domain/download_delay4/0/0.0spider.py礼貌robots_txt_obey/max_blocked_retriesFalse/3spider.pyAutoThrottleautothrottle_enabled/start_delay/max_delay/target_concurrency/block_backoffFalse/5.0/60.0/None/Truespider.py指纹fp_include_kwargs/fp_include_headers/fp_keep_fragments均为Falsespider.py开发模式development_mode/development_cache_dirFalse/Nonespider.py断点构造参数crawldir/intervalNone/300.0秒spider.py日志logging_level/logging_format/logging_date_format/log_file见上表spider.py补充一个与封禁重试相关的细节当is_blocked()判定被封时引擎最多重试max_blocked_retries默认 3次重试会降优先级、去掉代理并置dont_filter避免立即重打同一封禁点scrapling/spiders/engine.py——这与 AutoThrottle 的延迟翻倍是两条并行的防御线前者管要不要再试后者管多久再试。想进一步验证本文行为可运行仓库中对应的测试套件tests/spiders/test_throttle.pyAutoThrottle 全部收敛、回退、Retry-After用例、tests/spiders/test_checkpoint.py与tests/spiders/test_force_stop_checkpoint.py原子写入、双击强停、tests/spiders/test_cache.py开发模式缓存回放、tests/spiders/test_engine.py与tests/spiders/test_result.py调度与统计输出。配合官方文档站对应页面 docs/spiders/advanced.md 与 docs/spiders/architecture.md 阅读可以获得完整的 Spider 进阶图景。【免费下载链接】Scrapling️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl!项目地址: https://gitcode.com/GitHub_Trending/sc/Scrapling创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进