日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

彻底搞懂 Scrapy 的中间件

發布時間:2024/7/23 编程问答 25 豆豆
生活随笔 收集整理的這篇文章主要介紹了 彻底搞懂 Scrapy 的中间件 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

徹底搞懂Scrapy的中間件(一):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_1.html
徹底搞懂Scrapy的中間件(二):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_2.html
徹底搞懂Scrapy的中間件(三):https://www.cnblogs.com/xieqiankun/p/know_middleware_of_scrapy_3.html

在 Scrapy 中捕獲并處理各種異常

[ Scrapy使用技巧 ] 如何在 Scrapy 中捕獲處理各種異常https://blog.csdn.net/sc_lilei/article/details/80702449

重寫 scrapy 中間件之 RetryMiddleware:https://blog.csdn.net/qq_33854211/article/details/78535963

徹底搞懂Scrapy的中間件(一)

中間件是Scrapy里面的一個核心概念。使用中間件可以在爬蟲的請求發起之前或者請求返回之后對數據進行定制化修改,從而開發出適應不同情況的爬蟲。

“中間件”這個中文名字和前面章節講到的“中間人”只有一字之差。它們做的事情確實也非常相似。中間件和中間人都能在中途劫持數據,做一些修改再把數據傳遞出去。不同點在于,中間件是開發者主動加進去的組件,而中間人是被動的,一般是惡意地加進去的環節。中間件主要用來輔助開發,而中間人卻多被用來進行數據的竊取、偽造甚至攻擊。

在Scrapy中有兩種中間件:下載器中間件(Downloader Middleware)和爬蟲中間件(Spider Middleware)。

下載器中間件

Scrapy 的官方文檔中,對下載器中間件的解釋如下。

下載器中間件是介于Scrapy的request/response處理的鉤子框架,是用于全局修改 Scrapy 的 request 和 response 的一個輕量、底層的系統。

這個介紹看起來非常繞口,但其實用容易理解的話表述就是:更換代理IP,更換Cookies,更換User-Agent,自動重試。

如果完全沒有中間件,爬蟲的流程如下圖所示。

使用了中間件以后,爬蟲的流程如下圖所示。

開發代理中間件

Scrapy 設置代理終極寶典:https://zhuanlan.zhihu.com/p/79067223

scrapy 切換代理? ? 針對特定響應狀態碼,使用代理重新請求

HttpProxyMiddleware(HTTP 代理中間件):https://github.com/kohn/HttpProxyMiddleware
scrapy 爬蟲的自動代理中間件:https://github.com/cocoakekeyu/autoproxy

獲取免費代理, 主要抓的是大陸的高匿代理:https://github.com/kohn/HttpProxyMiddleware/blob/master/fetch_free_proxyes.py

scrapy 爬蟲代理 --- 利用 crawlera 神器,無需再尋找代理IP:http://blog.csdn.net/xiao4816/article/details/50650075

在爬蟲開發中,更換代理IP是非常常見的情況,有時候每一次訪問都需要隨機選擇一個代理IP來進行。

中間件本身是一個Python的類,只是爬蟲每次訪問網站之前都要先“經過”這個類,它就能給請求換新的代理IP,這樣就能實現動態改變代理。

創建 scrapy 工程:scrapy startproject <工程名>

創建完 Scrapy 工程以后( 這里創建的工程名為?AdvanceSpider ),工程文件夾下會有一個 middlewares.py 文件,打開以后其內容如下圖所示:

Scrapy 自動生成的這個文件名稱為 middlewares.py,名字后面的 s 表示復數,說明這個文件里面可以放很多個中間件。

middlewares.py 中有 2 個 Python類,每個類都代表一個中間件,代碼如下:

