Scrapy原始碼閱讀分析_4_請求處理流程
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原始碼分析(三)核心元件初始化),然後呼叫了spider
的start_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
方法,構建好種子請求物件後,呼叫了engine
的open_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 方法,也就是說,你可以定義多個自己的爬蟲中介軟體,每個類都重寫此方法,爬蟲在排程之前會分別呼叫你定義好的爬蟲中介軟體,來分別處理初始化請求,你可以進行過濾、加工、篩選以及你想做的任何邏輯。這樣做的好處就是,把想做的邏輯拆分成做箇中介軟體,功能獨立而且維護起來更加清晰。
排程器
接著呼叫了Scheduler
的open
:(scrapy/core/scheduler.py)
在open
方法中,例項化出優先順序佇列以及根據dqdir
決定是否使用磁碟佇列,然後呼叫了請求指紋過濾器的open
,在父類BaseDupeFilter
中定義:
請求過濾器提供了請求過濾的具體實現方式,Scrapy預設提供了RFPDupeFilter
過濾器實現過濾重複請求的邏輯,後面講具體是如何過濾重複請求的。
Scraper
再來看Scraper
的open_spider
:
這裡的工作主要是Scraper
呼叫所有Pipeline
的open_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.request
的request_fingerprint
:
這個過濾器先是通過 Request 物件 生成一個請求指紋,在這裡使用 sha1 演算法,並記錄到指紋集合,每次請求入隊前先到這裡驗證一下指紋集合,如果已存在,則認為請求重複,則不會重複入佇列。
不過如果我想不校驗重複,也想重複爬取怎麼辦?看 enqueue_request 的第一行判斷,僅需將 Request 例項的 dont_filter 定義為 True 就可以重複爬取此請求,非常靈活。
Scrapy 就是通過此邏輯實現重複請求的過濾邏輯,預設重複請求是不會進行重複抓取的。
下載請求
第一次請求進來後,肯定是不重複的,那麼則會正常進入排程器佇列。然後再進行下一次排程,再次呼叫_next_request_from_scheduler 方法,此時呼叫排程器的 next_request 方法,就是從排程器佇列中取出一個請求,這次就要開始進行網路下載了,也就是呼叫 _download:(scrapy/core/engine.py)
在進行網路下載時,呼叫了Downloader
的fetch
:(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)
下載前,先通過解析request
的scheme
來獲取對應的下載處理器,預設配置檔案中定義的下載處理器:
然後呼叫 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 模組進行互動。
處理下載結果
請求入隊邏輯不用再說,前面已經講過。現在主要看Scraper
的enqueue_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整體給我的感覺是,雖然它提供的只是單機版的爬蟲框架,但我們可以通過編寫更多的外掛和替換某些元件,來定製化自己的爬蟲,從而來實現更強大的功能,例如分散式、代理排程、併發控制、視覺化、監控等等功能,都是非常方便的!
相關文章
- Scrapy原始碼閱讀分析_2_啟動流程原始碼
- TiDB 原始碼閱讀系列文章(二十三)Prepare/Execute 請求處理TiDB原始碼
- 原始碼分析Retrofit請求流程原始碼
- axios原始碼分析——請求流程iOS原始碼
- SpringMVC請求流程原始碼分析SpringMVC原始碼
- Scrapy原始碼閱讀分析_1_整體框架和流程介紹原始碼框架
- ThinkPHP6 原始碼分析之請求處理PHP原始碼
- Scrapy原始碼閱讀分析_3_核心元件原始碼元件
- OkHttp 原始碼分析(一)—— 請求流程HTTP原始碼
- Kafka原始碼分析(四) - Server端-請求處理框架Kafka原始碼Server框架
- springmvc原始碼 ---DispatcherServlet 處理請求SpringMVC原始碼Servlet
- CesiumJS 2022^ 原始碼解讀[7] - 3DTiles 的請求、載入處理流程解析JS原始碼3D
- Spring MVC 處理一個請求的流程分析SpringMVC
- SpringMVC原始碼分析:POST請求中的檔案處理SpringMVC原始碼
- scrapy-redis原始碼解讀之傳送POST請求Redis原始碼
- Spring MVC原始碼(二) ----- DispatcherServlet 請求處理流程 面試必問SpringMVC原始碼Servlet面試
- ThinkPHP6 原始碼分析之請求流程PHP原始碼
- 如何實現一個HTTP請求庫——axios原始碼閱讀與分析HTTPiOS原始碼
- tomcat原始碼分析(第四篇 tomcat請求處理原理解析--Container原始碼分析)Tomcat原始碼AI
- Redis(一):服務啟動及基礎請求處理流程原始碼解析Redis原始碼
- 深入OKHttp原始碼分析(一)----同步和非同步請求流程和原始碼分析HTTP原始碼非同步
- SpringMVC請求處理過程原始碼簡析SpringMVC原始碼
- zookeeper原始碼 — 五、處理寫請求過程原始碼
- DRF之請求執行流程和APIView原始碼分析APIView原始碼
- snabbdom 原始碼閱讀分析原始碼
- Vuex原始碼閱讀分析Vue原始碼
- Nginx請求處理流程你瞭解嗎?Nginx
- 【原始碼分析】- 在SpringBoot中你會使用REST風格處理請求嗎?原始碼Spring BootREST
- 直播帶貨原始碼,非同步處理中會處理兩次請求原始碼非同步
- Laravel 請求類原始碼分析Laravel原始碼
- axios原始碼分析——取消請求iOS原始碼
- Okhttp同步請求原始碼分析HTTP原始碼
- 死磕Spring原始碼-MVC處理HTTP分發請求Spring原始碼MVCHTTP
- scrapy處理post請求的傳參和日誌等級
- yai 請求預處理指令碼AI指令碼
- Spring MVC框架處理Web請求的基本流程SpringMVC框架Web
- Tomcat 第四篇:請求處理流程(上)Tomcat
- SpringMVC底層——請求引數處理流程描述SpringMVC