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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,723评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,003评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,512评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,825评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,874评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,841评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,812评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,582评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,033评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,309评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,450评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,158评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,789评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,409评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,609评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,440评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,357评论 2 352

推荐阅读更多精彩内容