Scrapy原始碼閱讀分析_3_核心元件

擒賊先擒王發表於2019-02-19

 

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:從字面意思可以看出,爬蟲自定義配置,會覆蓋配置檔案的配置項;

 

 

引擎

 

分析完爬蟲類的初始化後,還是回到Crawlercrawl方法(scrapy/crawler.py 中 Crawler 類 的 crawl 方法

緊接著就是建立 引擎物件,也就是 _create_engine 方法,這裡直接進行了引擎初始化操作,看看都發生了什麼?

在這裡能看到,進行了核心元件的定義和初始化,包括:SchedulerDownloaderScrapyer,其中 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原始碼分析(一)架構概覽中已經大概說到,這個類沒有在架構圖中出現,但這個類其實是處於 EngineSpidersPipeline 之間,是連通這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 相關的互動邏輯。

 

 

總結

 

到這裡,所有元件:引擎、下載器、排程器、爬蟲類、輸出處理器都依次初始化完成,每個核心元件下其實都包含一些小的元件在裡面,幫助處理某一環節的各種流程。

 

 

 

相關文章