Scrapy源码分析之Dupfilters模块(第二期)

2023-11-12

大家好,我是TheWeiJun,欢迎来到我的公众号。今天给大家带来Scrapy源码分析之Dupfilters模块源码详解,希望大家能够喜欢。如果你觉得我的文章内容有价值,记得点赞+关注!

特别声明:本公众号文章只作为学术研究,不用于其它用途。

 目录

①    问题思考

②    案例分享

③    源码分析

④    源码重写

⑤    总结分享


一、问题思考

Question 

 ①我们在使用Scrapy框架的时候,一直在好奇Scrapy是如何对每一个请求进行指纹过滤的?

Question

 ②基于Scrapy原来的去重机制,如果要实现一个增量式爬虫。我们该如何实现呢?此刻默认的去重机制肯定无法满足我们的需求!

Question

 ③如果我不用Scrapy-Redis分布式做爬虫抓取,采用Scrapy,每次抓取完成后,指纹全部丢失,我们该如何将指纹和Scrapy-Redis一样进行持久化存储呢?当下一次再启动的时候,它依然存在每一个请求的指纹?

Question

④scrapy.Request请求参数设置为dont_filter=True,即可忽略去重,这个机制是如何触发的?

那么带着这些问题,我们对Scrapy的源码进行分析吧,我相信这篇文章会让大家受益匪浅!


二、案例分析

1.  源码分析前,我们还是和以往一样,构建一个小的demo。代码结构如何:


spiders目录下代码:

# -*- coding: utf-8 -*-import scrapyclass BaiduSpider(scrapy.Spider):    name = "baidu"    allowed_domains = ["baidu.com"]    start_urls = ['http://baidu.com/', 'http://baidu.com/']    def start_requests(self):        for index, url in enumerate(self.start_urls):            yield scrapy.Request(url=url, callback=self.parse, meta={"index": index})    def parse(self, response):        print(response.meta["index"], "-------------")


说明:为了更好的了解源代码,我们需要做一个简单的测试,如上图代码所示。启动爬虫后,输出如下:

结论:index为1并没有被打印出来,是因为触发了scrapy默认的去重机制导致,这个时候我们会发现scrapy的stats中间件将dupefilter/filtered的值设置为1。


2.  接下来换个思路进行B轮测试。测试方案为将:dont_filter=True,代码如下:

# -*- coding: utf-8 -*-import scrapyclass BaiduSpider(scrapy.Spider):    name = "baidu"    allowed_domains = ["baidu.com"]    start_urls = ['http://baidu.com/', 'http://baidu.com/']    def start_requests(self):        for index, url in enumerate(self.start_urls):            yield scrapy.Request(url=url, callback=self.parse, meta={"index": index}, dont_filter=True)    def parse(self, response):        print(response.meta["index"], "-------------")


输出如下:

结论:开启dont_filter=True,则不会对请求url进行去重,并且不会触发去重统计的信息。

探索:通过这个小的实验,带着好奇心,接下来我们需要对scrapy的源码进行分析了。


三、源码分析

1.  查看官网文档,搜索指定的模块dupefilter,搜索结果如下:

通过阅读文档,我们可以确定scrapy默认使用的去重机制:

1. scrapy.dupefilters.RFPDupefilter在settings.py模块中默认是开启状态!默认RFPDupeFilter基于使用该scrapy.utils.request.request_fingerprint函数的请求指纹进行过滤。

2. 为了更改检查重复项的方式,您可以子类化RFPDupeFilter并覆盖其request_fingerprint方法。此方法应接受scrapyRequest对象并返回其指纹。


2.  源码阅读及分析

先定位到scrapy默认配置去重机制的参数,如下:


搜索指定关键字,附上源码如下:

