Scrapy原始碼閱讀分析_4_請求處理流程

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

 

From:https://blog.csdn.net/weixin_37947156/article/details/74533108

 

 

執行入口

 

還是回到最初的入口,在Scrapy原始碼分析(二)執行入口這篇文章中已經講解到,在執行scrapy命令時,呼叫流程如下:

  • 呼叫cmdline.py的execute方法
  • 呼叫命令例項解析命令列
  • 構建CrawlerProcess例項,呼叫crawl和start方法

而 crawl 方法最終是呼叫了 Cralwer 例項的 crawl,這個方法最終把控制權交由 Engine,而 start 方法 註冊好協程池,開始非同步排程。

我們來看 Cralwer 的 crawl 方法:(scrapy/crawler.py

在把控制權交給引擎排程之前,先建立出爬蟲例項,然後建立引擎例項(此過程見Scrapy原始碼分析(三)核心元件初始化),然後呼叫了spiderstart_requests方法,這個方法就是我們平時寫的最多爬蟲類的父類,它在spiders/__init__.py中:

 

 

構建請求

 

在這裡我們能看到,平時我們必須要定義的start_urls,原來是在這裡拿來構建Request的,來看Request的是如何構建的:

(scrapy/http/request/__init__.py)

"""
This module implements the Request class which is used to represent HTTP
requests in Scrapy.

See documentation in docs/topics/request-response.rst
"""
import six
from w3lib.url import safe_url_string

from scrapy.http.headers import Headers
from scrapy.utils.python import to_bytes
from scrapy.utils.trackref import object_ref
from scrapy.utils.url import escape_ajax
from scrapy.http.common import obsolete_setter


class Request(object_ref):

    def __init__(self, url, callback=None, method='GET', headers=None, body=None,
                 cookies=None, meta=None, encoding='utf-8', priority=0,
                 dont_filter=False, errback=None, flags=None):

        # 編碼
        self._encoding = encoding  # this one has to be set first
        # 請求方法
        self.method = str(method).upper()
        # 設定 URL
        self._set_url(url)
        #  設定 body
        self._set_body(body)
        assert isinstance(priority, int), "Request priority not an integer: %r" % priority
        # 優先順序
        self.priority = priority
        if callback is not None and not callable(callback):
            raise TypeError('callback must be a callable, got %s' % type(callback).__name__)
        if errback is not None and not callable(errback):
            raise TypeError('errback must be a callable, got %s' % type(errback).__name__)
        assert callback or not errback, "Cannot use errback without a callback"
        # 回撥函式
        self.callback = callback
        # 異常回撥函式
        self.errback = errback
        # cookies
        self.cookies = cookies or {}
        # 構建Header
        self.headers = Headers(headers or {}, encoding=encoding)
        # 是否需要過濾
        self.dont_filter = dont_filter
        # 附加資訊
        self._meta = dict(meta) if meta else None
        self.flags = [] if flags is None else list(flags)

    @property
    def meta(self):
        if self._meta is None:
            self._meta = {}
        return self._meta

    def _get_url(self):
        return self._url

    def _set_url(self, url):
        if not isinstance(url, six.string_types):
            raise TypeError('Request url must be str or unicode, got %s:' % type(url).__name__)

        s = safe_url_string(url, self.encoding)
        self._url = escape_ajax(s)

        if ':' not in self._url:
            raise ValueError('Missing scheme in request url: %s' % self._url)

    url = property(_get_url, obsolete_setter(_set_url, 'url'))

    def _get_body(self):
        return self._body

    def _set_body(self, body):
        if body is None:
            self._body = b''
        else:
            self._body = to_bytes(body, self.encoding)

    body = property(_get_body, obsolete_setter(_set_body, 'body'))

    @property
    def encoding(self):
        return self._encoding

    def __str__(self):
        return "<%s %s>" % (self.method, self.url)

    __repr__ = __str__

    def copy(self):
        """Return a copy of this Request"""
        return self.replace()

    def replace(self, *args, **kwargs):
        """Create a new Request with the same attributes except for those
        given new values.
        """
        for x in ['url', 'method', 'headers', 'body', 'cookies', 'meta', 'flags',
                  'encoding', 'priority', 'dont_filter', 'callback', 'errback']:
            kwargs.setdefault(x, getattr(self, x))
        cls = kwargs.pop('cls', self.__class__)
        return cls(*args, **kwargs)

Request物件比較簡單,就是簡單封裝了請求引數、方式、回撥以及可附加的屬性資訊。

當然,你也可以在子類重寫start_requests以及make_requests_from_url這2個方法,來構建種子請求。

 

 

引擎排程

 

回到crawl方法,構建好種子請求物件後,呼叫了engineopen_spider方法:

初始化的過程之前的文章已講到,這裡不再多說。主要說一下處理流程,這裡第一步是構建了CallLaterOnce,把_next_request註冊進去,看此類的實現:

class CallLaterOnce(object):
    """Schedule a function to be called in the next reactor loop, but only if
    it hasn't been already scheduled since the last time it ran.
    """
    # 在twisted的reactor中迴圈排程一個方法

    def __init__(self, func, *a, **kw):
        self._func = func
        self._a = a
        self._kw = kw
        self._call = None

    def schedule(self, delay=0):
        # 上次發起排程,才可再次繼續排程
        if self._call is None:
            self._call = reactor.callLater(delay, self)

    def cancel(self):
        if self._call:
            self._call.cancel()

    def __call__(self):
        # 上面註冊的是self,所以會執行__call__
        self._call = None
        return self._func(*self._a, **self._kw)

這裡封裝了迴圈執行的方法類,並且註冊的方法會在 twisted 的 reactor 中非同步執行,以後執行只需呼叫 schedule 方法,就會註冊 self 到 reactor 的 callLater 中,然後它會執行 __call__ 方法,進而執行我們註冊的方法。而這裡我們註冊的方法是引擎的_next_request,也就是說,此方法會迴圈排程,直到程式退出。

然後呼叫了爬蟲中介軟體的 process_start_requests 方法,也就是說,你可以定義多個自己的爬蟲中介軟體,每個類都重寫此方法,爬蟲在排程之前會分別呼叫你定義好的爬蟲中介軟體,來分別處理初始化請求,你可以進行過濾、加工、篩選以及你想做的任何邏輯。這樣做的好處就是,把想做的邏輯拆分成做箇中介軟體,功能獨立而且維護起來更加清晰。

 

 

排程器

 

接著呼叫了Scheduleropen:(scrapy/core/scheduler.py)

open方法中,例項化出優先順序佇列以及根據dqdir決定是否使用磁碟佇列,然後呼叫了請求指紋過濾器open,在父類BaseDupeFilter中定義:

請求過濾器提供了請求過濾的具體實現方式,Scrapy預設提供了RFPDupeFilter過濾器實現過濾重複請求的邏輯,後面講具體是如何過濾重複請求的。

 

 

Scraper

 

再來看Scraperopen_spider

這裡的工作主要是Scraper呼叫所有Pipelineopen_spider方法,也就是說,如果我們定義了多個Pipeline輸出類,可重寫open_spider完成每個Pipeline處理輸出開始的初始化工作。

 

 

迴圈排程

 

呼叫了一些列的元件的open方法後,最後呼叫了nextcall.schedule()開始排程,

也就是迴圈執行在上面註冊的 _next_request方法:

_next_request 方法首先呼叫 _needs_backout 方法檢查是否需要等待,等待的條件有:

  • 引擎是否主動關閉
  • Slot是否關閉
  • 下載器網路下載超過預設引數
  • Scraper處理輸出超過預設引數

如果不需要等待,則呼叫 _next_request_from_scheduler,此方法從名字上就能看出,主要是從 Schduler中 獲取 Request。

這裡要注意,在第一次呼叫此方法時,Scheduler 中是沒有放入任何 Request 的,這裡會直接 break 出來,執行下面的邏輯,而下面就會呼叫 crawl 方法,實際是把請求放到 Scheduler 的請求佇列,放入佇列的過程會經過 請求過濾器 校驗是否重複。

下次再呼叫 _next_request_from_scheduler 時,就能從 Scheduler 中獲取到下載請求,然後執行下載動作。

先來看第一次排程,執行 crawl:

呼叫引擎的crawl實際就是把請求放入Scheduler的佇列中,下面看請求是如何入佇列的。

 

 

請求入隊

 

Scheduler 請求入隊方法:(scrapy/core/schedulter.py)

在之前將核心元件例項化時有說到,排程器主要定義了2種佇列:基於磁碟佇列、基於記憶體佇列。

如果在例項化Scheduler時候傳入jobdir,則使用磁碟佇列,否則使用記憶體佇列,預設使用記憶體佇列。

 

 

指紋過濾

 

在入隊之前,首先通過請求指紋過濾器檢查請求是否重複,也就是呼叫了過濾器的request_seen

(scrapy/duperfilters.py)

utils.requestrequest_fingerprint

這個過濾器先是通過 Request 物件 生成一個請求指紋,在這裡使用 sha1 演算法,並記錄到指紋集合,每次請求入隊前先到這裡驗證一下指紋集合,如果已存在,則認為請求重複,則不會重複入佇列。

不過如果我想不校驗重複,也想重複爬取怎麼辦?看 enqueue_request 的第一行判斷,僅需將 Request 例項的 dont_filter 定義為 True 就可以重複爬取此請求,非常靈活。

Scrapy 就是通過此邏輯實現重複請求的過濾邏輯,預設重複請求是不會進行重複抓取的。

 

 

下載請求

 

第一次請求進來後,肯定是不重複的,那麼則會正常進入排程器佇列。然後再進行下一次排程,再次呼叫_next_request_from_scheduler 方法,此時呼叫排程器的 next_request 方法,就是從排程器佇列中取出一個請求,這次就要開始進行網路下載了,也就是呼叫 _download(scrapy/core/engine.py)

在進行網路下載時,呼叫了Downloaderfetch(scrapy/core/downloader/__init__.py)

這裡呼叫下載器中介軟體的download方法 (scrapy/core/downloader/middleware.py),並註冊下載成功的回撥方法是_enqueue_request,來看下載方法:

在下載過程中,首先先找到所有定義好的下載器中介軟體,包括內建定義好的,也可以自己擴充套件下載器中介軟體,下載前先依次執行process_request 方法,可對 request 進行加工、處理、校驗等操作,然後發起真正的網路下載,也就是第一個引數download_func,在這裡是 Downloader _enqueue_request 方法:

下載成功後回撥 Downloader _enqueue_request(scrapy/core/downloader/__init__.py)

在這裡,也維護了一個下載佇列,可根據配置達到延遲下載的要求。真正發起下載請求的是呼叫了self.handlers.download_request(scrapy/core/downloader/handlers/__init__.py)

下載前,先通過解析requestscheme來獲取對應的下載處理器,預設配置檔案中定義的下載處理器:

然後呼叫 download_request 方法,完成網路下載,這裡不再詳細講解每個處理器的實現,簡單來說你就把它想象成封裝好的網路下載庫,輸入URL,輸出下載結果就好了,這樣方便理解。

在下載過程中,如果發生異常情況,則會依次呼叫下載器中介軟體的process_exception方法,每個中介軟體只需定義自己的異常處理邏輯即可。

如果下載成功,則會依次執行下載器中介軟體的 process_response 方法,每個中介軟體可以進一步處理下載後的結果,最終返回。

這裡值得提一下,除了process_request 方法是每個中介軟體順序執行的,而 process_response 和 process_exception 方法是每個中介軟體倒序執行的,具體可看一下 DownaloderMiddlewareManager 的 _add_middleware 方法,可明白是如何註冊這個方法鏈的。

拿到最終的下載結果後,再回到 ExecuteEngine 的 _next_request_from_scheduler 方法,會看到呼叫了_handle_downloader_output 方法,也就是處理下載結果的邏輯:

拿到下載結果後,主要分2個邏輯,如果是 Request 例項,則直接再次放入 Scheduler 請求佇列。如果是 Response 或 Failure 例項,則呼叫 Scraper 的 enqueue_scrape 方法,進行進一步處理。

Scrapyer 主要是與 Spider 模組和 Pipeline 模組進行互動。

 

 

處理下載結果

 

請求入隊邏輯不用再說,前面已經講過。現在主要看Scraperenqueue_scrape,看Scraper元件是如何處理後續邏輯的:

(scrapy/core/scraper.py)

首先加入到Scraper的處理佇列中,然後從佇列中獲取到任務,如果不是異常結果,則呼叫 爬蟲中介軟體管理器 的 scrape_response 方法:

有沒有感覺套路很熟悉?與上面下載器中介軟體呼叫方式非常相似,也呼叫一系列的前置方法,再執行真正的處理邏輯,然後執行一些列的後置方法。

 

 

回撥爬蟲

 

在這裡真正的處理邏輯是call_spider,也就是回撥我們寫的爬蟲類:(scrapy/core/scraper.py)

看到這裡,你應該更熟悉,平時我們寫的最多的爬蟲模組的parse則是第一個回撥方法,後續爬蟲模組拿到下載結果,可定義下載後的callback就是在這裡進行回撥執行的。

 

 

處理輸出

 

在與爬蟲模組互動完成之後,Scraper呼叫了handle_spider_output方法處理輸出結果:

我們編寫爬蟲類時,寫的那些回撥方法處理邏輯,也就是在這裡被回撥執行,執行完自定義的解析邏輯後,解析方法可返回新的 Request 或 BaseItem 例項,如果是新的請求,則再次通過 Scheduler 進入請求佇列,如果是 BaseItem 例項,則呼叫 Pipeline 管理器,依次執行 process_item,也就是我們想輸出結果時,只定義 Pepeline 類,然後重寫這個方法就可以了。

ItemPipeManager 處理邏輯:

可以看到ItemPipeManager也是一箇中介軟體,和之前下載器中介軟體管理器和爬蟲中介軟體管理器類似,如果子類有定義process_item,則依次執行它。

執行完後,呼叫_itemproc_finished

這裡可以看到,如果想在 Pipeline中 丟棄某個結果,直接丟擲 DropItem 異常即可,Scrapy 會進行對應的處理。

到這裡,抓取結果根據自定義的輸出類輸出到指定位置,而新的 Request 則會再次進入請求佇列,等待引擎下一次排程,也就是再次呼叫 ExecutionEngine 的 _next_request 方法,直至請求佇列沒有新的任務,整個程式退出。

 

 

CrawlerSpider

 

這裡也簡單說一下 CrawlerSpider 類,它其實就繼承了 Spider 類,然後重寫了 parse 方法(這也是整合此類不要重寫此方法的原因),並結合Rule等規則類,來完成Request的自動提取邏輯。

由此也可看出,Scrapy 的每個模組的實現都非常純粹,每個元件都通過配置檔案定義連線起來,如果想要擴充套件或替換,只需定義並實現自己的處理邏輯即可,其他模組均不受任何影響,這也導致編寫一個外掛是變得多麼容易!

 

 

總結

 

總結一下整個執行流程,還是用這兩張圖表示再清楚不過:

 

Scrapy整體給我的感覺是,雖然它提供的只是單機版的爬蟲框架,但我們可以通過編寫更多的外掛和替換某些元件,來定製化自己的爬蟲,從而來實現更強大的功能,例如分散式、代理排程、併發控制、視覺化、監控等等功能,都是非常方便的!

 

 

 

相關文章