# -*- coding: utf-8 -*-# Define here the models for your spider middleware # # See documentation in: # https://docs.scrapy.org/en/latest/topics/spider-middleware.htmlfrom scrapy import signalsclass AdvancespiderSpiderMiddleware(object):# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the spider middleware does not modify the# passed objects.@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)return sdef process_spider_input(self, response, spider):# Called for each response that goes through the spider# middleware and into the spider.# Should return None or raise an exception.return Nonedef process_spider_output(self, response, result, spider):# Called with the results returned from the Spider, after# it has processed the response.# Must return an iterable of Request, dict or Item objects.for i in result:yield idef process_spider_exception(self, response, exception, spider):# Called when a spider or process_spider_input() method# (from other spider middleware) raises an exception.# Should return either None or an iterable of Request, dict# or Item objects.passdef process_start_requests(self, start_requests, spider):# Called with the start requests of the spider, and works# similarly to the process_spider_output() method, except# that it doesn’t have a response associated.# Must return only requests (not items).for r in start_requests:yield rdef spider_opened(self, spider):spider.logger.info('Spider opened: %s' % spider.name)class AdvancespiderDownloaderMiddleware(object):# Not all methods need to be defined. If a method is not defined,# scrapy acts as if the downloader middleware does not modify the# passed objects.@classmethoddef from_crawler(cls, crawler):# This method is used by Scrapy to create your spiders.s = cls()crawler.signals.connect(s.spider_opened, signal=signals.spider_opened)return sdef process_request(self, request, spider):# Called for each request that goes through the downloader# middleware.# Must either:# - return None: continue processing this request# - or return a Response object# - or return a Request object# - or raise IgnoreRequest: process_exception() methods of# installed downloader middleware will be calledreturn Nonedef process_response(self, request, response, spider):# Called with the response returned from the downloader.# Must either;# - return a Response object# - return a Request object# - or raise IgnoreRequestreturn responsedef process_exception(self, request, exception, spider):# Called when a download handler or a process_request()# (from other downloader middleware) raises an exception.# Must either:# - return None: continue processing this exception# - return a Response object: stops process_exception() chain# - return a Request object: stops process_exception() chainpassdef spider_opened(self, spider):spider.logger.info('Spider opened: %s' % spider.name)

可以看到,自動生成的?middlewares.py 有 2?個中間件?

  • AdvancespiderSpiderMiddleware 代表 爬蟲中間件。(會在?徹底搞懂Scrapy的中間件(三)里面解釋說明。)
  • AdvancespiderDownloaderMiddleware 代表 下載器中間件。?

通過查看 Scrapy 源碼可以發現,總共就 2 種 類型的中間件:爬蟲中間件 和 下載器中間件。

官方中間件文檔說明:https://doc.scrapy.org/en/latest/topics/downloader-middleware.html

在 middlewares.py 中添加下面一段代碼( 可以直接修改?AdvancespiderDownloaderMiddleware?,也可以再單獨寫一個類?,這里是單獨寫一個 Python 類 作為中間件?):

圖示:

示例代碼:

class ProxyMiddleware(object):def __init__(self):self.settings = get_project_settings()def process_request(self, request, spider):proxy = random.choice(self.settings['PROXIES'])request.meta['proxy'] = proxy

要修改請求的代理,就需要在請求的 meta 里面添加一個 Key 為 proxy,Value 為代理 IP 的項。

由于用到了 random 和 settings,所以需要在 middlewares.py 開頭導入它們:

import random from scrapy.utils.project import get_project_settings

在下載器中間件里面有一個名為?process_request()的方法,這個方法中的代碼會在每次爬蟲訪問網頁之前執行。

打開 settings.py,首先添加幾個代理 IP:

PROXIES = ['https://114.217.243.25:8118','https://125.37.175.233:8118','http://1.85.116.218:8118' ]

需要注意的是,代理 IP 是有類型的,需要先看清楚是 HTTP 型的代理 IP 還是 HTTPS 型的代理 IP。

如果用錯了,就會導致無法訪問。

激活中間件

中間件寫好以后,需要去 settings.py 中啟動。在 settings.py 中找到下面這一段被注釋的語句:

# Enable or disable downloader middlewares # See http://scrapy.readthedocs.org/en/latest/topics/downloader-middleware.html #DOWNLOADER_MIDDLEWARES = { # 'AdvanceSpider.middlewares.MyCustomDownloaderMiddleware': 543, #}

解除注釋并修改,從而引用 ProxyMiddleware。修改為:

DOWNLOADER_MIDDLEWARES = {'AdvanceSpider.middlewares.ProxyMiddleware': 543, }

這其實就是一個字典,字典的 Key 就是用點分隔的中間件路徑,后面的數字表示這種中間件的順序。由于中間件是按順序運行的,因此如果遇到后一個中間件依賴前一個中間件的情況,中間件的順序就至關重要。

