Scrapy源码阅读分析_4_请求处理流程

 

From:https://blog.csdn.net/weixin_37947156/article/details/74533108

 

 

运行入口

 

还是回到最初的入口,在Scrapy源码分析(二)运行入口这篇文章中已经讲解到,在执行scrapy命令时,调用流程如下:

  • 调用cmdline.py的execute方法
  • 调用命令实例解析命令行
  • 构建CrawlerProcess实例,调用crawl和start方法

而 crawl 方法最终是调用了 Cralwer 实例的 crawl,这个方法最终把控制权交由 Engine,而 start 方法 注册好协程池,开始异步调度。

我们来看 Cralwer 的 crawl 方法:(scrapy/crawler.py

在把控制权交给引擎调度之前,先创建出爬虫实例,然后创建引擎实例(此过程见Scrapy源码分析(三)核心组件初始化),然后调用了spiderstart_requests方法,这个方法就是我们平时写的最多爬虫类的父类,它在spiders/__init__.py中:

 

 

构建请求

 

在这里我们能看到,平时我们必须要定义的start_urls,原来是在这里拿来构建Request的,来看Request的是如何构建的:

(scrapy/http/request/__init__.py)

"""
This module implements the Request class which is used to represent HTTP
requests in Scrapy.See documentation in docs/topics/request-response.rst
"""
import six
from w3lib.url import safe_url_stringfrom scrapy.http.headers import Headers
from scrapy.utils.python import to_bytes
from scrapy.utils.trackref import object_ref
from scrapy.utils.url import escape_ajax
from scrapy.http.common import obsolete_setterclass Request(object_ref):def __init__(self, url, callback=None, method='GET', headers=None, body=None,cookies=None, meta=None, encoding='utf-8', priority=0,dont_filter=False, errback=None, flags=None):# 编码self._encoding = encoding  # this one has to be set first# 请求方法self.method = str(method).upper()# 设置 URLself._set_url(url)#  设置 bodyself._set_body(body)assert isinstance(priority, int), "Request priority not an integer: %r" % priority# 优先级self.priority = priorityif callback is not None and not callable(callback):raise TypeError('callback must be a callable, got %s' % type(callback).__name__)if errback is not None and not callable(errback):raise TypeError('errback must be a callable, got %s' % type(errback).__name__)assert callback or not errback, "Cannot use errback without a callback"# 回调函数self.callback = callback# 异常回调函数self.errback = errback# cookiesself.cookies = cookies or {}# 构建Headerself.headers = Headers(headers or {}, encoding=encoding)# 是否需要过滤self.dont_filter = dont_filter# 附加信息self._meta = dict(meta) if meta else Noneself.flags = [] if flags is None else list(flags)@propertydef meta(self):if self._meta is None:self._meta = {}return self._metadef _get_url(self):return self._urldef _set_url(self, url):if not isinstance(url, six.string_types):raise TypeError('Request url must be str or unicode, got %s:' % type(url).__name__)s = safe_url_string(url, self.encoding)self._url = escape_ajax(s)if ':' not in self._url:raise ValueError('Missing scheme in request url: %s' % self._url)url = property(_get_url, obsolete_setter(_set_url, 'url'))def _get_body(self):return self._bodydef _set_body(self, body):if body is None:self._body = b''else:self._body = to_bytes(body, self.encoding)body = property(_get_body, obsolete_setter(_set_body, 'body'))@propertydef encoding(self):return self._encodingdef __str__(self):return "<%s %s>" % (self.method, self.url)__repr__ = __str__def copy(self):"""Return a copy of this Request"""return self.replace()def replace(self, *args, **kwargs):"""Create a new Request with the same attributes except for thosegiven new values."""for x in ['url', 'method', 'headers', 'body', 'cookies', 'meta', 'flags','encoding', 'priority', 'dont_filter', 'callback', 'errback']:kwargs.setdefault(x, getattr(self, x))cls = kwargs.pop('cls', self.__class__)return cls(*args, **kwargs)

Request对象比较简单,就是简单封装了请求参数、方式、回调以及可附加的属性信息。

当然,你也可以在子类重写start_requests以及make_requests_from_url这2个方法,来构建种子请求。

 

 

引擎调度

 

回到crawl方法,构建好种子请求对象后,调用了engineopen_spider方法:

初始化的过程之前的文章已讲到,这里不再多说。主要说一下处理流程,这里第一步是构建了CallLaterOnce,把_next_request注册进去,看此类的实现:

class CallLaterOnce(object):"""Schedule a function to be called in the next reactor loop, but only ifit hasn't been already scheduled since the last time it ran."""# 在twisted的reactor中循环调度一个方法def __init__(self, func, *a, **kw):self._func = funcself._a = aself._kw = kwself._call = Nonedef schedule(self, delay=0):# 上次发起调度,才可再次继续调度if self._call is None:self._call = reactor.callLater(delay, self)def cancel(self):if self._call:self._call.cancel()def __call__(self):# 上面注册的是self,所以会执行__call__self._call = Nonereturn self._func(*self._a, **self._kw)

这里封装了循环执行的方法类,并且注册的方法会在 twisted 的 reactor 中异步执行,以后执行只需调用 schedule 方法,就会注册 self 到 reactor 的 callLater 中,然后它会执行 __call__ 方法,进而执行我们注册的方法。而这里我们注册的方法是引擎的_next_request,也就是说,此方法会循环调度,直到程序退出。

然后调用了爬虫中间件的 process_start_requests 方法,也就是说,你可以定义多个自己的爬虫中间件,每个类都重写此方法,爬虫在调度之前会分别调用你定义好的爬虫中间件,来分别处理初始化请求,你可以进行过滤、加工、筛选以及你想做的任何逻辑。这样做的好处就是,把想做的逻辑拆分成做个中间件,功能独立而且维护起来更加清晰。

 

 

调度器

 

接着调用了Scheduleropen:(scrapy/core/scheduler.py)

open方法中,实例化出优先级队列以及根据dqdir决定是否使用磁盘队列,然后调用了请求指纹过滤器open,在父类BaseDupeFilter中定义:

请求过滤器提供了请求过滤的具体实现方式,Scrapy默认提供了RFPDupeFilter过滤器实现过滤重复请求的逻辑,后面讲具体是如何过滤重复请求的。

 

 

Scraper

 

再来看Scraperopen_spider

这里的工作主要是Scraper调用所有Pipelineopen_spider方法,也就是说,如果我们定义了多个Pipeline输出类,可重写open_spider完成每个Pipeline处理输出开始的初始化工作。

 

 

循环调度

 

调用了一些列的组件的open方法后,最后调用了nextcall.schedule()开始调度,

也就是循环执行在上面注册的 _next_request方法:

_next_request 方法首先调用 _needs_backout 方法检查是否需要等待,等待的条件有:

  • 引擎是否主动关闭
  • Slot是否关闭
  • 下载器网络下载超过预设参数
  • Scraper处理输出超过预设参数

如果不需要等待,则调用 _next_request_from_scheduler,此方法从名字上就能看出,主要是从 Schduler中 获取 Request。

这里要注意,在第一次调用此方法时,Scheduler 中是没有放入任何 Request 的,这里会直接 break 出来,执行下面的逻辑,而下面就会调用 crawl 方法,实际是把请求放到 Scheduler 的请求队列,放入队列的过程会经过 请求过滤器 校验是否重复。

下次再调用 _next_request_from_scheduler 时,就能从 Scheduler 中获取到下载请求,然后执行下载动作。

先来看第一次调度,执行 crawl:

调用引擎的crawl实际就是把请求放入Scheduler的队列中,下面看请求是如何入队列的。

 

 

请求入队

 

Scheduler 请求入队方法:(scrapy/core/schedulter.py)

在之前将核心组件实例化时有说到,调度器主要定义了2种队列:基于磁盘队列、基于内存队列。

如果在实例化Scheduler时候传入jobdir,则使用磁盘队列,否则使用内存队列,默认使用内存队列。

 

 

指纹过滤

 

在入队之前,首先通过请求指纹过滤器检查请求是否重复,也就是调用了过滤器的request_seen

(scrapy/duperfilters.py)

utils.requestrequest_fingerprint

这个过滤器先是通过 Request 对象 生成一个请求指纹,在这里使用 sha1 算法,并记录到指纹集合,每次请求入队前先到这里验证一下指纹集合,如果已存在,则认为请求重复,则不会重复入队列。

不过如果我想不校验重复,也想重复爬取怎么办?看 enqueue_request 的第一行判断,仅需将 Request 实例的 dont_filter 定义为 True 就可以重复爬取此请求,非常灵活。

Scrapy 就是通过此逻辑实现重复请求的过滤逻辑,默认重复请求是不会进行重复抓取的。

 

 

下载请求

 

第一次请求进来后,肯定是不重复的,那么则会正常进入调度器队列。然后再进行下一次调度,再次调用_next_request_from_scheduler 方法,此时调用调度器的 next_request 方法,就是从调度器队列中取出一个请求,这次就要开始进行网络下载了,也就是调用 _download(scrapy/core/engine.py)

在进行网络下载时,调用了Downloaderfetch(scrapy/core/downloader/__init__.py)

这里调用下载器中间件的download方法 (scrapy/core/downloader/middleware.py),并注册下载成功的回调方法是_enqueue_request,来看下载方法:

在下载过程中,首先先找到所有定义好的下载器中间件,包括内置定义好的,也可以自己扩展下载器中间件,下载前先依次执行process_request 方法,可对 request 进行加工、处理、校验等操作,然后发起真正的网络下载,也就是第一个参数download_func,在这里是 Downloader _enqueue_request 方法:

下载成功后回调 Downloader _enqueue_request(scrapy/core/downloader/__init__.py)

在这里,也维护了一个下载队列,可根据配置达到延迟下载的要求。真正发起下载请求的是调用了self.handlers.download_request(scrapy/core/downloader/handlers/__init__.py)

下载前,先通过解析requestscheme来获取对应的下载处理器,默认配置文件中定义的下载处理器:

然后调用 download_request 方法,完成网络下载,这里不再详细讲解每个处理器的实现,简单来说你就把它想象成封装好的网络下载库,输入URL,输出下载结果就好了,这样方便理解。

在下载过程中,如果发生异常情况,则会依次调用下载器中间件的process_exception方法,每个中间件只需定义自己的异常处理逻辑即可。

如果下载成功,则会依次执行下载器中间件的 process_response 方法,每个中间件可以进一步处理下载后的结果,最终返回。

这里值得提一下,除了process_request 方法是每个中间件顺序执行的,而 process_response 和 process_exception 方法是每个中间件倒序执行的,具体可看一下 DownaloderMiddlewareManager 的 _add_middleware 方法,可明白是如何注册这个方法链的。

拿到最终的下载结果后,再回到 ExecuteEngine 的 _next_request_from_scheduler 方法,会看到调用了_handle_downloader_output 方法,也就是处理下载结果的逻辑:

拿到下载结果后,主要分2个逻辑,如果是 Request 实例,则直接再次放入 Scheduler 请求队列。如果是 Response 或 Failure 实例,则调用 Scraper 的 enqueue_scrape 方法,进行进一步处理。

Scrapyer 主要是与 Spider 模块和 Pipeline 模块进行交互。

 

 

处理下载结果

 

请求入队逻辑不用再说,前面已经讲过。现在主要看Scraperenqueue_scrape,看Scraper组件是如何处理后续逻辑的:

(scrapy/core/scraper.py)

首先加入到Scraper的处理队列中,然后从队列中获取到任务,如果不是异常结果,则调用 爬虫中间件管理器 的 scrape_response 方法:

有没有感觉套路很熟悉?与上面下载器中间件调用方式非常相似,也调用一系列的前置方法,再执行真正的处理逻辑,然后执行一些列的后置方法。

 

 

回调爬虫

 

在这里真正的处理逻辑是call_spider,也就是回调我们写的爬虫类:(scrapy/core/scraper.py)

看到这里,你应该更熟悉,平时我们写的最多的爬虫模块的parse则是第一个回调方法,后续爬虫模块拿到下载结果,可定义下载后的callback就是在这里进行回调执行的。

 

 

处理输出

 

在与爬虫模块交互完成之后,Scraper调用了handle_spider_output方法处理输出结果:

我们编写爬虫类时,写的那些回调方法处理逻辑,也就是在这里被回调执行,执行完自定义的解析逻辑后,解析方法可返回新的 Request 或 BaseItem 实例,如果是新的请求,则再次通过 Scheduler 进入请求队列,如果是 BaseItem 实例,则调用 Pipeline 管理器,依次执行 process_item,也就是我们想输出结果时,只定义 Pepeline 类,然后重写这个方法就可以了。

ItemPipeManager 处理逻辑:

可以看到ItemPipeManager也是一个中间件,和之前下载器中间件管理器和爬虫中间件管理器类似,如果子类有定义process_item,则依次执行它。

执行完后,调用_itemproc_finished

这里可以看到,如果想在 Pipeline中 丢弃某个结果,直接抛出 DropItem 异常即可,Scrapy 会进行对应的处理。

到这里,抓取结果根据自定义的输出类输出到指定位置,而新的 Request 则会再次进入请求队列,等待引擎下一次调度,也就是再次调用 ExecutionEngine 的 _next_request 方法,直至请求队列没有新的任务,整个程序退出。

 

 

CrawlerSpider

 

这里也简单说一下 CrawlerSpider 类,它其实就继承了 Spider 类,然后重写了 parse 方法(这也是集成此类不要重写此方法的原因),并结合Rule等规则类,来完成Request的自动提取逻辑。

由此也可看出,Scrapy 的每个模块的实现都非常纯粹,每个组件都通过配置文件定义连接起来,如果想要扩展或替换,只需定义并实现自己的处理逻辑即可,其他模块均不受任何影响,这也导致编写一个插件是变得多么容易!

 

 

总结

 

总结一下整个运行流程,还是用这两张图表示再清楚不过:

 

Scrapy整体给我的感觉是,虽然它提供的只是单机版的爬虫框架,但我们可以通过编写更多的插件和替换某些组件,来定制化自己的爬虫,从而来实现更强大的功能,例如分布式、代理调度、并发控制、可视化、监控等等功能,都是非常方便的!

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/495940.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

普通电阻触摸屏多点触摸低成本解决方 转载

苹果公司iPhone的成功将多点触摸技术推到了一个前所未有的高度&#xff0c;经典的弹钢琴应用程序可以支持超过5点的同时触摸&#xff0c;虽然这一性能并不见得有太多的实用价值&#xff0c;但绝对带给了用户技术无限领先的震撼感。苹果公司的iPhone采用电容屏和他们的专利技术来…

Gartner:2018年前沿技术预测

本文转载自科技中国&#xff0c;作者&#xff1a;孟海华(上海市科学学研究所)&#xff0c;首发刊载于《科技中国》杂志2018年3月 第3期 预测。一、人工智能全球领先的信息技术研究与顾问公司Gartner认为&#xff0c;2018年将是人工智能大众化应用的开始&#xff0c;将影响到企业…

@font-face

问题描述&#xff1a; 产品展示的界面上有个产品编号&#xff0c;由后台程序动态生成&#xff0c;如图 而"875"的字体是特殊字体&#xff0c;如果客户端系统上没有安装该特殊字体&#xff0c;就达不到原设计效果。 解决办法(我认为的三种)&#xff1a;1。通过FLASH实…

扎克伯格|在美国国会数据门听证会上的证词-中英文全文

来源&#xff1a;网络法前哨美国时间2018年4月10日至11日&#xff0c;Facebook公司CEO马克-扎克伯格&#xff08;Mark Zuckerberg&#xff09;将在美国国会就“剑桥分析丑闻”作证。4月10日&#xff0c;扎克伯格已经参加了美国参议院司法与商业委员会举行的听证会。4月11日&…

Scrapy-Link Extractors(链接提取器)

Link Extractors 中文文档&#xff1a;https://scrapy-chs.readthedocs.io/zh_CN/1.0/topics/link-extractors.html Link Extractors 英文文档&#xff1a;http://doc.scrapy.org/en/latest/topics/link-extractors.html 利用爬虫Scrapy中的LinkExtractor&#xff08;链接提取器…

Java8 Stream详解~Stream 创建

Stream可以通过集合数组创建。 1、通过 java.util.Collection.stream() 方法用集合创建流 List<String> list Arrays.asList("a", "b", "c"); // 创建一个顺序流 Stream<String> stream list.stream(); // 创建一个并行流 Strea…

一张图:AI领域里各领风骚的BAT三巨头

来源&#xff1a;网络大数据近日&#xff0c;国内人工智能初创企业商汤科技完成6亿美元融资&#xff0c;由阿里巴巴集团领投&#xff0c;创下目前为止人工智能领域最大的一笔单轮融资。其实在过去两年里&#xff0c;BAT(百度、阿里巴巴、腾讯)就已在人工智能领域里纷纷交出了漂…

Scrapy - Request 和 Response(请求和响应)

Requests and Responses&#xff1a;http://doc.scrapy.org/en/latest/topics/request-response.html Requests and Responses&#xff08;中文版&#xff09;&#xff1a;https://scrapy-chs.readthedocs.io/zh_CN/latest/topics/request-response.html 请求 和 响应 通常&am…

一篇文章了解生物特征识别六大技术

来源&#xff1a;赵松科学网博客生物识别技术&#xff0c;通过计算机与光学、声学、生物传感器和生物统计学原理等高科技手段密切结合&#xff0c;利用人体固有的生理特性&#xff08;如指纹、脸象、虹膜等&#xff09;和行为特征&#xff08;如笔迹、声音、步态等&#xff09;…

Java8 Stream详解~遍历/匹配(foreach/find/match)

Stream也是支持类似集合的遍历和匹配元素的&#xff0c;只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。 // import已省略&#xff0c;请自行添加&#xff0c;后面代码亦是public class StreamTest {public static void main(String[] args) {List<…

Scrapy-Item Pipeline(项目管道)

Item Pipeline&#xff08;英文版&#xff09;&#xff1a;http://doc.scrapy.org/en/latest/topics/item-pipeline.html Item Pipeline&#xff08;中文版&#xff09;&#xff1a;https://scrapy-chs.readthedocs.io/zh_CN/latest/topics/item-pipeline.html Scrapy 1.3 文…

Java8 Stream详解~筛选:filter

筛选&#xff0c;是按照一定的规则校验流中的元素&#xff0c;将符合条件的元素提取到新的流中的操作。 「案例一&#xff1a;筛选出Integer集合中大于7的元素&#xff0c;并打印出来」 public class StreamTest {public static void main(String[] args) {List<Integer>…

《全球人工智能产业地图》发布(附PPT图片)

来源&#xff1a;中国信息通信研究院CAICT摘要&#xff1a;工业和信息化部、电子信息企业、人工智能企业、互联网企业、电信运营商、研究机构、社团组织、高校等代表参会&#xff0c;一致对《全球人工智能产业地图》表示高度肯定和认可。2018年4月10日&#xff0c;在工业和信息…

Java8 Stream详解~聚合(max/min/count)

max、min、count这些字眼你一定不陌生&#xff0c;没错&#xff0c;在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法&#xff0c;极大地方便了我们对集合、数组的数据统计工作。 「案例一&#xff1a;获取String集合中最长的元素。」 public class S…

为何学习新知识这么难?因为大脑可能比你想象中更死板

来源&#xff1a;科研圈撰文 John Rennie 翻译 齐睿娟 审校 魏潇某些情况下&#xff0c;大脑的适应能力似乎是用之不竭的。但通过观察学习状态下的大脑活动&#xff0c;科学家们发现&#xff0c;这一过程中大脑的神经元网络功能出乎意料地死板和低效。学习能力是人类智力的…

vs2010 学习Silverlight学习笔记(8):使用用户控件

概要&#xff1a; 这个类似于封装控件样式。不过封装的是整个或是多个控件罢了&#xff0c;然后用的时候就可以直接引用过来了。 创建用户控&#xff1a; 这个也很简单&#xff0c;不过有几个地方需要注意下。这个就不照抄了&#xff0c;咱们也自己写一个。  步骤&#xff1a…

群雄逐鹿,谁将赢得5G时代的物联网战争?

来源&#xff1a;IT港摘要&#xff1a;5G时代的物联网机遇&#xff0c;是一次重大产业变革机会&#xff0c;谁都不想错过&#xff0c;但谁能享受到这波红利&#xff0c;我们还需拭目以待。日本首富&#xff0c;软银集团创始人孙正义是全球科技界的传奇&#xff0c;他曾投资了两…

Java8 Stream详解~映射(map/flatMap)

映射&#xff0c;可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap&#xff1a; map&#xff1a;接收一个函数作为参数&#xff0c;该函数会被应用到每个元素上&#xff0c;并将其映射成一个新的元素。 flatMap&#xff1a;接收一个函数作为参数&…

Scrapy-redis 源码分析 及 框架使用

From&#xff1a;https://blog.csdn.net/weixin_37947156/article/details/75044971 From&#xff1a;https://cuiqingcai.com/6058.html Scrapy-redis github&#xff1a;https://github.com/rmax/scrapy-redis scrapy-redis分布式爬虫框架详解&#xff1a;https://segmentfa…

Java8 Stream详解~归约(reduce)

归约&#xff0c;也称缩减&#xff0c;顾名思义&#xff0c;是把一个流缩减成一个值&#xff0c;能实现对集合求和、求乘积和求最值操作。 「案例一&#xff1a;求Integer集合的元素之和、乘积和最大值。」 public class StreamTest {public static void main(String[] args) …