import loggingimport osfrom typing import Optional, Set, Type, TypeVarfrom twisted.internet.defer import Deferredfrom scrapy.http.request import Requestfrom scrapy.settings import BaseSettingsfrom scrapy.spiders import Spiderfrom scrapy.utils.job import job_dirfrom scrapy.utils.request import referer_str, request_fingerprintBaseDupeFilterTV = TypeVar("BaseDupeFilterTV", bound="BaseDupeFilter")class BaseDupeFilter:    @classmethod    def from_settings(cls: Type[BaseDupeFilterTV], settings: BaseSettings) -> BaseDupeFilterTV:        return cls()    def request_seen(self, request: Request) -> bool:        return False    def open(self) -> Optional[Deferred]:        pass    def close(self, reason: str) -> Optional[Deferred]:        pass    def log(self, request: Request, spider: Spider) -> None:        """Log that a request has been filtered"""        passRFPDupeFilterTV = TypeVar("RFPDupeFilterTV", bound="RFPDupeFilter")class RFPDupeFilter(BaseDupeFilter):    """Request Fingerprint duplicates filter"""    def __init__(self, path: Optional[str] = None, debug: bool = False) -> None:        self.file = None        self.fingerprints: Set[str] = 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: Type[RFPDupeFilterTV], settings: BaseSettings) -> RFPDupeFilterTV:        debug = settings.getbool('DUPEFILTER_DEBUG')        return cls(job_dir(settings), debug)    def request_seen(self, request: Request) -> bool:        fp = self.request_fingerprint(request)        if fp in self.fingerprints:            return True        self.fingerprints.add(fp)        if self.file:            self.file.write(fp + '\n')        return False    def request_fingerprint(self, request: Request) -> str:        return request_fingerprint(request)    def close(self, reason: str) -> None:        if self.file:            self.file.close()    def log(self, request: Request, spider: Spider) -> None:        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)

分析:和官方文档的说明一致,RFPDupeFilter类继承了BaseDupeFilter,实现了去重机制。接下来对源码进行内容拆分讲解。


  • __init__函数:

def __init__(self, path: Optional[str] = None, debug: bool = False) -> None:    self.file = None        self.fingerprints: Set[str] = set() # 用python set做去重    self.logdupes = True     self.debug = debug # 是否开启DUPEFILTER_DEBUG    self.logger = logging.getLogger(__name__)        if path: # 将本地化保存的requests.seen文件中的fp指纹加载到set中。        self.file = open(os.path.join(path, 'requests.seen'), 'a+')        self.file.seek(0)        self.fingerprints.update(x.rstrip() for x in self.file)

  • from_settings函数:

@classmethoddef from_settings(cls: Type[RFPDupeFilterTV], settings: BaseSettings) -> RFPDupeFilterTV:    debug = settings.getbool('DUPEFILTER_DEBUG')    return cls(job_dir(settings), debug)"""默认情况下,RFPDupeFilter仅记录第一个重复请求。设置DUPEFILTER_DEBUG为True将使其记录所有重复的请求。ob_dir(settings)为读取本地path路径,JOBDIR = "路径地址",代码如下:"""def job_dir(settings: BaseSettings) -> Optional[str]:    path = settings['JOBDIR']    if path and not os.path.exists(path):        os.makedirs(path)    return path

  • 其他方法:

def request_seen(self, request: Request) -> bool:    fp = self.request_fingerprint(request) # 计算指纹    if fp in self.fingerprints: # 判断指纹是否在set中。        return True    self.fingerprints.add(fp) # 不存在就添加指纹    if self.file: # 持久化指纹        self.file.write(fp + '\n')    return Falsedef request_fingerprint(self, request: Request) -> str:    return request_fingerprint(request) # 调用封装好的指纹方法,此刻两个方法名一致,注意:不是调用的同一个方法    # 下面会单独分析此方法def close(self, reason: str) -> None:    if self.file: # 如果有文件,关闭文件        self.file.close()def log(self, request: Request, spider: Spider) -> None:    # 输出日志,默认情况下,RFPDupeFilter仅记录第一个重复请求。设置DUPEFILTER_DEBUG为True将使其记录所有重复的请求。    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_fingerprint(request)函数:

"""由于代码过多,我只粘贴部分核心源码"""def request_fingerprint(    request: Request,    include_headers: Optional[Iterable[Union[bytes, str]]] = None,    keep_fragments: bool = False,) -> str:    """    Return the request fingerprint.    """    headers: Optional[Tuple[bytes, ...]] = None    if include_headers:        headers = tuple(to_bytes(h.lower()) for h in sorted(include_headers))    cache = _fingerprint_cache.setdefault(request, {})    cache_key = (headers, keep_fragments)    if cache_key not in cache:        fp = hashlib.sha1()        fp.update(to_bytes(request.method))        fp.update(to_bytes(canonicalize_url(request.url, keep_fragments=keep_fragments)))        fp.update(request.body or b'')        if headers:            for hdr in 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]  # 读取request:method、url、body or ""、headers进行sha1加密 # 加密后的内容放到cache字典中,然后最后返回fp。 # request_seen函数最后将fp添加到set()中。 # 默认不去重headers。


在scrapy中,当一个请求被spider发起时,它会先经过去重器校验,校验的过程大致如下:

