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

MicLon原创2022年2月15日
大约 4 分钟

请求去重

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

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加密计算指纹。

Loading...