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.py
的next_request()
方法获取了该爬取指令并返回了一个新的Request
(Request A
),随后由下载器下载。
然而由于该URL返回了一个301重定向,Scrapy中的默认下载器中间件(download middleware)RedirectMiddleware
的process_response
方法重新处理了对应的Response
,并返回了一个新的Request
(Request B
)。
由于Scrapy Cluster中distributed_scheduler.py
的next_request()
方法在创建Request A
过程中并没有设置dont_filter
参数。因此即使RedirectMiddleware
中间件中会保留原Request
(Request 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.py
的next_request()
方法,为创建的Request默认设置dont_filter
参数为True
。这种方法的好处是从根源上修复了问题。但是缺点是,如果Scrapy Cluster发布新版本,需要和新版本做一个代码合并,可能会产生冲突。
自定义一个下载器中间件(download middleware),在URL被下载器下载之前,先判断以下URL会不会被重定向。如果会发生重定向,在中间件中创建一个新的Request,并将dont_filter
参数为True
。这个的好处就是避免和系统本身冲突。弊端就是每一个下载的URL都会被做个HTTP的Head请求一下,非常消耗带宽资源。

扫码联系船长