scrapy中間件的工作原理是通過在請(qǐng)求和響應(yīng)流中插入處理邏輯,實(shí)現(xiàn)對(duì)核心流程的控制與擴(kuò)展。1. 請(qǐng)求流中,request會(huì)依次經(jīng)過下載器中間件的process_request方法,優(yōu)先級(jí)越高越早執(zhí)行;2. 響應(yīng)流中,response會(huì)倒序經(jīng)過之前處理該請(qǐng)求的中間件的process_response方法;3. 異常發(fā)生時(shí),process_exception方法會(huì)被調(diào)用,可進(jìn)行錯(cuò)誤處理或重試;4. 爬蟲中間件作用于爬蟲解析階段,處理輸入輸出及異常。編寫實(shí)用中間件的關(guān)鍵在于理解執(zhí)行順序、正確返回值、避免性能瓶頸,并通過日志和調(diào)試工具排查問題。
在Scrapy框架里,我們談?wù)摗安寮睍r(shí),最核心和常用的實(shí)現(xiàn)方式就是通過編寫各種“中間件”——包括下載器中間件(Downloader Middleware)、爬蟲中間件(Spider Middleware),以及處理數(shù)據(jù)流的Item Pipelines。它們就像是Scrapy工作流程中的一個(gè)個(gè)檢查站或處理站,讓你能在請(qǐng)求發(fā)出前、響應(yīng)接收后、數(shù)據(jù)處理前等關(guān)鍵節(jié)點(diǎn)插入自己的邏輯,進(jìn)行修改、過濾或增強(qiáng)。
解決方案
要開發(fā)一個(gè)Scrapy插件,通常意味著你要介入其核心的請(qǐng)求-響應(yīng)生命周期,或者對(duì)爬取到的數(shù)據(jù)進(jìn)行預(yù)處理。這其中,中間件無疑是最直接也最強(qiáng)大的工具。
一個(gè)基本的下載器中間件看起來是這樣的:
立即學(xué)習(xí)“Python免費(fèi)學(xué)習(xí)筆記(深入)”;
# myproject/middlewares.py from scrapy import signals from scrapy.exceptions import IgnoreRequest import Logging logger = logging.getLogger(__name__) class MyCustomDownloaderMiddleware: # 這個(gè)方法在Scrapy啟動(dòng)時(shí)被調(diào)用,可以用來初始化一些資源 @classmethod def from_crawler(cls, crawler): # 綁定信號(hào),比如在爬蟲關(guān)閉時(shí)做些清理工作 s = cls() crawler.signals.connect(s.spider_opened, signal=signals.spider_opened) crawler.signals.connect(s.spider_closed, signal=signals.spider_closed) return s def spider_opened(self, spider): logger.info(f"Spider {spider.name} opened, my custom middleware is ready!") def spider_closed(self, spider): logger.info(f"Spider {spider.name} closed, my custom middleware is done!") # 處理請(qǐng)求的方法,請(qǐng)求通過這里發(fā)送給下載器 def process_request(self, request, spider): # 你可以在這里修改請(qǐng)求頭,添加代理,或者直接丟棄請(qǐng)求 # 比如,給所有請(qǐng)求添加一個(gè)自定義的User-Agent request.headers['User-Agent'] = 'MyScrapyBot/1.0 (+http://www.example.com)' logger.debug(f"Processing request: {request.url} with custom UA") # 返回None或Request對(duì)象,Scrapy會(huì)繼續(xù)處理 # 如果返回Response對(duì)象,則跳過后續(xù)下載器和下載器中間件,直接交給爬蟲處理 # 如果拋出IgnoreRequest,則該請(qǐng)求被忽略 return None # 處理響應(yīng)的方法,響應(yīng)從下載器返回給爬蟲前會(huì)經(jīng)過這里 def process_response(self, request, response, spider): # 你可以在這里檢查響應(yīng)狀態(tài)碼,修改響應(yīng)體,或者根據(jù)響應(yīng)內(nèi)容生成新的請(qǐng)求 if response.status >= 400: logger.warning(f"Received bad response: {response.status} for {request.url}") # 也許可以根據(jù)情況返回一個(gè)新的Request,重新嘗試 # return request.copy() return response # 必須返回Request、Response或拋出異常 # 處理請(qǐng)求或響應(yīng)過程中發(fā)生異常的方法 def process_exception(self, request, exception, spider): # 當(dāng)下載器或process_request/process_response方法中拋出異常時(shí)被調(diào)用 logger.error(f"Error processing {request.url}: {exception}") # 返回None則異常會(huì)被忽略,Scrapy繼續(xù)處理 # 返回Response則該響應(yīng)被返回給爬蟲 # 返回Request則該請(qǐng)求被重新調(diào)度 pass
然后,在你的settings.py文件中啟用它:
# settings.py DOWNLOADER_MIDDLEWARES = { 'myproject.middlewares.MyCustomDownloaderMiddleware': 543, # 數(shù)字越小,優(yōu)先級(jí)越高 # 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 如果要用自己的UA,可以禁用Scrapy自帶的 } # 爬蟲中間件和Item Pipelines的啟用方式類似 # SPIDER_MIDDLEWARES = { # 'myproject.middlewares.MyCustomSpiderMiddleware': 543, # } # ITEM_PIPELINES = { # 'myproject.pipelines.MyCustomPipeline': 300, # }
這樣,你的自定義邏輯就會(huì)在Scrapy的請(qǐng)求和響應(yīng)流中生效了。中間件的強(qiáng)大之處在于它的可插拔性和對(duì)核心流程的深度介入能力,這讓Scrapy的擴(kuò)展性變得異常靈活。
Scrapy中間件的工作原理是什么?
說實(shí)話,Scrapy的中間件機(jī)制,我個(gè)人覺得是其設(shè)計(jì)中最精妙的部分之一。它巧妙地將復(fù)雜的爬取流程解耦成一系列可獨(dú)立控制的“階段”,每個(gè)階段都能被中間件攔截和處理。想象一下Scrapy的工作流,它其實(shí)是一個(gè)請(qǐng)求(Request)從爬蟲發(fā)出,經(jīng)過下載器中間件處理,然后被下載器執(zhí)行,得到響應(yīng)(Response),響應(yīng)再經(jīng)過下載器中間件處理,最終回到爬蟲進(jìn)行解析的過程。
具體來說:
-
請(qǐng)求流 (Request Flow):當(dāng)爬蟲生成一個(gè)Request對(duì)象時(shí),這個(gè)請(qǐng)求并不會(huì)直接發(fā)送給下載器。它會(huì)先經(jīng)過一系列已啟用的下載器中間件的process_request方法。每個(gè)中間件都有機(jī)會(huì)修改請(qǐng)求、丟棄請(qǐng)求(通過拋出IgnoreRequest),甚至直接返回一個(gè)Response對(duì)象(跳過下載)。這個(gè)過程是按照DOWNLOADER_MIDDLEWARES中定義的優(yōu)先級(jí)(數(shù)字越小,越早被處理)從高到低執(zhí)行的。
-
響應(yīng)流 (Response Flow):下載器獲取到網(wǎng)頁內(nèi)容并生成Response對(duì)象后,這個(gè)響應(yīng)也不是直接交給爬蟲。它會(huì)倒序經(jīng)過之前處理過請(qǐng)求的下載器中間件的process_response方法。這里,你可以檢查響應(yīng)內(nèi)容、狀態(tài)碼,或者根據(jù)響應(yīng)生成新的請(qǐng)求。如果在這個(gè)過程中發(fā)生異常,比如網(wǎng)絡(luò)錯(cuò)誤,process_exception方法就會(huì)被調(diào)用,給你一個(gè)處理錯(cuò)誤、重試請(qǐng)求的機(jī)會(huì)。
-
爬蟲中間件 (Spider Middleware):這個(gè)有點(diǎn)不一樣,它主要作用于爬蟲處理請(qǐng)求和生成Item/新的Request的環(huán)節(jié)。當(dāng)Response到達(dá)爬蟲并被parse方法處理時(shí),process_spider_input會(huì)被調(diào)用。爬蟲生成Item或新的Request后,它們會(huì)通過process_spider_output和process_spider_exception方法。這對(duì)于處理特定爬蟲邏輯、過濾輸出或統(tǒng)一錯(cuò)誤處理非常有用。
理解這個(gè)“雙向”的流程,尤其是優(yōu)先級(jí)對(duì)執(zhí)行順序的影響,是編寫高效中間件的關(guān)鍵。有時(shí),我發(fā)現(xiàn)一個(gè)看似簡(jiǎn)單的功能,如果中間件的順序不對(duì),就會(huì)導(dǎo)致意想不到的結(jié)果,這真的需要花點(diǎn)時(shí)間去琢磨。
如何編寫一個(gè)實(shí)用的Scrapy下載器中間件?
編寫實(shí)用的下載器中間件,往往是為了解決爬蟲在實(shí)際運(yùn)行中遇到的反爬問題,或者為了增強(qiáng)爬蟲的健壯性。一個(gè)非常經(jīng)典的例子就是User-Agent輪換。很多網(wǎng)站會(huì)根據(jù)User-Agent來判斷是否是爬蟲,并采取不同的策略。
# myproject/middlewares.py (續(xù)) import random class UserAgentRotationMiddleware: def __init__(self, user_agents): self.user_agents = user_agents @classmethod def from_crawler(cls, crawler): # 從settings中獲取User-Agent列表 user_agents = crawler.settings.getlist('USER_AGENTS') if not user_agents: # 如果沒有配置,可以使用默認(rèn)的或者拋出錯(cuò)誤 user_agents = [ 'Mozilla/5.0 (windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', 'Mozilla/5.0 (X11; linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', # 更多User-Agent... ] crawler.logger.warning("USER_AGENTS not set in settings, using default list.") return cls(user_agents) def process_request(self, request, spider): # 每次請(qǐng)求時(shí),隨機(jī)選擇一個(gè)User-Agent random_ua = random.choice(self.user_agents) request.headers['User-Agent'] = random_ua spider.logger.debug(f"Set User-Agent to: {random_ua} for {request.url}") return None # 繼續(xù)Scrapy的默認(rèn)處理流程
在settings.py中:
# settings.py DOWNLOADER_MIDDLEWARES = { 'myproject.middlewares.UserAgentRotationMiddleware': 400, # 確保在其他可能修改UA的中間件之前或之后 'scrapy.downloadermiddlewares.useragent.UserAgentMiddleware': None, # 禁用Scrapy自帶的User-Agent中間件 } USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Safari/537.36', 'Mozilla/5.0 (iphone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1', 'Mozilla/5.0 (Linux; Android 10; SM-G973F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.107 Mobile Safari/537.36', ]
這個(gè)中間件通過from_crawler方法從settings.py中讀取User-Agent列表,并在每次請(qǐng)求發(fā)出前,隨機(jī)選擇一個(gè)User-Agent設(shè)置到請(qǐng)求頭中。這在一定程度上能模擬真實(shí)用戶的行為,降低被識(shí)別為爬蟲的風(fēng)險(xiǎn)。當(dāng)然,實(shí)際場(chǎng)景可能更復(fù)雜,比如需要根據(jù)不同的域名使用不同的User-Agent,或者結(jié)合代理IP輪換,但核心思路都是在process_request中對(duì)請(qǐng)求進(jìn)行改造。
Scrapy中間件開發(fā)中常見的坑和調(diào)試技巧?
在開發(fā)Scrapy中間件的過程中,我個(gè)人踩過不少坑,也總結(jié)了一些調(diào)試經(jīng)驗(yàn)。這玩意兒雖然強(qiáng)大,但稍不注意就可能讓整個(gè)爬蟲行為變得詭異。
一個(gè)最常見的“坑”就是返回值問題。process_request、process_response和process_exception這幾個(gè)方法對(duì)返回值類型有嚴(yán)格要求。比如process_request,如果你不返回任何東西(即return None),Scrapy會(huì)繼續(xù)處理該請(qǐng)求。但如果你返回了一個(gè)Response對(duì)象,它就會(huì)跳過后續(xù)的下載器和下載器中間件,直接把這個(gè)Response交給爬蟲。反之,如果你返回了錯(cuò)誤的類型,或者忘記返回,整個(gè)流程就可能中斷或行為異常。我記得有一次,因?yàn)橐粋€(gè)中間件忘記了在特定條件下return request,導(dǎo)致所有請(qǐng)求都卡住了,花了老半天才定位到問題。
另一個(gè)讓人頭疼的是中間件的執(zhí)行順序。DOWNLOADER_MIDDLEWARES和SPIDER_MIDDLEWARES中的數(shù)字優(yōu)先級(jí)決定了它們的執(zhí)行順序。數(shù)字越小,優(yōu)先級(jí)越高,越早被處理。在請(qǐng)求流中,優(yōu)先級(jí)高的中間件先執(zhí)行process_request;在響應(yīng)流中,優(yōu)先級(jí)高的中間件的process_response反而會(huì)最后執(zhí)行。如果你的多個(gè)中間件都修改了同一個(gè)請(qǐng)求頭,或者依賴于前一個(gè)中間件的修改,那么這個(gè)順序就至關(guān)重要。我通常會(huì)把修改請(qǐng)求的放在前面,處理響應(yīng)的放在后面,但具體情況還得看實(shí)際需求。
性能問題也值得注意。中間件會(huì)在每個(gè)請(qǐng)求或響應(yīng)上執(zhí)行,如果你的中間件里有耗時(shí)的操作,比如復(fù)雜的正則匹配、大量的計(jì)算或者I/O操作,那么整個(gè)爬蟲的效率都會(huì)受到影響。所以,盡量讓中間件的邏輯保持簡(jiǎn)潔高效,避免在其中做過于“重”的工作。
至于調(diào)試技巧,最直接有效的就是日志(logging)。在中間件的關(guān)鍵位置,多打一些logger.debug()或logger.info(),輸出請(qǐng)求的URL、響應(yīng)的狀態(tài)碼、修改后的請(qǐng)求頭等信息。結(jié)合settings.py中LOG_LEVEL = ‘DEBUG’,你可以清晰地看到每個(gè)請(qǐng)求和響應(yīng)在中間件中是如何被處理和變化的。
此外,Scrapy Shell也是個(gè)神器。當(dāng)你遇到某個(gè)請(qǐng)求或響應(yīng)行為異常時(shí),可以在Scrapy Shell中手動(dòng)構(gòu)造請(qǐng)求,然后一步步模擬中間件的處理流程,觀察其輸出。這能幫你快速隔離問題,確定是中間件邏輯錯(cuò)誤,還是Scrapy配置問題。
最后,別忘了異常處理。在process_exception中,你可以捕獲并處理下載過程中可能出現(xiàn)的網(wǎng)絡(luò)錯(cuò)誤、dns解析失敗等問題。合理利用它來重試請(qǐng)求或記錄錯(cuò)誤,能大大提升爬蟲的健壯性。一個(gè)健壯的爬蟲,往往是在各種異常情況下都能優(yōu)雅地恢復(fù)或降級(jí)。