Scrapy源碼剖析:Scrapy有哪些核心組件?
在上一篇文章:Scrapy源碼剖析:Scrapy是如何運(yùn)行起來的?我們主要剖析了 Scrapy 是如何運(yùn)行起來的核心邏輯,也就是在真正執(zhí)行抓取任務(wù)之前,Scrapy 都做了哪些工作。
這篇文章,我們就來進(jìn)一步剖析一下,Scrapy 有哪些核心組件?以及它們主要負(fù)責(zé)了哪些工作?這些組件為了完成這些功能,內(nèi)部又是如何實(shí)現(xiàn)的。
爬蟲類
我們接著上一篇結(jié)束的地方開始講起。上次講到 Scrapy 運(yùn)行起來后,執(zhí)行到最后到了 Crawler 的 crawl 方法,我們來看這個(gè)方法:
- @defer.inlineCallbacks
- def crawl(self, *args, **kwargs):
- assert not self.crawling, "Crawling already taking place"
- self.crawling = True
- try:
- # 從spiderloader中找到爬蟲類 并實(shí)例化爬蟲實(shí)例
- selfself.spider = self._create_spider(*args, **kwargs)
- # 創(chuàng)建引擎
- selfself.engine = self._create_engine()
- # 調(diào)用爬蟲類的start_requests方法 拿到種子URL列表
- start_requests = iter(self.spider.start_requests())
- # 執(zhí)行引擎的open_spider 并傳入爬蟲實(shí)例和初始請(qǐng)求
- yield self.engine.open_spider(self.spider, start_requests)
- yield defer.maybeDeferred(self.engine.start)
- except Exception:
- if six.PY2:
- exc_info = sys.exc_info()
- self.crawling = False
- if self.engine is not None:
- yield self.engine.close()
- if six.PY2:
- six.reraise(*exc_info)
- raise
執(zhí)行到這里,我們看到首先創(chuàng)建了爬蟲實(shí)例,然后創(chuàng)建了引擎,最后把爬蟲交給引擎來處理了。
在上一篇文章我們也講到,在 Crawler 實(shí)例化時(shí),會(huì)創(chuàng)建 SpiderLoader,它會(huì)根據(jù)我們定義的配置文件 settings.py 找到存放爬蟲的位置,我們寫的爬蟲代碼都在這里。
然后 SpiderLoader 會(huì)掃描這些代碼文件,并找到父類是 scrapy.Spider 爬蟲類,然后根據(jù)爬蟲類中的 name 屬性(在編寫爬蟲時(shí),這個(gè)屬性是必填的),生成一個(gè) {spider_name: spider_cls} 的字典,最后根據(jù) scrapy crawl <spider_name> 命令中的 spider_name 找到我們寫的爬蟲類,然后實(shí)例化它,在這里就是調(diào)用了_create_spider方法:
- def _create_spider(self, *args, **kwargs):
- # 調(diào)用類方法from_crawler實(shí)例化
- return self.spidercls.from_crawler(self, *args, **kwargs)
實(shí)例化爬蟲比較有意思,它不是通過普通的構(gòu)造方法進(jìn)行初始化,而是調(diào)用了類方法 from_crawler 進(jìn)行的初始化,找到 scrapy.Spider 類:
- @classmethod
- def from_crawler(cls, crawler, *args, **kwargs):
- spider = cls(*args, **kwargs)
- spider._set_crawler(crawler)
- return spider
- def _set_crawler(self, crawler):
- self.crawler = crawler
- # 把settings對(duì)象賦給spider實(shí)例
- self.settings = crawler.settings
- crawler.signals.connect(self.close, signals.spider_closed)
在這里我們可以看到,這個(gè)類方法其實(shí)也是調(diào)用了構(gòu)造方法,進(jìn)行實(shí)例化,同時(shí)也拿到了 settings 配置,來看構(gòu)造方法干了些什么?
- class Spider(object_ref):
- name = None
- custom_settings = None
- def __init__(self, name=None, **kwargs):
- # name必填
- if name is not None:
- self.name = name
- elif not getattr(self, 'name', None):
- raise ValueError("%s must have a name" % type(self).__name__)
- self.__dict__.update(kwargs)
- # 如果沒有設(shè)置start_urls 默認(rèn)是[]
- if not hasattr(self, 'start_urls'):
- self.start_urls = []
看到這里是不是很熟悉?這里就是我們平時(shí)編寫爬蟲類時(shí),最常用的幾個(gè)屬性:name、start_urls、custom_settings:
- name:在運(yùn)行爬蟲時(shí)通過它找到我們編寫的爬蟲類;
- start_urls:抓取入口,也可以叫做種子URL;
- custom_settings:爬蟲自定義配置,會(huì)覆蓋配置文件中的配置項(xiàng);
引擎
分析完爬蟲類的初始化后,還是回到 Crawler 的 crawl 方法,緊接著就是創(chuàng)建引擎對(duì)象,也就是 _create_engine 方法,看看初始化時(shí)都發(fā)生了什么?
- class ExecutionEngine(object):
- """引擎"""
- def __init__(self, crawler, spider_closed_callback):
- self.crawler = crawler
- # 這里也把settings配置保存到引擎中
- self.settings = crawler.settings
- # 信號(hào)
- self.signals = crawler.signals
- # 日志格式
- self.logformatter = crawler.logformatter
- self.slot = None
- self.spider = None
- self.running = False
- self.paused = False
- # 從settings中找到Scheduler調(diào)度器,找到Scheduler類
- self.scheduler_cls = load_object(self.settings['SCHEDULER'])
- # 同樣,找到Downloader下載器類
- downloader_cls = load_object(self.settings['DOWNLOADER'])
- # 實(shí)例化Downloader
- self.downloader = downloader_cls(crawler)
- # 實(shí)例化Scraper 它是引擎連接爬蟲類的橋梁
- self.scraper = Scraper(crawler)
- self._spider_closed_callback = spider_closed_callback
在這里我們能看到,主要是對(duì)其他幾個(gè)核心組件進(jìn)行定義和初始化,主要包括包括:Scheduler、Downloader、Scrapyer,其中 Scheduler 只進(jìn)行了類定義,沒有實(shí)例化。
也就是說,引擎是整個(gè) Scrapy 的核心大腦,它負(fù)責(zé)管理和調(diào)度這些組件,讓這些組件更好地協(xié)調(diào)工作。
下面我們依次來看這幾個(gè)核心組件都是如何初始化的?
調(diào)度器
調(diào)度器初始化發(fā)生在引擎的 open_spider 方法中,我們提前來看一下調(diào)度器的初始化。
- class Scheduler(object):
- """調(diào)度器"""
- def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,
- logunser=False, stats=None, pqclass=None):
- # 指紋過濾器
- self.df = dupefilter
- # 任務(wù)隊(duì)列文件夾
- selfself.dqdir = self._dqdir(jobdir)
- # 優(yōu)先級(jí)任務(wù)隊(duì)列類
- self.pqclass = pqclass
- # 磁盤任務(wù)隊(duì)列類
- self.dqclass = dqclass
- # 內(nèi)存任務(wù)隊(duì)列類
- self.mqclass = mqclass
- # 日志是否序列化
- self.logunser = logunser
- self.stats = stats
- @classmethod
- def from_crawler(cls, crawler):
- settings = crawler.settings
- # 從配置文件中獲取指紋過濾器類
- dupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])
- # 實(shí)例化指紋過濾器
- dupefilter = dupefilter_cls.from_settings(settings)
- # 從配置文件中依次獲取優(yōu)先級(jí)任務(wù)隊(duì)列類、磁盤隊(duì)列類、內(nèi)存隊(duì)列類
- pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
- dqclass = load_object(settings['SCHEDULER_DISK_QUEUE'])
- mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
- # 請(qǐng)求日志序列化開關(guān)
- logunser = settings.getbool('LOG_UNSERIALIZABLE_REQUESTS', settings.getbool('SCHEDULER_DEBUG'))
- return cls(dupefilter, jobdir=job_dir(settings), logunserlogunser=logunser,
- stats=crawler.stats, pqclasspqclass=pqclass, dqclassdqclass=dqclass, mqclassmqclass=mqclass)
可以看到,調(diào)度器的初始化主要做了 2 件事:
- 實(shí)例化請(qǐng)求指紋過濾器:主要用來過濾重復(fù)請(qǐng)求;
- 定義不同類型的任務(wù)隊(duì)列:優(yōu)先級(jí)任務(wù)隊(duì)列、基于磁盤的任務(wù)隊(duì)列、基于內(nèi)存的任務(wù)隊(duì)列;
請(qǐng)求指紋過濾器又是什么?
在配置文件中,我們可以看到定義的默認(rèn)指紋過濾器是 RFPDupeFilter:
- class RFPDupeFilter(BaseDupeFilter):
- """請(qǐng)求指紋過濾器"""
- def __init__(self, path=None, debug=False):
- self.file = None
- # 指紋集合 使用的是Set 基于內(nèi)存
- self.fingerprints = set()
- self.logdupes = True
- self.debug = debug
- self.logger = logging.getLogger(__name__)
- # 請(qǐng)求指紋可存入磁盤
- 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 = settings.getbool('DUPEFILTER_DEBUG')
- return cls(job_dir(settings), debug)
請(qǐng)求指紋過濾器初始化時(shí),定義了指紋集合,這個(gè)集合使用內(nèi)存實(shí)現(xiàn)的 Set,而且可以控制這些指紋是否存入磁盤以供下次重復(fù)使用。
也就是說,指紋過濾器的主要職責(zé)是:過濾重復(fù)請(qǐng)求,可自定義過濾規(guī)則。
在下篇文章中我們會(huì)介紹到,每個(gè)請(qǐng)求是根據(jù)什么規(guī)則生成指紋的,然后是又如何實(shí)現(xiàn)重復(fù)請(qǐng)求過濾邏輯的,這里我們先知道它的功能即可。
下面來看調(diào)度器定義的任務(wù)隊(duì)列都有什么作用?
調(diào)度器默認(rèn)定義了 2 種隊(duì)列類型:
- 基于磁盤的任務(wù)隊(duì)列:在配置文件可配置存儲(chǔ)路徑,每次執(zhí)行后會(huì)把隊(duì)列任務(wù)保存到磁盤上;
- 基于內(nèi)存的任務(wù)隊(duì)列:每次都在內(nèi)存中執(zhí)行,下次啟動(dòng)則消失;
配置文件默認(rèn)定義如下:
- # 基于磁盤的任務(wù)隊(duì)列(后進(jìn)先出)
- SCHEDULER_DISK_QUEUE = 'scrapy.squeues.PickleLifoDiskQueue'
- # 基于內(nèi)存的任務(wù)隊(duì)列(后進(jìn)先出)
- SCHEDULER_MEMORY_QUEUE = 'scrapy.squeues.LifoMemoryQueue'
- # 優(yōu)先級(jí)隊(duì)列
- SCHEDULER_PRIORITY_QUEUE = 'queuelib.PriorityQueue'
如果我們?cè)谂渲梦募卸x了 JOBDIR 配置項(xiàng),那么每次執(zhí)行爬蟲時(shí),都會(huì)把任務(wù)隊(duì)列保存在磁盤中,下次啟動(dòng)爬蟲時(shí)可以重新加載繼續(xù)執(zhí)行我們的任務(wù)。
如果沒有定義這個(gè)配置項(xiàng),那么默認(rèn)使用的是內(nèi)存隊(duì)列。
細(xì)心的你可能會(huì)發(fā)現(xiàn),默認(rèn)定義的這些隊(duì)列結(jié)構(gòu)都是后進(jìn)先出的,什么意思呢?
也就是在運(yùn)行我們的爬蟲代碼時(shí),如果生成一個(gè)抓取任務(wù),放入到任務(wù)隊(duì)列中,那么下次抓取就會(huì)從任務(wù)隊(duì)列中先獲取到這個(gè)任務(wù),優(yōu)先執(zhí)行。
這么實(shí)現(xiàn)意味什么呢?其實(shí)意味著:Scrapy 默認(rèn)的采集規(guī)則是深度優(yōu)先!
如何改變這種機(jī)制,變?yōu)閺V度優(yōu)先采集呢?這時(shí)候我們就要看一下 scrapy.squeues 模塊了,在這里定義了很多種隊(duì)列:
- # 先進(jìn)先出磁盤隊(duì)列(pickle序列化)
- PickleFifoDiskQueue = _serializable_queue(queue.FifoDiskQueue, \
- _pickle_serialize, pickle.loads)
- # 后進(jìn)先出磁盤隊(duì)列(pickle序列化)
- PickleLifoDiskQueue = _serializable_queue(queue.LifoDiskQueue, \
- _pickle_serialize, pickle.loads)
- # 先進(jìn)先出磁盤隊(duì)列(marshal序列化)
- MarshalFifoDiskQueue = _serializable_queue(queue.FifoDiskQueue, \
- marshal.dumps, marshal.loads)
- # 后進(jìn)先出磁盤隊(duì)列(marshal序列化)
- MarshalLifoDiskQueue = _serializable_queue(queue.LifoDiskQueue, \
- marshal.dumps, marshal.loads)
- # 先進(jìn)先出內(nèi)存隊(duì)列
- FifoMemoryQueue = queue.FifoMemoryQueue
- # 后進(jìn)先出內(nèi)存隊(duì)列
- LifoMemoryQueue = queue.LifoMemoryQueue
如果我們想把抓取任務(wù)改為廣度優(yōu)先,我們只需要在配置文件中把隊(duì)列類修改為先進(jìn)先出隊(duì)列類就可以了!從這里我們也可以看出,Scrapy 各個(gè)組件之間的耦合性非常低,每個(gè)模塊都是可自定義的。
如果你想探究這些隊(duì)列是如何實(shí)現(xiàn)的,可以參考 Scrapy 作者寫的 scrapy/queuelib 項(xiàng)目,在 Github 上就可以找到,在這里有這些隊(duì)列的具體實(shí)現(xiàn)。
下載器
回到引擎的初始化的地方,接下來我們來看,下載器是如何初始化的。
在默認(rèn)的配置文件 default_settings.py 中,下載器配置如下:
- DOWNLOADER = 'scrapy.core.downloader.Downloader'
我們來看 Downloader 類的初始化:
- class Downloader(object):
- """下載器"""
- def __init__(self, crawler):
- # 同樣的 拿到settings對(duì)象
- self.settings = crawler.settings
- self.signals = crawler.signals
- self.slots = {}
- self.active = set()
- # 初始化DownloadHandlers
- self.handlers = DownloadHandlers(crawler)
- # 從配置中獲取設(shè)置的并發(fā)數(shù)
- selfself.total_concurrency = self.settings.getint('CONCURRENT_REQUESTS')
- # 同一域名并發(fā)數(shù)
- selfself.domain_concurrency = self.settings.getint('CONCURRENT_REQUESTS_PER_DOMAIN')
- # 同一IP并發(fā)數(shù)
- selfself.ip_concurrency = self.settings.getint('CONCURRENT_REQUESTS_PER_IP')
- # 隨機(jī)延遲下載時(shí)間
- selfself.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)
在這個(gè)過程中,主要是初始化了下載處理器、下載器中間件管理器以及從配置文件中拿到抓取請(qǐng)求控制的相關(guān)參數(shù)。
那么下載處理器是做什么的?下載器中間件又負(fù)責(zé)哪些工作?
先來看 DownloadHandlers:
- class DownloadHandlers(object):
- """下載器處理器"""
- def __init__(self, crawler):
- self._crawler = crawler
- self._schemes = {} # 存儲(chǔ)scheme對(duì)應(yīng)的類路徑 后面用于實(shí)例化
- self._handlers = {} # 存儲(chǔ)scheme對(duì)應(yīng)的下載器
- self._notconfigured = {}
- # 從配置中找到DOWNLOAD_HANDLERS_BASE 構(gòu)造下載處理器
- # 注意:這里是調(diào)用getwithbase方法 取的是配置中的XXXX_BASE配置
- handlers = without_none_values(
- crawler.settings.getwithbase('DOWNLOAD_HANDLERS'))
- # 存儲(chǔ)scheme對(duì)應(yīng)的類路徑 后面用于實(shí)例化
- for scheme, clspath in six.iteritems(handlers):
- self._schemes[scheme] = clspath
- crawler.signals.connect(self._close, signals.engine_stopped)
下載處理器在默認(rèn)的配置文件中是這樣配置的:
- # 用戶可自定義的下載處理器
- DOWNLOAD_HANDLERS = {}
- # 默認(rèn)的下載處理器
- DOWNLOAD_HANDLERS_BASE = {
- 'file': 'scrapy.core.downloader.handlers.file.FileDownloadHandler',
- 'http': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
- 'https': 'scrapy.core.downloader.handlers.http.HTTPDownloadHandler',
- 's3': 'scrapy.core.downloader.handlers.s3.S3DownloadHandler',
- 'ftp': 'scrapy.core.downloader.handlers.ftp.FTPDownloadHandler',
- }
看到這里你應(yīng)該能明白了,下載處理器會(huì)根據(jù)下載資源的類型,選擇對(duì)應(yīng)的下載器去下載資源。其中我們最常用的就是 http 和 https 對(duì)應(yīng)的處理器。
但是請(qǐng)注意,在這里,這些下載器是沒有被實(shí)例化的,只有在真正發(fā)起網(wǎng)絡(luò)請(qǐng)求時(shí),才會(huì)進(jìn)行初始化,而且只會(huì)初始化一次,后面文章會(huì)講到。
下面我們來看下載器中間件 DownloaderMiddlewareManager 初始化過程,同樣地,這里又調(diào)用了類方法 from_crawler 進(jìn)行初始化,而且 DownloaderMiddlewareManager 繼承了MiddlewareManager 類,來看它在初始化做了哪些工作:
- class MiddlewareManager(object):
- """所有中間件的父類,提供中間件公共的方法"""
- component_name = 'foo middleware'
- @classmethod
- def from_crawler(cls, crawler):
- # 調(diào)用from_settings
- return cls.from_settings(crawler.settings, crawler)
- @classmethod
- def from_settings(cls, settings, crawler=None):
- # 調(diào)用子類_get_mwlist_from_settings得到所有中間件類的模塊
- mwlist = cls._get_mwlist_from_settings(settings)
- middlewares = []
- enabled = []
- # 依次實(shí)例化
- for clspath in mwlist:
- try:
- # 加載這些中間件模塊
- mwcls = load_object(clspath)
- # 如果此中間件類定義了from_crawler 則調(diào)用此方法實(shí)例化
- if crawler and hasattr(mwcls, 'from_crawler'):
- mw = mwcls.from_crawler(crawler)
- # 如果此中間件類定義了from_settings 則調(diào)用此方法實(shí)例化
- elif hasattr(mwcls, 'from_settings'):
- mw = mwcls.from_settings(settings)
- # 上面2個(gè)方法都沒有,則直接調(diào)用構(gòu)造實(shí)例化
- else:
- mw = mwcls()
- middlewares.append(mw)
- enabled.append(clspath)
- except NotConfigured as e:
- if e.args:
- clsname = clspath.split('.')[-1]
- logger.warning("Disabled %(clsname)s: %(eargs)s",
- {'clsname': clsname, 'eargs': e.args[0]},
- extra={'crawler': crawler})
- logger.info("Enabled %(componentname)ss:\n%(enabledlist)s",
- {'componentname': cls.component_name,
- 'enabledlist': pprint.pformat(enabled)},
- extra={'crawler': crawler})
- # 調(diào)用構(gòu)造方法
- return cls(*middlewares)
- @classmethod
- def _get_mwlist_from_settings(cls, settings):
- # 具體有哪些中間件類,子類定義
- raise NotImplementedError
- def __init__(self, *middlewares):
- self.middlewares = middlewares
- # 定義中間件方法
- self.methods = defaultdict(list)
- for mw in middlewares:
- self._add_middleware(mw)
- def _add_middleware(self, mw):
- # 默認(rèn)定義的 子類可覆蓋
- # 如果中間件類有定義open_spider 則加入到methods
- if hasattr(mw, 'open_spider'):
- self.methods['open_spider'].append(mw.open_spider)
- # 如果中間件類有定義close_spider 則加入到methods
- # methods就是一串中間件的方法鏈 后期會(huì)依次調(diào)用
- if hasattr(mw, 'close_spider'):
- self.methods['close_spider'].insert(0, mw.close_spider)
DownloaderMiddlewareManager 實(shí)例化過程:
- class DownloaderMiddlewareManager(MiddlewareManager):
- """下載中間件管理器"""
- component_name = 'downloader middleware'
- @classmethod
- def _get_mwlist_from_settings(cls, settings):
- # 從配置文件DOWNLOADER_MIDDLEWARES_BASE和DOWNLOADER_MIDDLEWARES獲得所有下載器中間件
- return build_component_list(
- settings.getwithbase('DOWNLOADER_MIDDLEWARES'))
- def _add_middleware(self, mw):
- # 定義下載器中間件請(qǐng)求、響應(yīng)、異常一串方法
- if hasattr(mw, 'process_request'):
- self.methods['process_request'].append(mw.process_request)
- if hasattr(mw, 'process_response'):
- self.methods['process_response'].insert(0, mw.process_response)
- if hasattr(mw, 'process_exception'):
- self.methods['process_exception'].insert(0, mw.process_exception)
下載器中間件管理器繼承了 MiddlewareManager 類,然后重寫了 _add_middleware 方法,為下載行為定義默認(rèn)的下載前、下載后、異常時(shí)對(duì)應(yīng)的處理方法。
這里我們可以想一下,中間件這么做的好處是什么?
從這里能大概看出,從某個(gè)組件流向另一個(gè)組件時(shí),會(huì)經(jīng)過一系列中間件,每個(gè)中間件都定義了自己的處理流程,相當(dāng)于一個(gè)個(gè)管道,輸入時(shí)可以針對(duì)數(shù)據(jù)進(jìn)行處理,然后送達(dá)到另一個(gè)組件,另一個(gè)組件處理完邏輯后,又經(jīng)過這一系列中間件,這些中間件可再針對(duì)這個(gè)響應(yīng)結(jié)果進(jìn)行處理,最終輸出。
Scraper
下載器實(shí)例化完了之后,回到引擎的初始化方法中,然后就是實(shí)例化 Scraper,在Scrapy源碼分析(一)架構(gòu)概覽這篇文章中我提到過,這個(gè)類沒有在架構(gòu)圖中出現(xiàn),但這個(gè)類其實(shí)是處于Engine、Spiders、Pipeline 之間,是連通這三個(gè)組件的橋梁。
我們來看一下它的初始化過程:
- class Scraper(object):
- def __init__(self, crawler):
- self.slot = None
- # 實(shí)例化爬蟲中間件管理器
- self.spidermw = SpiderMiddlewareManager.from_crawler(crawler)
- # 從配置文件中加載Pipeline處理器類
- itemproc_cls = load_object(crawler.settings['ITEM_PROCESSOR'])
- # 實(shí)例化Pipeline處理器
- self.itemproc = itemproc_cls.from_crawler(crawler)
- # 從配置文件中獲取同時(shí)處理輸出的任務(wù)個(gè)數(shù)
- self.concurrent_items = crawler.settings.getint('CONCURRENT_ITEMS')
- self.crawler = crawler
- self.signals = crawler.signals
- self.logformatter = crawler.logformatter
Scraper 創(chuàng)建了 SpiderMiddlewareManager,它的初始化過程:
- class SpiderMiddlewareManager(MiddlewareManager):
- """爬蟲中間件管理器"""
- component_name = 'spider middleware'
- @classmethod
- def _get_mwlist_from_settings(cls, settings):
- # 從配置文件中SPIDER_MIDDLEWARES_BASE和SPIDER_MIDDLEWARES獲取默認(rèn)的爬蟲中間件類
- return build_component_list(settings.getwithbase('SPIDER_MIDDLEWARES'))
- def _add_middleware(self, mw):
- super(SpiderMiddlewareManager, self)._add_middleware(mw)
- # 定義爬蟲中間件處理方法
- if hasattr(mw, 'process_spider_input'):
- self.methods['process_spider_input'].append(mw.process_spider_input)
- if hasattr(mw, 'process_spider_output'):
- self.methods['process_spider_output'].insert(0, mw.process_spider_output)
- if hasattr(mw, 'process_spider_exception'):
- self.methods['process_spider_exception'].insert(0, mw.process_spider_exception)
- if hasattr(mw, 'process_start_requests'):
- self.methods['process_start_requests'].insert(0, mw.process_start_requests)
爬蟲中間件管理器初始化與之前的下載器中間件管理器類似,先是從配置文件中加載了默認(rèn)的爬蟲中間件類,然后依次注冊(cè)爬蟲中間件的一系列流程方法。配置文件中定義的默認(rèn)的爬蟲中間件類如下:
- SPIDER_MIDDLEWARES_BASE = {
- # 默認(rèn)的爬蟲中間件類
- 'scrapy.spidermiddlewares.httperror.HttpErrorMiddleware': 50,
- 'scrapy.spidermiddlewares.offsite.OffsiteMiddleware': 500,
- 'scrapy.spidermiddlewares.referer.RefererMiddleware': 700,
- 'scrapy.spidermiddlewares.urllength.UrlLengthMiddleware': 800,
- 'scrapy.spidermiddlewares.depth.DepthMiddleware': 900,
- }
這里解釋一下,這些默認(rèn)的爬蟲中間件的職責(zé):
- HttpErrorMiddleware:針對(duì)非 200 響應(yīng)錯(cuò)誤進(jìn)行邏輯處理;
- OffsiteMiddleware:如果Spider中定義了 allowed_domains,會(huì)自動(dòng)過濾除此之外的域名請(qǐng)求;
- RefererMiddleware:追加 Referer 頭信息;
- UrlLengthMiddleware:過濾 URL 長(zhǎng)度超過限制的請(qǐng)求;
- DepthMiddleware:過濾超過指定深度的抓取請(qǐng)求;
當(dāng)然,在這里你也可以定義自己的爬蟲中間件,來處理自己所需的邏輯。
爬蟲中間件管理器初始化完之后,然后就是 Pipeline 組件的初始化,默認(rèn)的 Pipeline 組件是 ItemPipelineManager:
- class ItemPipelineManager(MiddlewareManager):
- component_name = 'item pipeline'
- @classmethod
- def _get_mwlist_from_settings(cls, settings):
- # 從配置文件加載ITEM_PIPELINES_BASE和ITEM_PIPELINES類
- return build_component_list(settings.getwithbase('ITEM_PIPELINES'))
- def _add_middleware(self, pipe):
- super(ItemPipelineManager, self)._add_middleware(pipe)
- # 定義默認(rèn)的pipeline處理邏輯
- if hasattr(pipe, 'process_item'):
- self.methods['process_item'].append(pipe.process_item)
- def process_item(self, item, spider):
- # 依次調(diào)用所有子類的process_item方法
- return self._process_chain('process_item', item, spider)
我們可以看到 ItemPipelineManager 也是中間件管理器的一個(gè)子類,由于它的行為非常類似于中間件,但由于功能較為獨(dú)立,所以屬于核心組件之一。
從 Scraper 的初始化過程我們可以看出,它管理著 Spiders 和 Pipeline 相關(guān)的數(shù)據(jù)交互。
總結(jié)
好了,這篇文章我們主要剖析了 Scrapy 涉及到的核心的組件,主要包括:引擎、下載器、調(diào)度器、爬蟲類、輸出處理器,以及它們各自都是如何初始化的,在初始化過程中,它們又包含了哪些子模塊來輔助完成這些模塊的功能。
這些組件各司其職,相互協(xié)調(diào),共同完成爬蟲的抓取任務(wù),而且從代碼中我們也能發(fā)現(xiàn),每個(gè)組件類都是定義在配置文件中的,也就是說我們可以實(shí)現(xiàn)自己的邏輯,然后替代這些組件,這樣的設(shè)計(jì)模式也非常值得我們學(xué)習(xí)。