You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的所有请求:

  1. 先改造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}
  1. 新增下载中间件拦截无效请求:
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()
  1. 在settings.py中启用该中间件即可。

内容的提问来源于stack exchange,提问作者Georgian

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 06:54:07