Scrapy源码阅读分析_3_核心组件
?
From:https://blog.csdn.net/weixin_37947156/article/details/74481758
?
這篇這要是關于核心組件,講解這些核心組件初始化都做了哪些工作。包括:引擎、下載器、調度器、爬蟲類、輸出處理器 等的初始化。每個核心組件下其實都包含一些小的組件在里面,幫助處理某一環節的各種流程。
- 核心組件初始化
- 核心組件交互流程
?
?
爬蟲類
?
接著上次代碼講,上次的運行入口執行到最后是執行了?Crawler?的?crawl?方法:
@defer.inlineCallbacksdef crawl(self, *args, **kwargs):assert not self.crawling, "Crawling already taking place"self.crawling = Truetry:# 到現在,才是實例化一個爬蟲實例self.spider = self._create_spider(*args, **kwargs)# 創建引擎self.engine = self._create_engine()# 調用爬蟲類的start_requests方法start_requests = iter(self.spider.start_requests())# 執行引擎的open_spider,并傳入爬蟲實例和初始請求yield self.engine.open_spider(self.spider, start_requests)yield defer.maybeDeferred(self.engine.start)except Exception:# In Python 2 reraising an exception after yield discards# the original traceback (see https://bugs.python.org/issue7563),# so sys.exc_info() workaround is used.# This workaround also works in Python 3, but it is not needed,# and it is slower, so in Python 3 we use native `raise`.if six.PY2:exc_info = sys.exc_info()self.crawling = Falseif self.engine is not None:yield self.engine.close()if six.PY2:six.reraise(*exc_info)raise在這里,就交由 scrapy 的 引擎 來處理了。
依次來看,爬蟲類是如何實例化的?上文已講解過,在 Crawler 實例化時,會創建 SpiderLoader,它會根據用戶的配置文件settings.py 找到存放爬蟲的位置,我們寫的爬蟲都會放在這里。
然后 SpiderLoader 會掃描這里的所有文件,并找到?父類是 scrapy.Spider 爬蟲類,然后根據爬蟲類中的 name 屬性(在編寫爬蟲時,這個屬性是必填的),最后生成一個 {spider_name: spider_cls} 的 字典,然后根據 scrapy crawl <spider_name> 命令,根據 spider_name 找到對應的爬蟲類,然后實例化它,在這里就是調用了?_create_spider?方法:
class Crawler(object):def __init__(self, spidercls, settings=None):if isinstance(settings, dict) or settings is None:settings = Settings(settings)self.spidercls = spiderclsself.settings = settings.copy()self.spidercls.update_settings(self.settings)d = dict(overridden_settings(self.settings))logger.info("Overridden settings: %(settings)r", {'settings': d})self.signals = SignalManager(self)self.stats = load_object(self.settings['STATS_CLASS'])(self)handler = LogCounterHandler(self, level=self.settings.get('LOG_LEVEL'))logging.root.addHandler(handler)if get_scrapy_root_handler() is not None:# scrapy root handler already installed: update it with new settingsinstall_scrapy_root_handler(self.settings)# lambda is assigned to Crawler attribute because this way it is not# garbage collected after leaving __init__ scopeself.__remove_handler = lambda: logging.root.removeHandler(handler)self.signals.connect(self.__remove_handler, signals.engine_stopped)lf_cls = load_object(self.settings['LOG_FORMATTER'])self.logformatter = lf_cls.from_crawler(self)self.extensions = ExtensionManager.from_crawler(self)self.settings.freeze()self.crawling = Falseself.spider = Noneself.engine = None@propertydef spiders(self):if not hasattr(self, '_spiders'):warnings.warn("Crawler.spiders is deprecated, use ""CrawlerRunner.spider_loader or instantiate ""scrapy.spiderloader.SpiderLoader with your ""settings.",category=ScrapyDeprecationWarning, stacklevel=2)self._spiders = _get_spider_loader(self.settings.frozencopy())return self._spiders@defer.inlineCallbacksdef crawl(self, *args, **kwargs):assert not self.crawling, "Crawling already taking place"self.crawling = Truetry:# 到現在,才是實例化一個爬蟲實例self.spider = self._create_spider(*args, **kwargs)# 創建引擎self.engine = self._create_engine()# 調用爬蟲類的start_requests方法start_requests = iter(self.spider.start_requests())# 執行引擎的open_spider,并傳入爬蟲實例和初始請求yield self.engine.open_spider(self.spider, start_requests)yield defer.maybeDeferred(self.engine.start)except Exception:# In Python 2 reraising an exception after yield discards# the original traceback (see https://bugs.python.org/issue7563),# so sys.exc_info() workaround is used.# This workaround also works in Python 3, but it is not needed,# and it is slower, so in Python 3 we use native `raise`.if six.PY2:exc_info = sys.exc_info()self.crawling = Falseif self.engine is not None:yield self.engine.close()if six.PY2:six.reraise(*exc_info)raisedef _create_spider(self, *args, **kwargs):# 調用類方法from_crawler實例化return self.spidercls.from_crawler(self, *args, **kwargs)def _create_engine(self):return ExecutionEngine(self, lambda _: self.stop())@defer.inlineCallbacksdef stop(self):if self.crawling:self.crawling = Falseyield defer.maybeDeferred(self.engine.stop)實例化爬蟲比較有意思,它不是通過普通的構造方法進行初始化,而是調用了類方法?from_crawler?進行的初始化,找到scrapy.Spider?類:(scrapy/spiders/__init__.py)
class Spider(object_ref):"""Base class for scrapy spiders. All spiders must inherit from thisclass."""name = Nonecustom_settings = None # 自定義設置 def __init__(self, name=None, **kwargs):# spider name 必填if name is not None:self.name = nameelif not getattr(self, 'name', None):raise ValueError("%s must have a name" % type(self).__name__)self.__dict__.update(kwargs)# 如果沒有設置 start_urls,默認是[]if not hasattr(self, 'start_urls'):self.start_urls = []@propertydef logger(self):logger = logging.getLogger(self.name)return logging.LoggerAdapter(logger, {'spider': self})def log(self, message, level=logging.DEBUG, **kw):"""Log the given message at the given log levelThis helper wraps a log call to the logger within the spider, but youcan use it directly (e.g. Spider.logger.info('msg')) or use any otherPython logger too."""self.logger.log(level, message, **kw)@classmethoddef from_crawler(cls, crawler, *args, **kwargs):spider = cls(*args, **kwargs)spider._set_crawler(crawler)return spiderdef set_crawler(self, crawler):warnings.warn("set_crawler is deprecated, instantiate and bound the ""spider to this crawler with from_crawler method ""instead.",category=ScrapyDeprecationWarning, stacklevel=2)assert not hasattr(self, 'crawler'), "Spider already bounded to a " \"crawler"self._set_crawler(crawler)def _set_crawler(self, crawler):self.crawler = crawler# 把settings對象賦給spider實例self.settings = crawler.settingscrawler.signals.connect(self.close, signals.spider_closed)def start_requests(self):cls = self.__class__if method_is_overridden(cls, Spider, 'make_requests_from_url'):warnings.warn("Spider.make_requests_from_url method is deprecated; it ""won't be called in future Scrapy releases. Please ""override Spider.start_requests method instead (see %s.%s)." % (cls.__module__, cls.__name__),)for url in self.start_urls:yield self.make_requests_from_url(url)else:for url in self.start_urls:yield Request(url, dont_filter=True)def make_requests_from_url(self, url):""" This method is deprecated. """return Request(url, dont_filter=True)def parse(self, response):raise NotImplementedError('{}.parse callback is not defined'.format(self.__class__.__name__))@classmethoddef update_settings(cls, settings):settings.setdict(cls.custom_settings or {}, priority='spider')@classmethoddef handles_request(cls, request):return url_is_from_spider(request.url, cls)@staticmethoddef close(spider, reason):closed = getattr(spider, 'closed', None)if callable(closed):return closed(reason)def __str__(self):return "<%s %r at 0x%0x>" % (type(self).__name__, self.name, id(self))__repr__ = __str__在這里可以看到,這個類方法其實也是調用了構造方法,進行實例化,同時也拿到了?settings?配置,
再看構造方法干了些什么?就是我們平時編寫爬蟲類時,最常用的幾個屬性:name、start_urls、custom_settings。
- name:在運行爬蟲時通過它找到對應的爬蟲腳本而使用;
- start_urls:定義種子URL;
- custom_settings:從字面意思可以看出,爬蟲自定義配置,會覆蓋配置文件的配置項;
?
?
引擎
?
分析完爬蟲類的初始化后,還是回到Crawler的crawl方法(scrapy/crawler.py 中 Crawler 類 的 crawl 方法)
緊接著就是創建?引擎對象,也就是?_create_engine?方法,這里直接進行了引擎初始化操作,看看都發生了什么?
在這里能看到,進行了核心組件的定義和初始化,包括:Scheduler、Downloader、Scrapyer,其中?Scheduler?只進行了類定義,沒有實例化。
?
?
調度器
?
調度器初始化發生在引擎的?open_spider?方法中,
我們提前來看一下 調度器 的 初始化 完成了哪些工作?
調度器的初始化主要做了2件事:
- 實例化請求指紋過濾器:用來過濾重復請求,可自己重寫替換之;
- 定義各種不同類型的任務隊列:優先級任務隊列、基于磁盤的任務隊列、基于內存的任務隊列;
?
?
請求指紋過濾器
?
先來看請求指紋過濾器是什么?在配置文件中定義的默認指紋過濾器是 RFPDupeFilter:
DUPEFILTER_CLASS = 'scrapy.dupefilters.RFPDupeFilter'
請求指紋過濾器初始化時定義了指紋集合,這個集合使用內存實現的 set,而且可以控制這些指紋是否存入磁盤供下次重復使用。
指紋過濾器的主要職責是:過濾重復請求,可自定義過濾規則。
在下篇文章中會介紹到,每個請求是根據什么規則生成指紋,進而實現重復請求過濾邏輯的。
?
?
任務隊列
?
調度器默認定義的2種隊列類型:
- 基于磁盤的任務隊列:在配置文件可配置存儲路徑,每次執行后會把隊列任務保存到磁盤上;
- 基于內存的任務隊列:每次都在內存中執行,下次啟動則消失;
配置文件默認定義如下:
如果用戶在配置文件中定義了 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/queuelib?模塊。
?
?
下載器
?
回頭繼續看引擎的初始化,來看下載器是如何初始化的。
在默認的配置文件?default_settings.py?中,下載器配置如下:
DOWNLOADER = 'scrapy.core.downloader.Downloader'Downloader?實例化:
這個過程主要是初始化了?下載處理器、下載器中間件管理器?以及從配置文件中拿到抓取請求控制相關參數。
下載器?DownloadHandlers?是做什么的?
下載器中間件?DownloaderMiddlewareManager?初始化發生了什么?
?
?
下載處理器
?
下載處理器在默認的配置文件中是這樣配置的:
看到這里你應該能明白了,說白了就是需下載的資源是什么類型,就選用哪一種下載處理器進行網絡下載,其中最常用的就是http 和 https 對應的處理器。
從這里你也能看出,scrapy的架構是非常低耦合的,任何涉及到的組件及模塊都是可重寫和配置的。scrapy提供了基礎的服務組件,你也可以自己實現其中的某些組件,修改配置即可達到替換的目的。
到這里,大概就能明白,下載處理器的工作就是:管理著各種資源對應的下載器,在真正發起網絡請求時,選取對應的下載器進行資源下載。
但是請注意,在這個初始化過程中,這些下載器是沒有被實例化的,也就是說,在真正發起網絡請求時,才會進行初始化,而且只會初始化一次,后面會講到。
?
?
下載器中間件管理器
?
下面來看下載器中間件?DownloaderMiddlewareManager?初始化,同樣的這里又調用了類方法?from_crawler?進行初始化,DownloaderMiddlewareManager?繼承了?MiddlewareManager?類,來看它在初始化做了哪些工作:
(scrapy/core/downloader/middleware.py)
from collections import defaultdict, deque import logging import pprintfrom scrapy.exceptions import NotConfigured from scrapy.utils.misc import create_instance, load_object from scrapy.utils.defer import process_parallel, process_chain, process_chain_bothlogger = logging.getLogger(__name__)class MiddlewareManager(object):"""所有中間件的父類,提供中間件公共的方法"""component_name = 'foo middleware'def __init__(self, *middlewares):self.middlewares = middlewares# 定義中間件方法self.methods = defaultdict(deque)for mw in middlewares:self._add_middleware(mw)@classmethoddef _get_mwlist_from_settings(cls, settings):# 具體有哪些中間件類,子類定義raise NotImplementedError@classmethoddef from_settings(cls, settings, crawler=None):# 調用子類_get_mwlist_from_settings得到所有中間件類的模塊mwlist = cls._get_mwlist_from_settings(settings)middlewares = []enabled = []# 依次實例化for clspath in mwlist:try:# 加載這些中間件模塊mwcls = load_object(clspath)mw = create_instance(mwcls, settings, crawler)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})# 調用構造方法return cls(*middlewares)@classmethoddef from_crawler(cls, crawler):# 調用 from_settingsreturn cls.from_settings(crawler.settings, crawler)def _add_middleware(self, mw):# 默認定義的,子類可覆蓋# 如果中間件類有定義open_spider,則加入到methodsif hasattr(mw, 'open_spider'):self.methods['open_spider'].append(mw.open_spider)# 如果中間件類有定義close_spider,則加入到methods# methods就是一串中間件的方法鏈,后期會依次調用if hasattr(mw, 'close_spider'):self.methods['close_spider'].appendleft(mw.close_spider)def _process_parallel(self, methodname, obj, *args):return process_parallel(self.methods[methodname], obj, *args)def _process_chain(self, methodname, obj, *args):return process_chain(self.methods[methodname], obj, *args)def _process_chain_both(self, cb_methodname, eb_methodname, obj, *args):return process_chain_both(self.methods[cb_methodname], \self.methods[eb_methodname], obj, *args)def open_spider(self, spider):return self._process_parallel('open_spider', spider)def close_spider(self, spider):return self._process_parallel('close_spider', spider)create_instance 函數:
def create_instance(objcls, settings, crawler, *args, **kwargs):"""Construct a class instance using its ``from_crawler`` or``from_settings`` constructors, if available.At least one of ``settings`` and ``crawler`` needs to be different from``None``. If ``settings `` is ``None``, ``crawler.settings`` will be used.If ``crawler`` is ``None``, only the ``from_settings`` constructor will betried.``*args`` and ``**kwargs`` are forwarded to the constructors.Raises ``ValueError`` if both ``settings`` and ``crawler`` are ``None``."""if settings is None:if crawler is None:raise ValueError("Specifiy at least one of settings and crawler.")settings = crawler.settings# 如果此中間件類定義了from_crawler,則調用此方法實例化if crawler and hasattr(objcls, 'from_crawler'):return objcls.from_crawler(crawler, *args, **kwargs)# 如果此中間件類定義了from_settings,則調用此方法實例化elif hasattr(objcls, 'from_settings'):return objcls.from_settings(settings, *args, **kwargs)else:# 上面2個方法都沒有,則直接調用構造實例化return objcls(*args, **kwargs)DownloaderMiddlewareManager?實例化:
下載器中間件管理器?繼承了 MiddlewareManager 類,然后重寫了?_add_middleware?方法,為下載行為定義默認的?下載前、下載后、異常時?對應的處理方法。?
中間件的職責是什么?從這里能大概看出,從某個組件流向另一個組件時,會經過一系列中間件,每個中間件都定義了自己的處理流程,相當于一個個管道,輸入時可以針對數據進行處理,然后送達到另一個組件,另一個組件處理完邏輯后,又經過這一系列中間件,這些中間件可再針對這個響應結果進行處理,最終輸出。
?
?
Scraper
?
下載器實例化完了之后,回到引擎的初始化方法中,然后是實例化?Scraper,在Scrapy源碼分析(一)架構概覽中已經大概說到,這個類沒有在架構圖中出現,但這個類其實是處于?Engine、Spiders、Pipeline?之間,是連通這3個組件的橋梁。
來看它的初始化:(scrapy/core/scraper.py)
?
?
爬蟲中間件管理器
?
SpiderMiddlewareManager?初始化:
爬蟲中間件管理器初始化與之前的下載器中間件管理器類似,先是從配置文件中加載了默認的爬蟲中間件類,然后依次注冊爬蟲中間件的一系列流程方法。
配置文件中定義的默認的爬蟲中間件類如下:
這些默認的爬蟲中間件職責分別如下:
- HttpErrorMiddleware:會針對響應不是 200 錯誤進行邏輯處理;
- OffsiteMiddleware:如果Spider中定義了 allowed_domains,會自動過濾除此之外的域名請求;
- RefererMiddleware:追加 Referer 頭信息;
- UrlLengthMiddleware:控制過濾URL長度超過配置的請求;
- DepthMiddleware:過濾超過配置深入的抓取請求;
當然,你也可以定義自己的爬蟲中間件,來處理自己需要的邏輯。
?
?
Pipeline管理器
?
爬蟲中間件管理器初始化完之后,然后就是?Pipeline?組件的初始化,默認的?Pipeline?組件是?ItemPipelineManager:
可以看到?ItemPipelineManager?也是一個中間件管理器的子類,由于它的行為非常類似于中間件,但由于功能較為獨立,所以屬于核心組件之一。
從?Scraper?的初始化能夠看到,它管理著?Spiders?和?Pipeline?相關的交互邏輯。
?
?
總結
?
到這里,所有組件:引擎、下載器、調度器、爬蟲類、輸出處理器都依次初始化完成,每個核心組件下其實都包含一些小的組件在里面,幫助處理某一環節的各種流程。
?
?
?
總結
以上是生活随笔為你收集整理的Scrapy源码阅读分析_3_核心组件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ARM 汇编语言入门
- 下一篇: Scrapy-Item Loaders(