儿童白癜风的症状 http://m.39.net/pf/a_4844670.html在上一篇文章:Scrapy源码剖析:Scrapy是如何运行起来的?我们主要剖析了Scrapy是如何运行起来的核心逻辑,也就是在真正执行抓取任务之前,Scrapy都做了哪些工作。这篇文章,我们就来进一步剖析一下,Scrapy有哪些核心组件?以及它们主要负责了哪些工作?这些组件为了完成这些功能,内部又是如何实现的。爬虫类我们接着上一篇结束的地方开始讲起。上次讲到Scrapy运行起来后,执行到最后到了Crawler的crawl方法,我们来看这个方法:defer.inlineCallbacksdefcrawl(self,*args,**kwargs):assertnotself.crawling,"Crawlingalreadytakingplace"self.crawling=Truetry:#从spiderloader中找到爬虫类并实例化爬虫实例self.spider=self._create_spider(*args,**kwargs)#创建引擎self.engine=self._create_engine()#调用爬虫类的start_requests方法拿到种子URL列表start_requests=iter(self.spider.start_requests())#执行引擎的open_spider并传入爬虫实例和初始请求yieldself.engine.open_spider(self.spider,start_requests)yielddefer.maybeDeferred(self.engine.start)exceptException:ifsix.PY2:exc_info=sys.exc_info()self.crawling=Falseifself.engineisnotNone:yieldself.engine.close()ifsix.PY2:six.reraise(*exc_info)raise执行到这里,我们看到首先创建了爬虫实例,然后创建了引擎,最后把爬虫交给引擎来处理了。在上一篇文章我们也讲到,在Crawler实例化时,会创建SpiderLoader,它会根据我们定义的配置文件settings.py找到存放爬虫的位置,我们写的爬虫代码都在这里。然后SpiderLoader会扫描这些代码文件,并找到父类是scrapy.Spider爬虫类,然后根据爬虫类中的name属性(在编写爬虫时,这个属性是必填的),生成一个{spider_name:spider_cls}的字典,最后根据scrapycrawlspider_name命令中的spider_name找到我们写的爬虫类,然后实例化它,在这里就是调用了_create_spider方法:
def_create_spider(self,*args,**kwargs):#调用类方法from_crawler实例化returnself.spidercls.from_crawler(self,*args,**kwargs)实例化爬虫比较有意思,它不是通过普通的构造方法进行初始化,而是调用了类方法from_crawler进行的初始化,找到scrapy.Spider类:classmethoddeffrom_crawler(cls,crawler,*args,**kwargs):spider=cls(*args,**kwargs)spider._set_crawler(crawler)returnspiderdef_set_crawler(self,crawler):self.crawler=crawler#把settings对象赋给spider实例self.settings=crawler.settingscrawler.signals.connect(self.close,signals.spider_closed)在这里我们可以看到,这个类方法其实也是调用了构造方法,进行实例化,同时也拿到了settings配置,来看构造方法干了些什么?
classSpider(object_ref):name=Nonecustom_settings=Nonedef__init__(self,name=None,**kwargs):#name必填ifnameisnotNone:self.name=nameelifnotgetattr(self,name,None):raiseValueError("%smusthaveaname"%type(self).__name__)self.__dict__.update(kwargs)#如果没有设置start_urls默认是[]ifnothasattr(self,start_urls):self.start_urls=[]看到这里是不是很熟悉?这里就是我们平时编写爬虫类时,最常用的几个属性:name、start_urls、custom_settings:name:在运行爬虫时通过它找到我们编写的爬虫类;start_urls:抓取入口,也可以叫做种子URL;custom_settings:爬虫自定义配置,会覆盖配置文件中的配置项;引擎分析完爬虫类的初始化后,还是回到Crawler的crawl方法,紧接着就是创建引擎对象,也就是_create_engine方法,看看初始化时都发生了什么?
classExecutionEngine(object):"""引擎"""def__init__(self,crawler,spider_closed_callback):self.crawler=crawler#这里也把settings配置保存到引擎中self.settings=crawler.settings#信号self.signals=crawler.signals#日志格式self.logformatter=crawler.logformatterself.slot=Noneself.spider=Noneself.running=Falseself.paused=False#从settings中找到Scheduler调度器,找到Scheduler类self.scheduler_cls=load_object(self.settings[SCHEDULER])#同样,找到Downloader下载器类downloader_cls=load_object(self.settings[DOWNLOADER])#实例化Downloaderself.downloader=downloader_cls(crawler)#实例化Scraper它是引擎连接爬虫类的桥梁self.scraper=Scraper(crawler)self._spider_closed_callback=spider_closed_callback在这里我们能看到,主要是对其他几个核心组件进行定义和初始化,主要包括包括:Scheduler、Downloader、Scrapyer,其中Scheduler只进行了类定义,没有实例化。也就是说,引擎是整个Scrapy的核心大脑,它负责管理和调度这些组件,让这些组件更好地协调工作。下面我们依次来看这几个核心组件都是如何初始化的?调度器调度器初始化发生在引擎的open_spider方法中,我们提前来看一下调度器的初始化。
classScheduler(object):"""调度器"""def__init__(self,dupefilter,jobdir=None,dqclass=None,mqclass=None,logunser=False,stats=None,pqclass=None):#指纹过滤器self.df=dupefilter#任务队列文件夹self.dqdir=self._dqdir(jobdir)#优先级任务队列类self.pqclass=pqclass#磁盘任务队列类self.dqclass=dqclass#内存任务队列类self.mqclass=mqclass#日志是否序列化self.logunser=logunserself.stats=stats
classmethoddeffrom_crawler(cls,crawler):settings=crawler.settings#从配置文件中获取指纹过滤器类dupefilter_cls=load_object(settings[DUPEFILTER_CLASS])#实例化指纹过滤器dupefilter=dupefilter_cls.from_settings(settings)#从配置文件中依次获取优先级任务队列类、磁盘队列类、内存队列类pqclass=load_object(settings[SCHEDULER_PRIORITY_QUEUE])dqclass=load_object(settings[SCHEDULER_DISK_QUEUE])mqclass=load_object(settings[SCHEDULER_MEMORY_QUEUE])#请求日志序列化开关logunser=settings.getbool(LOG_UNSERIALIZABLE_REQUESTS,settings.getbool(SCHEDULER_DEBUG))returncls(dupefilter,jobdir=job_dir(settings),logunser=logunser,stats=crawler.stats,pqclass=pqclass,dqclass=dqclass,mqclass=mqclass)可以看到,调度器的初始化主要做了2件事:实例化请求指纹过滤器:主要用来过滤重复请求;定义不同类型的任务队列:优先级任务队列、基于磁盘的任务队列、基于内存的任务队列;请求指纹过滤器又是什么?在配置文件中,我们可以看到定义的默认指纹过滤器是RFPDupeFilter:classRFPDupeFilter(BaseDupeFilter):"""请求指纹过滤器"""def__init__(self,path=None,debug=False):self.file=None#指纹集合使用的是Set基于内存self.fingerprints=set()self.logdupes=Trueself.debug=debugself.logger=logging.getLogger(__name__)#请求指纹可存入磁盘ifpath:self.file=open(os.path.join(path,requests.seen),a+)self.file.seek(0)self.fingerprints.update(x.rstrip()forxinself.file)
classmethoddeffrom_settings(cls,settings):debug=settings.getbool(DUPEFILTER_DEBUG)returncls(job_dir(settings),debug)请求指纹过滤器初始化时,定义了指纹集合,这个集合使用内存实现的Set,而且可以控制这些指纹是否存入磁盘以供下次重复使用。也就是说,指纹过滤器的主要职责是:过滤重复请求,可自定义过滤规则。在下篇文章中我们会介绍到,每个请求是根据什么规则生成指纹的,然后是又如何实现重复请求过滤逻辑的,这里我们先知道它的功能即可。下面来看调度器定义的任务队列都有什么作用?调度器默认定义了2种队列类型:基于磁盘的任务队列:在配置文件可配置存储路径,每次执行后会把队列任务保存到磁盘上;基于内存的任务队列:每次都在内存中执行,下次启动则消失;配置文件默认定义如下:#基于磁盘的任务队列(后进先出)SCHEDULER_DISK_QUEUE=scrapy.squeues.PickleLifoDiskQueue#基于内存的任务队列(后进先出)SCHEDULER_MEMORY_QUEUE=scrapy.squeues.LifoMemoryQueue#优先级队列SCHEDULER_PRIORITY_QUEUE=queuelib.PriorityQueue如果我们在配置文件中定义了JOBDIR配置项,那么每次执行爬虫时,都会把任务队列保存在磁盘中,下次启动爬虫时可以重新加载继续执行我们的任务。如果没有定义这个配置项,那么默认使用的是内存队列。细心的你可能会发现,默认定义的这些队列结构都是后进先出的,什么意思呢?也就是在运行我们的爬虫代码时,如果生成一个抓取任务,放入到任务队列中,那么下次抓取就会从任务队列中先获取到这个任务,优先执行。这么实现意味什么呢?其实意味着:Scrapy默认的采集规则是深度优先!如何改变这种机制,变为广度优先采集呢?这时候我们就要看一下scrapy.squeues模块了,在这里定义了很多种队列:
#先进先出磁盘队列(pickle序列化)PickleFifoDiskQueue=_serializable_queue(queue.FifoDiskQueue,\_pickle_serialize,pickle.loads)#后进先出磁盘队列(pickle序列化)PickleLifoDiskQueue=_serializable_queue(queue.LifoDiskQueue,\_pickle_serialize,pickle.loads)#先进先出磁盘队列(marshal序列化)MarshalFifoDiskQueue=_serializable_queue(queue.FifoDiskQueue,\marshal.dumps,marshal.loads)#后进先出磁盘队列(marshal序列化)MarshalLifoDiskQueue=_serializable_queue(queue.LifoDiskQueue,\marshal.dumps,marshal.loads)#先进先出内存队列FifoMemoryQueue=queue.FifoMemoryQueue#后进先出内存队列LifoMemoryQueue=queue.LifoMemoryQueue如果我们想把抓取任务改为广度优先,我们只需要在配置文件中把队列类修改为先进先出队列类就可以了!从这里我们也可以看出,Scrapy各个组件之间的耦合性非常低,每个模块都是可自定义的。如果你想探究这些队列是如何实现的,可以参考Scrapy作者写的scrapy/queuelib项目,在Github上就可以找到,在这里有这些队列的具体实现。下载器回到引擎的初始化的地方,接下来我们来看,下载器是如何初始化的。在默认的配置文件default_settings.py中,下载器配置如下:
DOWNLOADER=scrapy.core.downloader.Downloader我们来看Downloader类的初始化:
classDownloader(object):"""下载器"""def__init__(self,crawler):#同样的拿到settings对象self.settings=crawler.settingsself.signals=crawler.signalsself.slots={}self.active=set()#初始化DownloadHandlersself.handlers=DownloadHandlers(crawler)#从配置中获取设置的并发数self.total_concurrency=self.settings.getint(CONCURRENT_REQUESTS)#同一域名并发数self.domain_concurrency=self.settings.getint(CONCURRENT_REQUESTS_PER_DOMAIN)#同一IP并发数self.ip_concurrency=self.settings.getint(CONCURRENT_REQUESTS_PER_IP)#随机延迟下载时间self.randomize_delay=self.settings.getbool(RANDOMIZE_DOWNLOAD_DELAY)#初始化下载器中间件self.middleware=DownloaderMiddlewareManager.from_crawler(crawler)self._slot_gc_loop=task.LoopingCall(self._slot_gc)self._slot_gc_loop.start(60)在这个过程中,主要是初始化了下载处理器、下载器中间件管理器以及从配置文件中拿到抓取请求控制的相关参数。那么下载处理器是做什么的?下载器中间件又负责哪些工作?先来看DownloadHandlers:
classDownloadHandlers(object):"""下载器处理器"""def__init__(self,crawler):self._crawler=crawlerself._schemes={}#存储scheme对应的类路径后面用于实例化self._handlers={}#存储scheme对应的下载器self._notconfigured={}#从配置中找到DOWNLOAD_HANDLERS_BASE构造下载处理器#注意:这里是调用getwithbase方法取的是配置中的XXXX_BASE配置handlers=without_none_values(crawler.settings.getwithbase(DOWNLOAD_HANDLERS))#存储scheme对应的类路径后面用于实例化forscheme,clspathinsix.iteritems(handlers):self._schemes[scheme]=clspathcrawler.signals.connect(self._close,signals.engine_stopped)下载处理器在默认的配置文件中是这样配置的:
#用户可自定义的下载处理器DOWNLOAD_HANDLERS={}#默认的下载处理器DOWNLOAD_HANDLERS_BASE={file:scrapy.core.downloader.handlers.file.FileDownloadHandler,