Scrapy Cluster中重定向被错误去重

Scrapy Cluster在默认情况下,会自动去重已经爬取过的URL。因此,如果一个URL被爬取过以后,会在一段时间内,系统中又碰到这个URL,就会把它过滤掉。大家可以在localsettings.py文件中设置DUPEFILTER_TIMEOUT,单位为秒。默认为600秒。也就是说,一个URL被爬取以后,默认情况下,600秒内都会被过滤掉,除非新的Request对象中设置了dont_filter参数为True。那么,在Scrapy Cluster系统中是如何实现URL的去重功能的呢?另外,在Scrapy Cluster中重定向的URL又是如何被错误去重的呢?

Scrapy Cluster中的去重机制

当我们启动一个Scrapy Cluster中的爬虫以后,Scrapy Cluster中的distributed_scheduler.py里的next_request()方法就会不断被heartbeat调用。以下是Scrapy中该段逻辑的实现原理:

    #/scrapy/core/engine.py
    def open_spider(self, spider, start_requests=(), close_if_idle=True):
        assert self.has_capacity(), "No free spider slot when opening %r" % \
            spider.name
        logger.info("Spider opened", extra={'spider': spider})
        nextcall = CallLaterOnce(self._next_request, spider)
        scheduler = self.scheduler_cls.from_crawler(self.crawler)
        start_requests = yield self.scraper.spidermw.process_start_requests(start_requests, spider)
        slot = Slot(start_requests, close_if_idle, nextcall, scheduler)
        self.slot = slot
        self.spider = spider
        yield scheduler.open(spider)
        yield self.scraper.open_spider(spider)
        self.crawler.stats.open_spider(spider)
        yield self.signals.send_catch_log_deferred(signals.spider_opened, spider=spider)
        slot.nextcall.schedule()
        slot.heartbeat.start(5)

从代码中看,应该是每5秒钟就会调用一次。在Scrapy Cluster的next_request()方法中,会从Redis中获取一个爬取信息item,并且创建一个Request对象,把爬取信息item中的内容赋值给该对象。由于代码量太大,就补贴在这里了。但是代码中有一个问题,就是没有复制dont_filter信息。实际上,在Scrapy Cluster的next_request()方法中新建了一个Request对象后就会送到Downloader下载器下载。因此只要是能够进入next_request()方法URL都不会被过滤掉。这就是为什么我们直接推送到kafka爬取的URL不会被过滤(因为URL被推送到Kafka以后,被kafka_monitor直接推送到了Redis,然后直接被next_request()方法取出并爬取)。

实际上,只有通过Scrapy Cluster创建的Request爬取对象才会触发去重机制。比如说,从spider里面返回的Request爬取对象,或者从下载中间件中返回的Request爬取对象。只有这些爬取对象会触发Scrapy Cluster中的distributed_scheduler.py里的enqueue_request()方法。该方法一开始就会判断去重。以下是该方法中的一个代码片段:

        if not request.dont_filter and self.dupefilter.request_seen(request):
            self.logger.debug("Request not added back to redis")
            return

所以Scrapy Cluster创建的Request爬取对象如果设置了dont_filter参数就可以避免去重。

Scrapy Cluster中重定向被错误去重

这个问题是在调试另外一个问题的过程中被发现的。由于发送给Kafka的爬取指令被kafka_monitor消费并推送给Redis,Scrapy Cluster中distributed_scheduler.pynext_request()方法获取了该爬取指令并返回了一个新的RequestRequest A),随后由下载器下载。

working flow in scrapy cluster

然而由于该URL返回了一个301重定向,Scrapy中的默认下载器中间件(download middleware)RedirectMiddlewareprocess_response方法重新处理了对应的Response,并返回了一个新的RequestRequest B)。

由于Scrapy Cluster中distributed_scheduler.pynext_request()方法在创建Request A过程中并没有设置dont_filter参数。因此即使RedirectMiddleware中间件中会保留原RequestRequest A)中的dont_filter参数,新的code>Request(Request B)也不会有dont_filter

所以,当Scrapy Cluster中distributed_scheduler.py里的enqueue_request()方法获取到新的code>Request(Request B)时,就发现这个Request已经爬取过了(而且没有过期),因此就会错误地将其做去重处理。

此处附上Scrapy中的RedirectMiddleware下载器中间件代码供参考:

#scrapy/downloadermiddlewares/redirect.py
class RedirectMiddleware(BaseRedirectMiddleware):
    """
    Handle redirection of requests based on response status
    and meta-refresh html tag.
    """
    def process_response(self, request, response, spider):
        if (request.meta.get('dont_redirect', False) or
                response.status in getattr(spider, 'handle_httpstatus_list', []) or
                response.status in request.meta.get('handle_httpstatus_list', []) or
                request.meta.get('handle_httpstatus_all', False)):
            return response

        allowed_status = (301, 302, 303, 307)
        if 'Location' not in response.headers or response.status not in allowed_status:
            return response

        location = safe_url_string(response.headers['location'])

        redirected_url = urljoin(request.url, location)

        if response.status in (301, 307) or request.method == 'HEAD':
            redirected = request.replace(url=redirected_url)
            return self._redirect(redirected, request, spider, response.status)

        redirected = self._redirect_request_using_get(request, redirected_url)
        return self._redirect(redirected, request, spider, response.status)

如何解决Scrapy Cluster中重定向被错误去重的问题

处理该问题,我们可以有以下两个思路:

  • 思路一:
  • 修改Scrapy Cluster中distributed_scheduler.pynext_request()方法,为创建的Request默认设置dont_filter参数为True。这种方法的好处是从根源上修复了问题。但是缺点是,如果Scrapy Cluster发布新版本,需要和新版本做一个代码合并,可能会产生冲突。

  • 思路二:
  • 自定义一个下载器中间件(download middleware),在URL被下载器下载之前,先判断以下URL会不会被重定向。如果会发生重定向,在中间件中创建一个新的Request,并将dont_filter参数为True。这个的好处就是避免和系统本身冲突。弊端就是每一个下载的URL都会被做个HTTP的Head请求一下,非常消耗带宽资源。

Captain QR Code

扫码联系船长

发表回复

您的电子邮箱地址不会被公开。