如何確定后面的數字應該怎么寫呢?最簡單的辦法就是從 543 開始,逐漸加一,這樣一般不會出現什么大問題。如果想把中間件做得更專業一點,那就需要知道 Scrapy 自帶中間件的順序,如圖下圖所示:

數字越小的中間件越先執行,例如 Scrapy 自帶的第1個中間件?RobotsTxtMiddleware,它的作用是首先查看 settings.py 中ROBOTSTXT_OBEY?這一項的配置是?True?還是?False。如果是?True,表示要遵守 Robots.txt 協議,它就會檢查將要訪問的網址能不能被運行訪問,如果不被允許訪問,那么直接就取消這一次請求,接下來的和這次請求有關的各種操作全部都不需要繼續了。

開發者自定義的中間件,會被按順序插入到 Scrapy 自帶的中間件中。爬蟲會按照從 100~900 的順序依次運行所有的中間件。直到所有中間件全部運行完成,或者遇到某一個中間件而取消了這次請求。

Scrapy 其實自帶了 UA 中間件(UserAgentMiddleware)、代理中間件(HttpProxyMiddleware)和 重試中間件(RetryMiddleware)。所以,從 “原則上” 說,要自己開發這3個中間件,需要先禁用 Scrapy 里面自帶的這 3 個中間件。要禁用Scrapy 的中間件,需要在 settings.py 里面將這個中間件的順序設為 None:

DOWNLOADER_MIDDLEWARES = {'AdvanceSpider.middlewares.ProxyMiddleware': 543,'scrapy.contrib.downloadermiddleware.useragent.UserAgentMiddleware': None,'scrapy.contrib.downloadermiddleware.httpproxy.HttpProxyMiddleware': None }

為什么說“原則上”應該禁用呢?先查看 Scrapy 自帶的代理中間件的源代碼,如下圖所示:

從上圖可以看出,如果 Scrapy 發現這個請求已經被設置了代理,那么這個中間件就會什么也不做,直接返回。因此雖然 Scrapy 自帶的這個代理中間件順序為 750,比開發者自定義的代理中間件的順序 543 大,但是它并不會覆蓋開發者自己定義的代理信息,所以即使不禁用系統自帶的這個代理中間件也沒有關系。

完整地激活自定義中間件的 settings.py 的部分內容如下圖所示。

配置好以后運行爬蟲,爬蟲會在每次請求前都隨機設置一個代理。

要測試代理中間件的運行效果,可以使用下面這個練習頁面:http://exercise.kingname.info/exercise_middleware_ip

這個頁面會返回爬蟲的IP地址,直接在網頁上打開,如下圖所示。

這個練習頁支持翻頁功能,在網址后面加上“/頁數”即可翻頁。

例如第100頁的網址為:http://exercise.kingname.info/exercise_middleware_ip/100

使用了代理中間件為每次請求更換代理的運行結果,如下圖所示。

