> 文档中心 > Scrapy源码学习-请求去重(单机)

Scrapy源码学习-请求去重(单机)


请求去重

这是爬虫岗一道高频出现的面试题:

Q:对于重复的请求,scrapy是如何去重的?去重原理是什么?请求是如何计算唯一性的?

带着这个问题,进入今天的主题。

DUPEFILTER_CLASS

在scrapy项目配置中,DUPEFILTER_CLASS是框架对请求去重规则的设置项。默认的类路径:scrapy.dupefilters.RFPDupeFilter

进入到文件中,观察到类RFPDupeFilter继承自BaseDupeFilter,而BaseDupeFilter似乎什么都没做,只是定义了一些方法。所以,真正的去重核心代码都在RFPDupeFilter类中。逐行分析下其原理。

RFPDupeFilter

class RFPDupeFilter(BaseDupeFilter):    """Request Fingerprint duplicates filter"""    def __init__(self, path=None, debug=False): self.file = None # 用python内置set()作为请求的指纹 # set的特性:无序不重复元素集 self.fingerprints = set() self.logdupes = True self.debug = debug self.logger = logging.getLogger(__name__) # 本地持久化请求指纹 if path:     self.file = open(os.path.join(path, 'requests.seen'), 'a+')     self.file.seek(0)     self.fingerprints.update(x.rstrip() for x in self.file)    @classmethod    def from_settings(cls, settings): # 配置中开启DEBUG,就会持久化文件 debug = settings.getbool('DUPEFILTER_DEBUG') return cls(job_dir(settings), debug)    def request_seen(self, request): # !!!核心,用于检测指纹是否存在。 # 使用request_fingerprint来获取请求的指纹 fp = self.request_fingerprint(request) # 指纹在集合中,返回True if fp in self.fingerprints:     return True # 不在集合中,追加到集合里 self.fingerprints.add(fp) if self.file:     self.file.write(fp + '\n')    def request_fingerprint(self, request): # 调用scrapy的request_fingerprint来进行指纹计算 return request_fingerprint(request)    def close(self, reason): # 资源销毁 if self.file:     self.file.close()    def log(self, request, spider): # 日志的输出和记录 if self.debug:     msg = "Filtered duplicate request: %(request)s (referer: %(referer)s)"     args = {'request': request, 'referer': referer_str(request)}     self.logger.debug(msg, args, extra={'spider': spider}) elif self.logdupes:     msg = ("Filtered duplicate request: %(request)s"     " - no more duplicates will be shown"     " (see DUPEFILTER_DEBUG to show all duplicates)")     self.logger.debug(msg, {'request': request}, extra={'spider': spider})     self.logdupes = False spider.crawler.stats.inc_value('dupefilter/filtered', spider=spider)

上述代码非常简单,简单到任何人都可以自己轻松写一个。其中request_seen方法用于检测请求是否重复,返回True则重复,否则通过。其中核心的是调用了request_fingerprint来计算指纹。进去看看。

request_fingerprint

The request fingerprint is a hash that uniquely identifies the resource the request points to
请求指纹是唯一标识请求指向的资源的哈希值

def request_fingerprint(request, include_headers=None, keep_fragments=False):    # 是否计算headers    if include_headers: include_headers = tuple(to_bytes(h.lower()) for h in sorted(include_headers))    cache = _fingerprint_cache.setdefault(request, {})    cache_key = (include_headers, keep_fragments)    if cache_key not in cache: # 开始计算,加密算法sha1 fp = hashlib.sha1() # 将请求方式和请求url,请求的body加入计算, # 此处的url如果指向同一个资源,同样认为一样,比如:      # http://www.example.com/query?id=111&cat=222     # http://www.example.com/query?cat=222&id=111 # 这两个url指向同一目标,我们也认为是重复的request.url fp.update(to_bytes(request.method)) fp.update(to_bytes(canonicalize_url(request.url, keep_fragments=keep_fragments))) fp.update(request.body or b'') # headers加入计算 if include_headers:     for hdr in include_headers:  if hdr in request.headers:      fp.update(hdr)      for v in request.headers.getlist(hdr):   fp.update(v) cache[cache_key] = fp.hexdigest()    return cache[cache_key]

调度器的执行流程

在scrapy的调度器代码中Scheduler,通过类方法from_crawler读取配置项中DUPEFILTER_CLASS的类路径,使用load_object加载并通过create_instance实例化对象。赋给属性self.df

class Scheduler: def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,   logunser=False, stats=None, pqclass=None, crawler=None): self.df = dupefilter ……    @classmethod    def from_crawler(cls, crawler): settings = crawler.settings dupefilter_cls = load_object(settings['DUPEFILTER_CLASS']) dupefilter = create_instance(dupefilter_cls, settings, crawler) …… return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,     stats=crawler.stats, pqclass=pqclass, dqclass=dqclass,     mqclass=mqclass, crawler=crawler)    def open(self, spider): …… return self.df.open()    def close(self, reason): …… return self.df.close(reason)    def enqueue_request(self, request): if not request.dont_filter and self.df.request_seen(request):     self.df.log(request, self.spider)     return False …… return True

调度器被打开open、关闭close、请求入列enqueue_request的时候
分别触发过滤器的打开open、关闭close、计算指纹request_seen。

当构造请求时,参数dont_filter为False的时候,才会进入去重计算。

新手经常犯的错。dont_filter=True认为是去重。实际上国外人思维和我们直接表达不同。可能我们做参数就filter=True是过滤,filter=False就不过滤。加了dont,dont_filter=True 翻译过来就是:不过滤?是的。

总结

现在再来回答面试官的问题:

Q:对于重复的请求,scrapy是如何去重的?去重原理是什么?请求是如何计算唯一性的?

A:scrapy是通过配置文件中DUPEFILTER_CLASS属性来选择去重的方法。默认情况下,是调用scrapy.dupefilters.RFPDupeFilter。
scrapy请求是通过Python内置set不重复集合的特性来做本地去重的。
其加密算法是sha1。默认情况针对请求的方式、url、body来做唯一性计算。

核心两点:set 指纹去重,sha1加密计算指纹。