Scrapy原始碼閱讀分析_3_核心元件
From:https://blog.csdn.net/weixin_37947156/article/details/74481758
這篇這要是關於核心元件,講解這些核心元件初始化都做了哪些工作。包括:引擎、下載器、排程器、爬蟲類、輸出處理器 等的初始化。每個核心元件下其實都包含一些小的元件在裡面,幫助處理某一環節的各種流程。
- 核心元件初始化
- 核心元件互動流程
爬蟲類
接著上次程式碼講,上次的執行入口執行到最後是執行了 Crawler
的 crawl
方法:
@defer.inlineCallbacks
def crawl(self, *args, **kwargs):
assert not self.crawling, "Crawling already taking place"
self.crawling = True
try:
# 到現在,才是例項化一個爬蟲例項
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 = False
if 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 = spidercls
self.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 settings
install_scrapy_root_handler(self.settings)
# lambda is assigned to Crawler attribute because this way it is not
# garbage collected after leaving __init__ scope
self.__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 = False
self.spider = None
self.engine = None
@property
def 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.inlineCallbacks
def crawl(self, *args, **kwargs):
assert not self.crawling, "Crawling already taking place"
self.crawling = True
try:
# 到現在,才是例項化一個爬蟲例項
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 = False
if self.engine is not None:
yield self.engine.close()
if six.PY2:
six.reraise(*exc_info)
raise
def _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.inlineCallbacks
def stop(self):
if self.crawling:
self.crawling = False
yield 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 this
class.
"""
name = None
custom_settings = None # 自定義設定
def __init__(self, name=None, **kwargs):
# spider 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)
# 如果沒有設定 start_urls,預設是[]
if not hasattr(self, 'start_urls'):
self.start_urls = []
@property
def 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 level
This helper wraps a log call to the logger within the spider, but you
can use it directly (e.g. Spider.logger.info('msg')) or use any other
Python logger too.
"""
self.logger.log(level, message, **kw)
@classmethod
def from_crawler(cls, crawler, *args, **kwargs):
spider = cls(*args, **kwargs)
spider._set_crawler(crawler)
return spider
def 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.settings
crawler.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__))
@classmethod
def update_settings(cls, settings):
settings.setdict(cls.custom_settings or {}, priority='spider')
@classmethod
def handles_request(cls, request):
return url_is_from_spider(request.url, cls)
@staticmethod
def 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 pprint
from scrapy.exceptions import NotConfigured
from scrapy.utils.misc import create_instance, load_object
from scrapy.utils.defer import process_parallel, process_chain, process_chain_both
logger = 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)
@classmethod
def _get_mwlist_from_settings(cls, settings):
# 具體有哪些中介軟體類,子類定義
raise NotImplementedError
@classmethod
def 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)
@classmethod
def from_crawler(cls, crawler):
# 呼叫 from_settings
return cls.from_settings(crawler.settings, crawler)
def _add_middleware(self, mw):
# 預設定義的,子類可覆蓋
# 如果中介軟體類有定義open_spider,則加入到methods
if 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 be
tried.
``*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原始碼閱讀分析_2_啟動流程原始碼
- Scrapy原始碼閱讀分析_4_請求處理流程原始碼
- Scrapy原始碼閱讀分析_1_整體框架和流程介紹原始碼框架
- Vuex原始碼閱讀分析Vue原始碼
- snabbdom 原始碼閱讀分析原始碼
- Laravel 原始碼閱讀指南 -- HTTP 核心Laravel原始碼HTTP
- Vue 原始碼閱讀(六)元件化Vue原始碼元件化
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- 為什麼要閱讀核心原始碼?原始碼
- Docker原始碼分析,附閱讀地址Docker原始碼
- Laravel核心解讀–Cookie原始碼分析LaravelCookie原始碼
- Laravel 原始碼閱讀指南 -- Console 核心Laravel原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【核心檔案系統】原始碼閱讀stat.h原始碼
- linux核心原始碼閱讀-塊裝置驅動Linux原始碼
- iOS開發原始碼閱讀篇--FMDB原始碼分析1(FMResultSet)iOS原始碼
- iOS開發原始碼閱讀篇--FMDB原始碼分析2(FMResultSet)iOS原始碼
- ReactorKit原始碼閱讀React原始碼
- AQS原始碼閱讀AQS原始碼
- CountDownLatch原始碼閱讀CountDownLatch原始碼
- HashMap 原始碼閱讀HashMap原始碼
- delta原始碼閱讀原始碼
- 原始碼閱讀-HashMap原始碼HashMap
- NGINX原始碼閱讀Nginx原始碼
- Mux 原始碼閱讀UX原始碼
- HashMap原始碼閱讀HashMap原始碼
- fuzz原始碼閱讀原始碼
- RunLoop 原始碼閱讀OOP原始碼
- express 原始碼閱讀Express原始碼
- muduo原始碼閱讀原始碼
- stack原始碼閱讀原始碼
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- PostgreSQL 原始碼解讀(3)- 如何閱讀原始碼SQL原始碼
- jQuery原始碼閱讀(十)---jQuery靜態方法分析jQuery原始碼
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- 拜讀及分析Element原始碼-alert元件篇原始碼元件
- 拜讀及分析Element原始碼-input元件篇原始碼元件