代理中間件的可用代理列表不一定非要寫在 settings.py 里面,也可以將它們寫到 數據庫 或者 Redis 中。一個可行的自動更換代理的爬蟲系統,應該有如下的3個功能。

  • 有一個小爬蟲ProxySpider去各大代理網站爬取免費代理并驗證,將可以使用的代理IP保存到數據庫中。
  • 在 ProxyMiddlerware 的 process_request 中,每次從數據庫里面隨機選擇一條代理IP地址使用。
  • 周期性驗證數據庫中的無效代理,及時將其刪除。由于免費代理極其容易失效,因此如果有一定開發預算的話,建議購買專業代理機構的代理服務,高速而穩定。
  • 開發UA中間件

    開發UA中間件和開發代理中間件幾乎一樣,它也是從 settings.py 配置好的 UA 列表中隨機選擇一項,加入到請求頭中。代碼如下:

    class UAMiddleware(object):def process_request(self, request, spider):ua = random.choice(settings['USER_AGENT_LIST'])request.headers['User-Agent'] = ua

    比IP更好的是,UA不會存在失效的問題,所以只要收集幾十個UA,就可以一直使用。常見的UA如下:

    USER_AGENT_LIST = [ "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36","Dalvik/1.6.0 (Linux; U; Android 4.2.1; 2013022 MIUI/JHACNBL30.0)","Mozilla/5.0 (Linux; U; Android 4.4.2; zh-cn; HUAWEI MT7-TL00 Build/HuaweiMT7-TL00) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1","AndroidDownloadManager","Apache-HttpClient/UNAVAILABLE (java 1.4)","Dalvik/1.6.0 (Linux; U; Android 4.3; SM-N7508V Build/JLS36C)","Android50-AndroidPhone-8000-76-0-Statistics-wifi","Dalvik/1.6.0 (Linux; U; Android 4.4.4; MI 3 MIUI/V7.2.1.0.KXCCNDA)","Dalvik/1.6.0 (Linux; U; Android 4.4.2; Lenovo A3800-d Build/LenovoA3800-d)","Lite 1.0 ( http://litesuits.com )","Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; .NET4.0C; .NET4.0E; .NET CLR 2.0.50727)","Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/38.0.2125.122 Safari/537.36 SE 2.X MetaSr 1.0","Mozilla/5.0 (Linux; U; Android 4.1.1; zh-cn; HTC T528t Build/JRO03H) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30; 360browser(securitypay,securityinstalled); 360(android,uppayplugin); 360 Aphone Browser (2.0.4)", ]

    配置好UA以后,在 settings.py下載器中間件里面激活它,

    使用 UA 練習頁來驗證 UA 是否每一次都不一樣。

    練習頁的地址為:http://exercise.kingname.info/exercise_middleware_ua

    UA練習頁和代理練習頁一樣,也是可以無限制翻頁的。

    示例爬蟲代碼:

    # -*- coding: utf-8 -*- import scrapyclass ExampleSpider(scrapy.Spider):name = 'example'allowed_domains = ['example.com']start_urls = []def __init__(self):super(ExampleSpider, self).__init__()url = 'http://exercise.kingname.info/exercise_middleware_ua'for i in range(5):self.start_urls.append(url)def parse(self, response):print(response.text)passif __name__ == '__main__':from scrapy import cmdlinecmdline.execute('scrapy crawl example'.split())

    運行結果如下圖所示:

    開發 Cookies中間件

    對于需要登錄的網站,可以使用 Cookies 來保持登錄狀態。那么如果單獨寫一個小程序,用 Selenium 持續不斷地用不同的賬號登錄網站,就可以得到很多不同的 Cookies。由于 Cookies 本質上就是一段文本,所以可以把這段文本放在 Redis 里面。這樣一來,當Scrapy 爬蟲請求網頁時,可以從 Redis 中讀取 Cookies 并給爬蟲換上。這樣爬蟲就可以一直保持登錄狀態。

    以下面這個練習頁面為例:http://exercise.kingname.info/exercise_login_success

    如果直接用Scrapy訪問,得到的是登錄界面的源代碼,如下圖所示。

    現在,使用中間件,可以實現完全不改動這個loginSpider.py里面的代碼,就打印出登錄以后才顯示的內容。

    首先開發一個小程序,通過Selenium登錄這個頁面,并將網站返回的Headers保存到Redis中。這個小程序的代碼如下圖所示。

    這段代碼的作用是使用 Selenium 和 ChromeDriver 填寫用戶名和密碼,實現登錄練習頁面,然后將登錄以后的 Cookies 轉換為JSON格式的字符串并保存到Redis中。

    接下來,再寫一個中間件,用來從Redis中讀取Cookies,并把這個 Cookies 給 Scrapy 使用:

    class LoginMiddleware(object):def __init__(self):self.client = redis.StrictRedis()def process_request(self, request, spider):if spider.name == 'loginSpider':cookies = json.loads(self.client.lpop('cookies').decode())request.cookies = cookies

    設置了這個中間件以后,爬蟲里面的代碼不需要做任何修改就可以成功得到登錄以后才能看到的HTML,如圖12-12所示。

    如果有某網站的100個賬號,那么單獨寫一個程序,持續不斷地用Selenium和ChromeDriver或者Selenium 和PhantomJS登錄,獲取Cookies,并將Cookies存放到Redis中。爬蟲每次訪問都從Redis中讀取一個新的Cookies來進行爬取,就大大降低了被網站發現或者封鎖的可能性。

    這種方式不僅適用于登錄,也適用于驗證碼的處理。

    這一篇就講到這里,在下一篇,我們將會介紹如何在下載器中間件中集成Selenium,進行請求重試和處理異常。

    本文節選自我的新書《Python爬蟲開發 從入門到實戰》完整目錄可以在京東查詢到?https://item.jd.com/12436581.html

    徹底搞懂 Scrapy 的中間件(二)

    上面介紹了下載器中間件的一些簡單應用。

    現在再來通過案例說說如何使用下載器中間件 集成 Selenium、重試 和 處理請求異常。

    在中間件中集成 Selenium

    對于一些很麻煩的異步加載頁面,手動尋找它的后臺API代價可能太大。這種情況下可以使用Selenium和ChromeDriver或者Selenium和PhantomJS來實現渲染網頁。

    這是前面的章節已經講到的內容。那么,如何把Scrapy與Selenium結合起來呢?這個時候又要用到中間件了。

    創建一個SeleniumMiddleware,其代碼如下:

    from scrapy.http import HtmlResponse class SeleniumMiddleware(object):def __init__(self):self.driver = webdriver.Chrome('./chromedriver')def process_request(self, request, spider):if spider.name == 'seleniumSpider':self.driver.get(request.url)time.sleep(2)body = self.driver.page_sourcereturn HtmlResponse(self.driver.current_url,body=body,encoding='utf-8',request=request)

    這個中間件的作用,就是對名為“seleniumSpider”的爬蟲請求的網址,使用ChromeDriver先進行渲染,然后用返回的渲染后的HTML代碼構造一個Response對象。如果是其他的爬蟲,就什么都不做。在上面的代碼中,等待頁面渲染完成是通過time.sleep(2)來實現的,當然讀者也可以使用前面章節講到的等待某個元素出現的方法來實現。

    有了這個中間件以后,就可以像訪問普通網頁那樣直接處理需要異步加載的頁面,如下圖所示。

    在中間件里重試

    在爬蟲的運行過程中,可能會因為網絡問題或者是網站反爬蟲機制生效等原因,導致一些請求失敗。在某些情況下,少量的數據丟失是無關緊要的,例如在幾億次請求里面失敗了十幾次,損失微乎其微,沒有必要重試。但還有一些情況,每一條請求都至關重要,容不得有一次失敗。此時就需要使用中間件來進行重試。

    有的網站的反爬蟲機制被觸發了,它會自動將請求重定向到一個xxx/404.html頁面。那么如果發現了這種自動的重定向,就沒有必要讓這一次的請求返回的內容進入數據提取的邏輯,而應該直接丟掉或者重試。

    還有一種情況,某網站的請求參數里面有一項,Key為date,Value為發起請求的這一天的日期或者發起請求的這一天的前一天的日期。例如今天是“2017-08-10”,但是這個參數的值是今天早上10點之前,都必須使用“2017-08-09”,在10點之后才能使用“2017-08-10”,否則,網站就不會返回正確的結果,而是返回“參數錯誤”這4個字。然而,這個日期切換的時間點受到其他參數的影響,有可能第1個請求使用“2017-08-10”可以成功訪問,而第2個請求卻只有使用“2017-08-09”才能訪問。遇到這種情況,與其花費大量的時間和精力去追蹤時間切換點的變化規律,不如簡單粗暴,直接先用今天去試,再用昨天的日期去試,反正最多兩次,總有一個是正確的。

    以上的兩種場景,使用重試中間件都能輕松搞定。

    打開練習頁面

    http://exercise.kingname.info/exercise_middleware_retry.html。

    這個頁面實現了翻頁邏輯,可以上一頁、下一頁地翻頁,也可以直接跳到任意頁數,如下圖所示。

    現在需要獲取1~9頁的內容,那么使用前面章節學到的內容,通過Chrome瀏覽器的開發者工具很容易就能發現翻頁實際上是一個POST請求,提交的參數為“date”,它的值是日期“2017-08-12”,如下圖所示。

    使用Scrapy寫一個爬蟲來獲取1~9頁的內容,運行結果如下圖所示。

    從上圖可以看到,第5頁沒有正常獲取到,返回的結果是參數錯誤。于是在網頁上看一下,發現第5頁的請求中body里面的date對應的日期是“2017-08-11”,如下圖所示。

    如果測試的次數足夠多,時間足夠長,就會發現以下內容。

  • 同一個時間點,不同頁數提交的參數中,date對應的日期可能是今天的也可能是昨天的。
  • 同一個頁數,不同時間提交的參數中,date對應的日期可能是今天的也可能是昨天的。
  • 由于日期不是今天,就是昨天,所以針對這種情況,寫一個重試中間件是最簡單粗暴且有效的解決辦法。中間件的代碼如下圖所示。

    這個中間件只對名為“middlewareSpider”的爬蟲有用。由于middlewareSpider爬蟲默認使用的是“今天”的日期,所以如果被網站返回了“參數錯誤”,那么正確的日期就必然是昨天的了。所以在這個中間件里面,第119行,直接把原來請求的body換成了昨天的日期,這個請求的其他參數不變。讓這個中間件生效以后,爬蟲就能成功爬取第5頁了,如下圖所示。

    爬蟲本身的代碼,數據提取部分完全沒有做任何修改,如果不看中間件代碼,完全感覺不出爬蟲在第5頁重試過。

    除了檢查網站返回的內容外,還可以檢查返回內容對應的網址。將上面練習頁后臺網址的第1個參數“para”改為404,暫時禁用重試中間件,再跑一次爬蟲。其運行結果如下圖所示。

    此時,對于參數不正確的請求,網站會自動重定向到以下網址對應的頁面:

    http://exercise.kingname.info/404.html

    由于Scrapy自帶網址自動去重機制,因此雖然第3頁、第6頁和第7頁都被自動轉到了404頁面,但是爬蟲只會爬一次404頁面,剩下兩個404頁面會被自動過濾。

    對于這種情況,在重試中間件里面判斷返回的網址即可解決,如下圖12-21所示。

    在代碼的第115行,判斷是否被自動跳轉到了404頁面,或者是否被返回了“參數錯誤”。如果都不是,說明這一次請求目前看起來正常,直接把response返回,交給后面的中間件來處理。如果被重定向到了404頁面,或者被返回“參數錯誤”,那么進入重試的邏輯。如果返回了“參數錯誤”,那么進入第126行,直接替換原來請求的body即可重新發起請求。

    如果自動跳轉到了404頁面,那么這里有一點需要特別注意:此時的請求,request這個對象對應的是向404頁面發起的GET請求,而不是原來的向練習頁后臺發起的請求。所以,重新構造新的請求時必須把URL、body、請求方式、Headers全部都換一遍才可以。

    由于request對應的是向404頁面發起的請求,所以resquest.url對應的網址是404頁面的網址。因此,如果想知道調整之前的URL,可以使用如下的代碼:

    request.meta['redirect_urls']

    這個值對應的是一個列表。請求自動跳轉了幾次,這個列表里面就有幾個URL。這些URL是按照跳轉的先后次序依次append進列表的。由于本例中只跳轉了一次,所以直接讀取下標為0的元素即可,也就是原始網址。

    重新激活這個重試中間件,不改變爬蟲數據抓取部分的代碼,直接運行以后可以正確得到1~9頁的全部內容,如下圖所示。

    在中間件里處理異常

    在默認情況下,一次請求失敗了,Scrapy會立刻原地重試,再失敗再重試,如此3次。如果3次都失敗了,就放棄這個請求。這種重試邏輯存在一些缺陷。以代理IP為例,代理存在不穩定性,特別是免費的代理,差不多10個里面只有3個能用。而現在市面上有一些收費代理IP提供商,購買他們的服務以后,會直接提供一個固定的網址。把這個網址設為Scrapy的代理,就能實現每分鐘自動以不同的IP訪問網站。如果其中一個IP出現了故障,那么需要等一分鐘以后才會更換新的IP。在這種場景下,Scrapy自帶的重試邏輯就會導致3次重試都失敗。

    這種場景下,如果能立刻更換代理就立刻更換;如果不能立刻更換代理,比較好的處理方法是延遲重試。而使用Scrapy_redis就能實現這一點。爬蟲的請求來自于Redis,請求失敗以后的URL又放回Redis的末尾。一旦一個請求原地重試3次還是失敗,那么就把它放到Redis的末尾,這樣Scrapy需要把Redis列表前面的請求都消費以后才會重試之前的失敗請求。這就為更換IP帶來了足夠的時間。

    重新打開代理中間件,這一次故意設置一個有問題的代理,于是可以看到Scrapy控制臺打印出了報錯信息,如下圖所示。

    從上圖可以看到Scrapy自動重試的過程。由于代理有問題,最后會拋出方框框住的異常,表示TCP超時。在中間件里面如果捕獲到了這個異常,就可以提前更換代理,或者進行重試。這里以更換代理為例。首先根據上圖中方框框住的內容導入TCPTimeOutError這個異常:

    from twisted.internet.error import TCPTimedOutError

    修改前面開發的重試中間件,添加一個process_exception()方法。這個方法接收3個參數,分別為request、exception和spider,如下圖所示。

    process_exception() 方法只對名為 “exceptionSpider” 的爬蟲生效,如果請求遇到了 TCPTimeOutError,那么就首先調用remove_broken_proxy() 方法把失效的這個代理IP移除,然后返回這個請求對象request。返回以后,Scrapy會重新調度這個請求,就像它第一次調度一樣。由于原來的ProxyMiddleware依然在工作,于是它就會再一次給這個請求更換代理IP。又由于剛才已經移除了失效的代理IP,所以ProxyMiddleware會從剩下的代理IP里面隨機找一個來給這個請求換上。

    特別提醒:圖片中的remove_broken_proxy() 函數體里面寫的是 pass,但是在實際開發過程中,讀者可根據實際情況實現這個方法,寫出移除失效代理的具體邏輯。

    下載器中間件功能總結

    能在中間件中實現的功能,都能通過直接把代碼寫到爬蟲中實現。使用中間件的好處在于,它可以把數據爬取和其他操作分開。在爬蟲的代碼里面專心寫數據爬取的代碼;在中間件里面專心寫突破反爬蟲、登錄、重試和渲染AJAX等操作。

    對團隊來說,這種寫法能實現多人同時開發,提高開發效率;對個人來說,寫爬蟲的時候不用考慮反爬蟲、登錄、驗證碼和異步加載等操作。另外,寫中間件的時候不用考慮數據怎樣提取。一段時間只做一件事,思路更清晰。

    徹底搞懂 Scrapy的中間件(三)

    在前面兩篇文章介紹了下載器中間件的使用,這篇文章將會介紹爬蟲中間件(Spider Middleware)的使用。

    爬蟲中間件

    爬蟲中間件的用法與下載器中間件非常相似,只是它們的作用對象不同。下載器中間件的作用對象是請求request和返回response;爬蟲中間件的作用對象是爬蟲,更具體地來說,就是寫在spiders文件夾下面的各個文件。它們的關系,在Scrapy的數據流圖上可以很好地區分開來,如下圖所示。

    其中,4、5 表示下載器中間件,6、7 表示爬蟲中間件。

    爬蟲中間件 會在以下幾種情況被調用。

  • 當運行到?yield scrapy.Request()或者?yield item?的時候,爬蟲中間件的?process_spider_output()方法被調用。
  • 當爬蟲本身的代碼出現了?Exception?的時候,爬蟲中間件的?process_spider_exception()方法被調用。
  • 當爬蟲里面的某一個回調函數?parse_xxx()被調用之前,爬蟲中間件的?process_spider_input()方法被調用。
  • 當運行到?start_requests()的時候,爬蟲中間件的?process_start_requests()方法被調用。
  • 在中間件處理爬蟲本身的異常

    在爬蟲中間件里面可以處理爬蟲本身的異常。例如編寫一個爬蟲,爬取UA練習頁面http://exercise.kingname.info/exercise_middleware_ua?,故意在爬蟲中制造一個異常,如圖12-26所示。

    由于網站返回的只是一段普通的字符串,并不是 JSON 格式的字符串,因此使用 JSON去 解析,就一定會導致報錯。這種報錯和下載器中間件里面遇到的報錯不一樣。下載器中間件里面的報錯一般是由于外部原因引起的,和代碼層面無關。而現在的這種報錯是由于代碼本身的問題導致的,是代碼寫得不夠周全引起的。

    為了解決這個問題,除了仔細檢查代碼、考慮各種情況外,還可以通過開發爬蟲中間件來跳過或者處理這種報錯。在middlewares.py 中編寫一個類:

    class ExceptionCheckSpider(object):def process_spider_exception(self, response, exception, spider):print(f'返回的內容是:{response.body.decode()}\n報錯原因:{type(exception)}')return None

    這個類僅僅起到記錄 Log 的作用。在使用 JSON 解析網站返回內容出錯的時候,將網站返回的內容打印出來。

    process_spider_exception()?這個方法,它可以返回?None,也可以運行?yield item?語句或者像爬蟲的代碼一樣,使用?yield scrapy.Request()?發起新的請求。如果運行了?yield item?或者?yield scrapy.Request(),程序就會繞過爬蟲里面原有的代碼。

    例如,對于有異常的請求,不需要進行重試,但是需要記錄是哪一個請求出現了異常,此時就可以在爬蟲中間件里面檢測異常,然后生成一個只包含標記的 item。還是以抓取?http://exercise.kingname.info/exercise_middleware_retry.html?這個練習頁的內容為例,但是這一次不進行重試,只記錄哪一頁出現了問題。先看爬蟲的代碼,這一次在 meta 中把頁數帶上,如下圖所示。

    爬蟲里面如果發現了參數錯誤,就使用 raise 這個關鍵字人工拋出一個自定義的異常。在實際爬蟲開發中,讀者也可以在某些地方故意不使用 try ... except 捕獲異常,而是讓異常直接拋出。例如 XPath 匹配處理的結果,直接讀里面的值,不用先判斷列表是否為空。這樣如果列表為空,就會被拋出一個 IndexError,于是就能讓爬蟲的流程進入到爬蟲中間件的process_spider_exception()?中。

    在 items.py 里面創建了一個 ErrorItem 來記錄哪一頁出現了問題,如下圖所示。

    接下來,在爬蟲中間件中將出錯的頁面和當前時間存放到ErrorItem里面,并提交給pipeline,保存到MongoDB中,如下圖所示。

    這樣就實現了記錄錯誤頁數的功能,方便在后面對錯誤原因進行分析。由于這里會把 item 提交給 pipeline,所以不要忘記在settings.py 里面打開 pipeline,并配置好 MongoDB。儲存錯誤頁數到 MongoDB 的代碼如下圖所示。

    激活 爬蟲中間件

    爬蟲中間件的激活方式與下載器中間件非常相似,在 settings.py 中,在下載器中間件配置項的上面就是爬蟲中間件的配置項,它默認也是被注釋了的,解除注釋,并把自定義的爬蟲中間件添加進去即可,如下圖所示。

    Scrapy也有幾個自帶的爬蟲中間件,它們的名字和順序如下圖所示。

    下載器中間件的數字越小越接近 Scrapy 引擎,數字越大越接近爬蟲。如果不能確定自己的自定義中間件應該靠近哪個方向,那么就在 500~700 之間選擇最為妥當。

    爬蟲中間件 輸入/輸出

    在爬蟲中間件里面還有兩個不太常用的方法,

    分別為process_spider_input(response, spider)和process_spider_output(response, result, spider)。

    • process_spider_input(response, spider)在下載器中間件處理完成后,馬上要進入某個回調函數parse_xxx()前調用。
    • process_spider_output(response, result, output)是在爬蟲運行?yield item?或者?yield scrapy.Request()?的時候調用。在這個方法處理完成以后,數據如果是 item,就會被交給 pipeline;如果是請求,就會被交給調度器,然后下載器中間件才會開始運行。所以在這個方法里面可以進一步對 item 或者 請求 做一些修改。這個方法的參數 result 就是爬蟲爬出來的 item 或者scrapy.Request()。由于 yield 得到的是一個生成器,生成器是可以迭代的,所以 result 也是可以迭代的,可以使用 for 循環來把它展開。

    item 進行處理

    def process_spider_output(response, result, spider):for item in result:if isinstance(item, scrapy.Item):# 這里可以對即將被提交給pipeline的item進行各種操作print(f'item將會被提交給pipeline')yield item

    或者對 請求 進行監控和修改:

    def process_spider_output(response, result, spider):for request in result:if not isinstance(request, scrapy.Item):# 這里可以對請求進行各種修改print('現在還可以對請求對象進行修改。。。。')request.meta['request_start_time'] = time.time()yield request

    總結

    以上是生活随笔為你收集整理的彻底搞懂 Scrapy 的中间件的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。