1.对发起的请求的相关信息,通过特定的算法(sha1),生成一个请求指纹2.判断这个指纹是否存在于指纹集合中.3.如果在指纹集合,则表示此请求曾经执行过,舍弃它.4.如果不在,则表示此为第一次执行,将指纹加入到指纹集合中,并将请求加入到请求队列中,等待调度.


scrapy默认的调度器是scrapy.core.scheduler.Scheduler,其中主要的去重代码都在enqueue_request这个方法里,代码如下:

def enqueue_request(self, request):    if not request.dont_filter and self.df.request_seen(request):        self.df.log(request, self.spider)        return False    dqok = self._dqpush(request)    if dqok:        self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)    else:        self._mqpush(request)        self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)    self.stats.inc_value('scheduler/enqueued', spider=self.spider)    return True"""scrapy的Request对象如果设置dont_filter=True,则不会去重。我们知道request传入dont_filter=True时会不去重,这个逻辑就是在这里判断的。self.df.request_seen(request)在上面中我们已经提到。"""


四、源码重写

# settings.py自定自定义模块DUPEFILTER_CLASS = 'scrapy_demo.dupfilters.RFPDupeFilter'# 假设对首页域名不去重,可以这样设置,直接重写request_seen即可。def request_seen(self, request: Request) -> bool:    fp = self.request_fingerprint(request)    path = furl(request.url).pathstr    if path and len(path) == 1:        return False    if fp in self.fingerprints:        return True    self.fingerprints.add(fp)    if self.file:        self.file.write(fp + '\n')    return False


五、总结分享

总结:如果为了自定义某些功能,建议大家从scrapy运行流程图去入手,即可定位到需要重写的模块范围,然后查看官网文档进行阅读即可。


作者简介

我是TheWeiJun,有着执着的追求,信奉终身成长,不定义自己,热爱技术但不拘泥于技术,爱好分享,喜欢读书和乐于结交朋友,欢迎扫我微信与我交朋友

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Scrapy源码分析之Dupfilters模块(第二期) 的相关文章

