Scrapy爬虫如何基于历史爬取结果跳过同bucket下的无效请求
问题根因
两个方案失效的核心原因是Scrapy的异步调度机制:
- 方案1在
start_requests阶段就将所有符合初始状态的请求全部提交到了下载队列,后续更新buckets_with_red_balls只能控制还没生成的请求,已经入队的请求依然会正常执行,所以会爬完全部URL - 方案2每次回调都生成全量请求,Scrapy自带的重复过滤器只能过滤已经爬过的URL,不会过滤同bucket下还没爬过的其他ball的请求,同样会产生大量无效请求
正确实现方案
方案A:按bucket独立生成请求(推荐,无多余请求生成)
核心思路是每个bucket的ball逐个生成请求,不提前批量生成所有请求,找到红球后直接停止生成当前bucket的后续请求,还支持多bucket并行处理:
class BucketsBallsSpider(scrapy.Spider): name = 'test_spider' base_url = 'https://bucketswithballs.com' buckets = [] # 填实际的bucket列表 balls = [] # 填实际的ball列表 def start_requests(self): # 所有bucket同时开始爬,每个bucket独立处理自己的ball序列 for bucket_idx in range(len(self.buckets)): yield self._build_request(bucket_idx, ball_idx=0) def _build_request(self, bucket_idx, ball_idx): url = add_or_replace_parameter(self.base_url, 'bucket', self.buckets[bucket_idx]) url = add_or_replace_parameter(url, 'ball', self.balls[ball_idx]) # 通过meta传递当前处理的索引,避免全局状态冲突 return scrapy.Request( url, self.parse, meta={"bucket_idx": bucket_idx, "ball_idx": ball_idx} ) def parse(self, response, **kwargs): bucket_idx = response.meta["bucket_idx"] ball_idx = response.meta["ball_idx"] current_bucket = self.buckets[bucket_idx] is_red_ball = response.xpath('//*[@id="is_red_ball"]').extract() if is_red_ball: yield {'bucket_with_red_ball': current_bucket} # 找到红球,直接终止当前bucket的后续爬取,不用生成新请求 return # 没找到红球,生成当前bucket下一个ball的请求 next_ball_idx = ball_idx + 1 if next_ball_idx < len(self.balls): yield self._build_request(bucket_idx, next_ball_idx)
该方案从根源上避免了无效请求的生成,不会有多余请求进入调度队列,性能最优。
方案B:下载中间件拦截无效请求(适合需要高并发批量生成请求的场景)
如果业务需要提前批量生成所有请求,可以新增下载中间件,在请求发出前拦截已找到红球的bucket的所有请求:
- 先改造Spider,用集合存储已找到红球的bucket(查询效率O(1)远高于列表的O(n)):
class BucketsBallsSpider(scrapy.Spider): name = 'test_spider' base_url = 'https://bucketswithballs.com' buckets = [] balls = [] # 改用集合存储,查询更快 buckets_with_red_balls = set() def start_requests(self): for bucket in self.buckets: for ball in self.balls: url = add_or_replace_parameter(self.base_url, 'bucket', bucket) url = add_or_replace_parameter(url, 'ball', ball) yield scrapy.Request(url, self.parse) def parse(self, response, **kwargs): is_red_ball = response.xpath('//*[@id="is_red_ball"]').extract() if is_red_ball: bucket_id = url_query_parameter(response.url, 'bucket') self.buckets_with_red_balls.add(bucket_id) yield {'bucket_with_red_ball': bucket_id}
- 新增下载中间件拦截无效请求:
from scrapy.exceptions import IgnoreRequest class RedBucketInterceptor: def process_request(self, request, spider): bucket_id = url_query_parameter(request.url, 'bucket') if bucket_id in getattr(spider, 'buckets_with_red_balls', set()): # 直接忽略该请求,不会产生实际网络请求 raise IgnoreRequest()
- 在
settings.py中启用该中间件即可。
内容的提问来源于stack exchange,提问作者Georgian
相关产品推荐
相关产品推荐

