看了上一节分析的scrapy
架构,我们最想了解的,应该就是5大核心模块是怎么实现的吧。好,从github中找到各大核心模块的源码:
- (1) Engine:scrapy/scrapy/core/engine.py
- (2) Scheduler:scrapy/scrapy/core/scheduler.py
- (3) Downloader:scrapy/scrapy/core/downloader/
- (4) Spider:scrapy/scrapy/spider.py
- (5) Item pipeline:scrapy/scrapy/pipelines/
这些模块,不是用class实现的,就是用package实现的。看模块代码顶多能了解它们的功能(成员函数),根本看不出scrapy是怎么运行的。那么问题来了,scrapy是怎么被启动并运行的?
一个scrapy程序的运行过程
从(1)中我们可以看到,我们自己写一个scrapy程序,这个程序的运行,是从命令scrapy crawl hello
开始的。
那这条命令到底做了哪些事呢?
crawl
命令的运行
scrapy程序的源代码结构是非常清晰的,可以“猜”到crawl
命令的代码在这里:
https://github.com/scrapy/scrapy/blob/master/scrapy/commands/crawl.py
看样子是用command模式写的,命令会执行run()
中的代码,去掉异常处理部分,其实crawl
命令最核心的代码就是下面两条:
self.crawler_process.crawl(spname, **opts.spargs)
self.crawler_process.start()
只要能找到self.crawler_process.start()
的代码,就能搞清楚crawl
命令的运行细节了。要搞明白这两条代码做了什么,必须知道crawler_process
是什么。在github中搜一下这个crawler_process
,发现
cmd.crawler_process = CrawlerProcess(settings)
继续搜CrawlerProcess
(github的搜索功能用起来真是爽)。终于在scrapy/scrapy/crawler.py
里发现了我们需要寻找的CrawlerProcess.start()
函数。
CrawlerProcess.start()
的逻辑
通过上面的分析,scrapy程序的启动,就是执行了self.crawler_process.start()
,跟进去发现其实是调用了CrawlerProcess.start()
函数。从代码注释,可看到这个函数做了那么几件事:
- (1) 初始化一个Twisted
reactor
对象 - (2) 调整这个
reactor
对象的线程池大小 - (3) 创建一个DNS缓存,并调整缓存大小
- (4) 判断是否有其他爬虫没跑结束的,必须等所有爬虫跑结束才启动该爬虫
- (5) 然后运行
reactor.run(installSignalHandlers=False)
然后这个reactor
就run了,嗯?!这个reactor
是什么鸟,它run啥内容了呢?
在这个crawler.py
文件中看了一圈,也没发现往reactor
中传入什么handler之类的参数。不过倒是发现shutdown/kill一个reactor是很容易的。
代码跟到这里,必须要看一下Twisted
框架的reactor
对象运行机制是怎么样的了。
Twisted reactor
对象
Twisted
是python的异步网络编程框架。作为一个框架,它有自己的编程套路。这个套路,就是传说中的“异步编程模式/事件驱动模式”。 事件驱动的特点是包含一个事件循环(loop),当外部事件发生时用回调机制来触发相应的事件处理代码。
而reactor
,就是I/O并发模型中“reactor模式”的实现。从(3)中可知,reactor
就实现了这个事件环(loop)。
而最重要的是,reactor
模式,实现了单线程环境中,调度多个事件源产生的事件到它们各自的事件处理例程中去。借助这个reactor模式,用单线程,就实现了事件处理机制(callback)。
总结一下,reactor
只是一种设计模式,就是一个代码框架而已。真正的代码逻辑,应该在调用reactor.run()
之前就搞定了的。所以想弄明白CrawlerProcess.start()
,得看这个函数调用之前,做了哪些事。
能做啥事呢?也就是CrawlerProcess
类,以及它父类的初始化。
CrawlerProcess类
CrawlerProcess
类的父类是CrawlerRunner
类。看看它们的初始化工作都做了啥。
CrawlerRunner类
看它的注释,“keeps track of, manages and runs crawlers inside an already setup Twisted reactor
”。看到了吧,它就是管理reactor
中的各个爬虫的。
这个类的初始化,主要代码就是下面两行:
self.settings = settings
self.spider_loader = _get_spider_loader(settings)
加载配置,并根据配置加载spider
。所谓加载spider
,从scrapy/scrapy/spiderloader.py
可以看出,就是加载各个sipder
的属性(name等等)。
CrawlerProcess类
父类初始化后,子类CrawlerProcess
才执行初始化。而CrawlerProcess
类的功能,根据注释:run multiple scrapy crawlers in a process simultaneously。就是在单进程中跑多个爬虫(用twisted的reactor实现)。这个类里就实现了scrapy的“异步编程模式/事件驱动模式”。
它之所以叫xxxProcess,作者想说的就是“单进程”的意思吧。
这个类除了实现reactor模式,还添加了log
,shutdown
信号处理(Ctrl+C)功能。
总结
理一下,当我们运行scrapy crawl hello
命令,就启动了一个scrapy爬虫,它的启动过程是这样的:
- (1) 加载用户配置,加载所有
spider
(父类CrawlerRunner
类初始化) - (2) 初始化
log
,与shutdown
(Ctrl+C)信号处理机制(子类CrawlerProcess
类初始化) - (3) 运行
CrawlerProcess.start()
,依次完成下面的逻辑:- (3.1) 初始化一个Twisted
reactor
对象,调整这个reactor
对象的线程池大小 - (3.2) 创建一个DNS缓存,并调整缓存大小
- (3.3) 判断是否有其他爬虫没跑结束的,必须等所有爬虫跑结束才启动当前爬虫
- (3.1) 初始化一个Twisted
- (4) 然后运行
reactor.run()
。至此,第一步加载的spider
,都放在reactor模式中运行了。
每个spider
有自己的name
,start_urls
等属性。根据(4),scrapy会为每一个URL创建scrapy.Request
对象,并将spider
的parse()
方法作为scrapy.Request
对象的callback
函数。
参考
- (1) 简单scrapy程序的运行,https://github.com/ybdesire/WebLearn/tree/master/23_scrapy/hello_scrapy
- (2) twisted.internet.reactor,https://twistedmatrix.com/documents/current/api/twisted.internet.reactor.html
- (3) https://likebeta.gitbooks.io/twisted-intro-cn/content/zh/p03.html
- (4) scrapy的运行,http://doc.scrapy.org/en/latest/intro/tutorial.html#what-just-happened-under-the-hood