随机推荐

  • linux sed -i replace text/sed 跟expression替换文本

    1 生成测试文本 peng peng cat gt aa txt aa bb cc dd ee C 2 原本的方案 用vi替换文本 aa替换成abc s aa abc 3 用sed命令替换文本 replace aa with abc at
  • 交换两个变量的值(包括字符串的交换)

    例 交换两个变量的值 输入两个整型变量a和b 设计一个交换函数将其交换后再输出 注意 不能直接输出b和a 错误代码 include
  • 适配任何数据结构的异步Excel生成(企业级开发)

    文章目录 前言 一 Java操作Excel的基础知识 二 测试准备 三 实现源码 四 功能测试 总结 源码 前言 背景 由于公司的excel生成过于缓慢 有时生成一个excel文件需要等待几十秒甚至几分钟 在等待的时候用户不能跳转其他页面
  • YOLOv4:目标检测的最佳速度和精度

    YOLOv4 目标检测的最佳速度和精度 摘要 随着深度学习的发展 目前已经出现了很多算法 或者训练技巧 tricks 来提升神经网络的准确率 在实际测试中评价一个算法的好坏优劣主要看两点 一是能否在大规模的数据集中起作用 work 二是是否
  • 机器人教育培养孩子的逻辑思维

    孩子拥有好的思维逻辑 是每个父母梦寐以求的 怎样锻炼孩子的思维逻辑能力 也是每个父母头疼的事情 格物斯坦小坦克想说 其实培养孩子的思维能力是有迹可循的 了解顺序概念 事物按照大小 硬软 胖瘦等会有一个顺序 如小朋友们有时候会按高矮站队 这些
  • 华为OD机试-机器人走迷宫

    题目描述 机器人走一个迷宫 给出迷宫的x和y x y的迷宫 并且迷宫中有障碍物 输入k表示障碍物有k个 并且会将障碍物的坐标挨个输入 机器人从0 0的位置走到x y的位置并且只能向x y增加的方向走 不能回退 如代码类注释展示的样子 表示可
  • 因果4-因果模型

    上一章我们从统计学角度学习了贝叶斯网络中点与点的关系 并没有真正涉及因果的重要内容 因为基于的都是条件概率 没有牵扯到干预 而干预是因果很重要的操作 这一章我们从干预开始 进一步学习如何识别因果图中的因果量 首先让我们回顾并正式定义第一章中
  • 【2021】最新的ECMAScript标准定义了8种数据类型

    最新的ECMAScript标准定义了8种数据类型 一 七种基本数据类型 Boolean Null Undefined Number String Symbol ES6新增 一种实例是唯一且不可改变的数据类型 Bigint 任意精度的整数 可
  • CiteSpace可视化出图:制作聚类图、时间线图、时区图、Landscape视图、地理可视化图等多种可视化绘制。

    CiteSpace 是一款优秀的文献计量学软件 能够将文献之间的关系以科学知识图谱的方式可视化的展现在操作者面前 科研人员 多多少少都会用到一些 但是 CiteSpace 是基于 Java 开发 旧版本需要安装 Java 运行环境才能使用
  • ADC误差

    本文转载自 http blog csdn net tianhen791 article details 38736217 动态测试关注的是器件的传输和性能特征 即采样和重现时序变化信号的能力 相比之下 线性测试关注的则是器件内部电路的误差
  • IntelliJ IDEA和Eclipse快捷键对比总结

    IntelliJ IDEA和Eclipse快捷键对比总结 Eclipse Oxygen Release 4 7 0 IntelliJ IDEA 2017 3 4 Ultimate Edition 提醒一点 需要注意和其他软件的热键冲突 比如
  • opencore 启动总是在win_刷黑苹果之后无法进入BIOS设置opencore

    子方有话 子方的配置是是华硕B450MK AMD R5 2600 GT710 在完成子方黑苹果系统的安装后 子方把引导转到了硬盘 没几次后 子方发现无法进入BIOS设置 不过可以通过F8键进入启动设置 选择启动windows 但不管通过什么
  • SpringBoot集成redis(3)

    SpringBoot集成redis 3 Redisson方式实现分布式锁 文章目录 SpringBoot集成redis 3 Redisson方式实现分布式锁 TOC 前言 一 Redisson是什么 二 集成步骤 1 依赖引入 2 文件配置
  • mysql的流程控制if与case

    mysql中常用的流程控制有两种 1 if语句 基本语法 IF expr v1 v2 如果表达式 expr 成立 返回结果 v1 否则 返回结果 v2 用法跟三目运算符类似 适用只有两种结果 案例 SELECT IF 1 gt 0 正确 错
  • 疯壳AI语音及人脸识别教程2-4串口

    目录 1 1寄存器 1 1 2实验现象 17 视频地址 https fengke club GeekMart su f9cTSxNsp jsp 串口 官方QQ群 457586268 串行接口分为异步串行接口和同步串行接口两种 异步串行接口统
  • 这100套毕设项目,是给计算机系学弟学妹在毕业季的一波镇定剂!练手收藏

    又到了一年一度的毕业季了 有憧憬社会的 也有怀念校园生活的 不管如何我们都要努力向前 迎接变化 这次小编整理的100套Java毕设项目 给正在发愁的你和将来要项目练手的你一波助力 具体内容目录给大家看看 希望可以帮到你 需要更多学习方式和资
  • [论文阅读] (28)李沐老师视频学习——1.研究的艺术·跟读者建立联系

    娜璋带你读论文 系列主要是督促自己阅读优秀论文及听取学术讲座 并分享给大家 希望您喜欢 由于作者的英文水平和学术能力不高 需要不断提升 所以还请大家批评指正 非常欢迎大家给我留言评论 学术路上期待与您前行 加油 前一篇文章介绍AAAI20腾
  • depends工具查看exe和dll依赖关系

    应用场景 在使用QT等图形用户界面应用程序开发框架开发Windows程序时 通常需要将写到的程序发布到其它计算机中进使用 在使用Qt发布程序时 虽然使用windeployqt工具能够自动打包好大部分依赖库 但还是难免会漏掉一些第三方库导致发
  • C#学习05-类简介与派生继承

    基本概念 类是一种数据结构 它可以包含数据成员 函数成员以及嵌套类型 C 中类的声明 C 中类的声明即定义 不同于c 中声明与定义是分开的 C 类构造函数 类的 构造函数 是类的一个特殊的成员函数 当创建类的新对象时执行 构造函数的名称与类
  • Scrapy源码分析之Dupfilters模块(第二期)

    大家好 我是TheWeiJun 欢迎来到我的公众号 今天给大家带来Scrapy源码分析之Dupfilters模块源码详解 希望大家能够喜欢 如果你觉得我的文章内容有价值 记得点赞 关注 特别声明 本公众号文章只作为学术研究 不用于其它用途