scrapy結(jié)合selenium解析動(dòng)態(tài)頁(yè)面的實(shí)現(xiàn)
1. 問題
雖然scrapy能夠完美且快速的抓取靜態(tài)頁(yè)面,但是在現(xiàn)實(shí)中,目前絕大多數(shù)網(wǎng)站的頁(yè)面都是動(dòng)態(tài)頁(yè)面,動(dòng)態(tài)頁(yè)面中的部分內(nèi)容是瀏覽器運(yùn)行頁(yè)面中的JavaScript腳本動(dòng)態(tài)生成的,爬取相對(duì)困難;
比如你信心滿滿的寫好了一個(gè)爬蟲,寫好了目標(biāo)內(nèi)容的選擇器,一跑起來發(fā)現(xiàn)根本找不到這個(gè)元素,當(dāng)時(shí)肯定一萬個(gè)黑人問號(hào)
于是你在瀏覽器里打開F12,一頓操作,發(fā)現(xiàn)原來這你妹的是ajax加載的,不然就是硬編碼在js代碼里的,blabla的…
然后你得去調(diào)ajax的接口,然后解析json啊,轉(zhuǎn)成python字典啊,然后才能拿到你想要的東西
妹的就不能對(duì)我們這些小爬爬友好一點(diǎn)嗎?
于是大家伙肯定想過,“為啥不能瀏覽器看到是咋樣的html頁(yè)面,我們爬蟲得到的也是同樣的html頁(yè)面呢? 要是可以,那得多么美滋滋啊”
2. 解決方案
既然是想要得到和瀏覽器一模一樣的html頁(yè)面,那我們就先用瀏覽器渲染一波目標(biāo)網(wǎng)頁(yè),然后再將瀏覽器渲染后的html拿給scrapy進(jìn)行進(jìn)一步解析不就好了嗎
2.1 獲取瀏覽器渲染后的html
有了思路,肯定是網(wǎng)上搜一波然后開干啊,搜python操作瀏覽器的庫(kù)啊
貨比三家之后,找到了selenium這貨
selenium可以模擬真實(shí)瀏覽器,自動(dòng)化測(cè)試工具,支持多種瀏覽器,爬蟲中主要用來解決JavaScript渲染問題。
臥槽,這就是我們要的東西啦
先試一波看看效果如何,目標(biāo)網(wǎng)址http://quotes.toscrape.com/js/
別著急,先來看一下網(wǎng)頁(yè)源碼
我們想要的div.quote被硬編碼在js代碼中
用selenium試一下看能不能獲取到瀏覽器渲染后的html
from selenium import webdriver # 控制火狐瀏覽器 browser = webdriver.Firefox() # 訪問我們的目標(biāo)網(wǎng)址 browser.get("http://quotes.toscrape.com/js/") # 獲取渲染后的html頁(yè)面 html = browser.page_source
perfect,到這里我們已經(jīng)順利拿到瀏覽器渲染后的html了,selenium大法好啊?
2.2 通過下載器中間件返回渲染過后html的Response
這里先放一張scrapy的流程圖
所以我們只需要在scrapy下載網(wǎng)頁(yè)(downloader下載好網(wǎng)頁(yè),構(gòu)造Response返回)之前,通過下載器中間件返回我們自己<通過渲染后html構(gòu)造的Response>不就可以了嗎?
道理我都懂,關(guān)鍵是在哪一步使用瀏覽器呢?
分析:
(1)我們的scrapy可能是有很多個(gè)爬蟲的,有些爬蟲處理的是純純的靜態(tài)頁(yè)面,而有些是處理的純純的動(dòng)態(tài)頁(yè)面,又有些是動(dòng)靜態(tài)結(jié)合的頁(yè)面(有可能列表頁(yè)是靜態(tài)的,正文頁(yè)是動(dòng)態(tài)的),如果把<瀏覽器調(diào)用代碼>放在下載器中間件中,那么除非特別區(qū)分哪些爬蟲需要selenium,否則每一個(gè)爬蟲都用selenium去下載解析頁(yè)面的話,實(shí)在是太浪費(fèi)資源了,就相當(dāng)于殺雞用牛刀了,所以得出結(jié)論,<瀏覽器調(diào)用代碼>應(yīng)該是放置于Spider類中更好一點(diǎn);
(2)如果放置于Spider類中,就意味著一個(gè)爬蟲占用一個(gè)瀏覽器的一個(gè)tab頁(yè),如果這個(gè)爬蟲里的某些Request需要selenium,而某些不需要呢? 所以我們還要在區(qū)分一下Request;
結(jié)論:
SeleniumDownloaderMiddleware(selenium專用下載器中間件):負(fù)責(zé)返回瀏覽器渲染后的ResponseSeleniumSpider(selenium專用Spider):一個(gè)spider開一個(gè)瀏覽器SeleniumRequest:只是繼承一下scrapy.Request,然后pass,好區(qū)分哪些Request需要啟用selenium進(jìn)行解析頁(yè)面,相當(dāng)于改個(gè)名
3. 擼代碼,盤他
3.1 自定義Request
#!usr/bin/env python # -*- coding:utf-8 _*- """ @author:Joshua @description: 只是繼承一下scrapy.Request,然后pass,好區(qū)分哪些Request需要啟用selenium進(jìn)行解析頁(yè)面,相當(dāng)于改個(gè)名 """ import scrapy class SeleniumRequest(scrapy.Request): """ selenium專用Request類 """ pass
3.2 自定義Spider
#!usr/bin/env python # -*- coding:utf-8 _*- """ @author:Joshua @description: 一個(gè)spider開一個(gè)瀏覽器 """ import logging import scrapy from selenium import webdriver class SeleniumSpider(scrapy.Spider): """ Selenium專用spider 一個(gè)spider開一個(gè)瀏覽器 瀏覽器驅(qū)動(dòng)下載地址:http://www.cnblogs.com/qiezizi/p/8632058.html """ # 瀏覽器是否設(shè)置無頭模式,僅測(cè)試時(shí)可以為False SetHeadless = True # 是否允許瀏覽器使用cookies EnableBrowserCookies = True def __init__(self, *args, **kwargs): super(SeleniumSpider, self).__init__(*args, **kwargs) # 獲取瀏覽器操控權(quán) self.browser = self._get_browser() def _get_browser(self): """ 返回瀏覽器實(shí)例 """ # 設(shè)置selenium與urllib3的logger的日志等級(jí)為ERROR # 如果不加這一步,運(yùn)行爬蟲過程中將會(huì)產(chǎn)生一大堆無用輸出 logging.getLogger('selenium').setLevel('ERROR') logging.getLogger('urllib3').setLevel('ERROR') # selenium已經(jīng)放棄了PhantomJS,開始支持firefox與chrome的無頭模式 return self._use_firefox() def _use_firefox(self): """ 使用selenium操作火狐瀏覽器 """ profile = webdriver.FirefoxProfile() options = webdriver.FirefoxOptions() # 下面一系列禁用操作是為了減少selenium的資源耗用,加速scrapy # 禁用圖片 profile.set_preference('permissions.default.image', 2) profile.set_preference('browser.migration.version', 9001) # 禁用css profile.set_preference('permissions.default.stylesheet', 2) # 禁用flash profile.set_preference('dom.ipc.plugins.enabled.libflashplayer.so', 'false') # 如果EnableBrowserCookies的值設(shè)為False,那么禁用cookies if hasattr(self, "EnableBrowserCookies") and self.EnableBrowserCookies: # •值1 - 阻止所有第三方cookie。 # •值2 - 阻止所有cookie。 # •值3 - 阻止來自未訪問網(wǎng)站的cookie。 # •值4 - 新的Cookie Jar策略(阻止對(duì)跟蹤器的存儲(chǔ)訪問) profile.set_preference("network.cookie.cookieBehavior", 2) # 默認(rèn)是無頭模式,意思是瀏覽器將會(huì)在后臺(tái)運(yùn)行,也是為了加速scrapy # 我們可不想跑著爬蟲時(shí),旁邊還顯示著瀏覽器訪問的頁(yè)面 # 調(diào)試的時(shí)候可以把SetHeadless設(shè)為False,看一下跑著爬蟲時(shí)候,瀏覽器在干什么 if self.SetHeadless: # 無頭模式,無UI options.add_argument('-headless') # 禁用gpu加速 options.add_argument('--disable-gpu') return webdriver.Firefox(firefox_profile=profile, options=options) def selenium_func(self, request): """ 在返回瀏覽器渲染的html前做一些事情 1.比如等待瀏覽器頁(yè)面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html; 2.比如將頁(yè)面切換進(jìn)iframe中的頁(yè)面; 在需要使用的子類中要重寫該方法,并利用self.browser操作瀏覽器 """ pass def closed(self, reason): # 在爬蟲關(guān)閉后,關(guān)閉瀏覽器的所有tab頁(yè),并關(guān)閉瀏覽器 self.browser.quit() # 日志記錄一下 self.logger.info("selenium已關(guān)閉瀏覽器...")
之所以不把獲取瀏覽器的具體代碼寫在__init__方法里,是因?yàn)楣P者之前寫的代碼里考慮過
- 兩種瀏覽器的調(diào)用(支持firefox與chrome),雖然后來感覺還是firefox比較方便,因?yàn)樗邪姹镜幕鸷鼮g覽器的驅(qū)動(dòng)都是一樣的,但是谷歌瀏覽器是不同版本的瀏覽器必須用不同版本的驅(qū)動(dòng)(坑爹啊- -'')
- 自動(dòng)區(qū)分不同的操作系統(tǒng)并選擇對(duì)應(yīng)操作系統(tǒng)的瀏覽器驅(qū)動(dòng)
額… 所以上面spider的代碼是精簡(jiǎn)過的版本
備注: 針對(duì)selenium做了一系列的優(yōu)化加速,啟用了無頭模式,禁用了css、flash、圖片、gpu加速等… 因?yàn)榕老x嘛,肯定是跑的越快越好啦?
3.3 自定義下載器中間件
#!usr/bin/env python # -*- coding:utf-8 _*- """ @author:Joshua @description: 負(fù)責(zé)返回瀏覽器渲染后的Response """ import hashlib import time from scrapy.http import HtmlResponse from twisted.internet import defer, threads from tender_scrapy.extendsion.selenium.spider import SeleniumSpider from tender_scrapy.extendsion.selenium.requests import SeleniumRequest class SeleniumDownloaderMiddleware(object): """ Selenium下載器中間件 """ def process_request(self, request, spider): # 如果spider為SeleniumSpider的實(shí)例,并且request為SeleniumRequest的實(shí)例 # 那么該Request就認(rèn)定為需要啟用selenium來進(jìn)行渲染html if isinstance(spider, SeleniumSpider) and isinstance(request, SeleniumRequest): # 控制瀏覽器打開目標(biāo)鏈接 browser.get(request.url) # 在構(gòu)造渲染后的HtmlResponse之前,做一些事情 #1.比如等待瀏覽器頁(yè)面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html; #2.比如將頁(yè)面切換進(jìn)iframe中的頁(yè)面; spider.selenium_func(request) # 獲取瀏覽器渲染后的html html = browser.page_source # 構(gòu)造Response # 這個(gè)Response將會(huì)被你的爬蟲進(jìn)一步處理 return HtmlResponse(url=browser.current_url, request=request, body=html.encode(), encoding="utf-8")
這里要說一下下載器中間件的process_request方法,當(dāng)每個(gè)request通過下載中間件時(shí),該方法被調(diào)用。
- process_request() 必須返回其中之一: 返回 None 、返回一個(gè) Response 對(duì)象、返回一個(gè) Request 對(duì)象或raise IgnoreRequest 。
- 如果其返回 Response 對(duì)象,Scrapy將不會(huì)調(diào)用 任何 其他的 process_request() 或 process_exception() 方法,或相應(yīng)地下載函數(shù); 其將返回該response。 已安裝的中間件的 process_response() 方法則會(huì)在每個(gè)response返回時(shí)被調(diào)用。
更詳細(xì)的關(guān)于下載器中間件的資料 -> https://scrapy-chs.readthedocs.io/zh_CN/0.24/topics/downloader-middleware.html#id2
3.4 額外的工具
眼尖的讀者可能注意到SeleniumSpider類里有個(gè)selenium_func方法,并且在SeleniumDownloaderMiddleware的process_request方法返回Resposne之前調(diào)用了spider的selenium_func方法
這樣做的好處是,我們可以在構(gòu)造渲染后的HtmlResponse之前,做一些事情(比如…那種…很騷的那種…你懂的)
- 比如等待瀏覽器頁(yè)面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html;
- 比如將頁(yè)面切換進(jìn)iframe中的頁(yè)面,然后返回iframe里面的html(夠騷嗎);
等待某個(gè)元素出現(xiàn),然后再返回渲染后的html這種操作很常見的,比如你訪問一篇文章,它的正文是ajax加載然后js添加到html里的,ajax是需要時(shí)間的,但是selenium并不會(huì)等待所有請(qǐng)求都完畢后再返回
解決方法:
- 您可以通過browser.implicitly_wait(30),來強(qiáng)制selenium等待30秒(無論元素是否加載出來,都必須等待30秒)
- 可以通過等待,直到某個(gè)元素出現(xiàn),然后再返回html
所以筆者對(duì)<等待某個(gè)元素出現(xiàn)>這一功能做了進(jìn)一步的封裝,代碼如下
#!usr/bin/env python # -*- coding:utf-8 _*- """ @author:Joshua @description: """ import functools from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC def waitFor(browser, select_arg, select_method, timeout=2): """ 阻塞等待某個(gè)元素的出現(xiàn)直到timeout結(jié)束 :param browser:瀏覽器實(shí)例 :param select_method:所使用的選擇器方法 :param select_arg:選擇器參數(shù) :param timeout:超時(shí)時(shí)間 :return: """ element = WebDriverWait(browser, timeout).until( EC.presence_of_element_located((select_method, select_arg)) ) # 用xpath選擇器等待元素 waitForXpath = functools.partial(waitFor, select_method=By.XPATH) # 用css選擇器等待元素 waitForCss = functools.partial(waitFor, select_method=By.CSS_SELECTOR)
waitForXpath與waitForCss 是waitFor函數(shù)的兩個(gè)偏函數(shù),意思這兩個(gè)偏函數(shù)是設(shè)置了select_method參數(shù)默認(rèn)值的waitFor函數(shù),分別應(yīng)用不同的選擇器來定位元素
4. 中間件當(dāng)然要在settings中激活一下
在我們scrapy項(xiàng)目的settings文件中的DOWNLOADER_MIDDLEWARES字典中添加到適當(dāng)?shù)奈恢眉纯?/p>
5. 使用示例
5.1一個(gè)完整的爬蟲示例
# -*- coding: utf-8 -*- """ @author:Joshua @description: 整合selenium的爬蟲示例 """ import scrapy from my_project.requests import SeleniumRequest from my_project.spider import SeleniumSpider from my_project.tools import waitForXpath # 這個(gè)爬蟲類繼承了SeleniumSpider # 在爬蟲跑起來的時(shí)候,將啟動(dòng)一個(gè)瀏覽器 class SeleniumExampleSpider(SeleniumSpider): """ 這一網(wǎng)站,他的列表頁(yè)是靜態(tài)的,但是內(nèi)容頁(yè)是動(dòng)態(tài)的 所以,用selenium試一下,目標(biāo)是扣出內(nèi)容頁(yè)的#content """ name = 'selenium_example' allowed_domains = ['pingdingshan.hngp.gov.cn'] url_format = 'http://pingdingshan.hngp.gov.cn/pingdingshan/ggcx?appCode=H65&channelCode=0301&bz=0&pageSize=20&pageNo={page_num}' def start_requests(self): """ 開始發(fā)起請(qǐng)求,記錄頁(yè)碼 """ start_url = self.url_format.format(page_num=1) meta = dict(page_num=1) # 列表頁(yè)是靜態(tài)的,所以不需要啟用selenium,用普通的scrapy.Request就可以了 yield scrapy.Request(start_url, meta=meta, callback=self.parse) def parse(self, response): """ 從列表頁(yè)解析出正文的url """ meta = response.meta all_li = response.css("div.List2>ul>li") # 列表 for li in all_li: content_href = li.xpath('./a/@href').extract() content_url = response.urljoin(content_href) # 內(nèi)容頁(yè)是動(dòng)態(tài)的,#content是ajax動(dòng)態(tài)加載的,所以啟用一波selenium yield SeleniumRequest(url=content_url, meta=meta, callback=self.parse_content) # 翻頁(yè) meta['page_num'] += 1 next_url = self.url_format.format(page_num=meta['page_num']) # 列表頁(yè)是靜態(tài)的,所以不需要啟用selenium,用普通的scrapy.Request就可以了 yield scrapy.Request(url=next_url, meta=meta, callback=self.parse) def parse_content(self, response): """ 解析正文內(nèi)容 """ content = response.css('#content').extract_first() yield dict(content=content) def selenium_func(self, request): # 這個(gè)方法會(huì)在我們的下載器中間件返回Response之前被調(diào)用 # 等待content內(nèi)容加載成功后,再繼續(xù) # 這樣的話,我們就能在parse_content方法里應(yīng)用選擇器扣出#content了 waitForXpath(self.browser, "http://*[@id='content']/*[1]")
5.2 更騷一點(diǎn)的操作…
假如內(nèi)容頁(yè)的目標(biāo)信息處于iframe中,我們可以將窗口切換進(jìn)目標(biāo)iframe里面,然后返回iframe的html
要實(shí)現(xiàn)這樣的操作,只需要重寫一下SeleniumSpider子類中的selenium_func方法
要注意到SeleniumSpider中的selenium_func其實(shí)是啥也沒做的,一個(gè)pass,所有的功能都在子類中重寫
def selenium_func(self, request): # 找到id為myPanel的iframe target = self.browser.find_element_by_xpath("http://iframe[@id='myPanel']") # 將瀏覽器的窗口切換進(jìn)該iframe中 # 那么切換后的self.browser的page_source將會(huì)是iframe的html self.browser.switch_to.frame(target)
6. selenium的一些替代(一些解決動(dòng)態(tài)頁(yè)面別的方法)
scrapy官方推薦的scrapy_splash
優(yōu)點(diǎn)
- 是異步的
- 可以將部署scrapy的服務(wù)器與部署splash的服務(wù)器分離開
- 留給讀者遐想的空間
本人覺得的缺點(diǎn)
- 喂喂,lua腳本很麻煩好嗎…(大牛請(qǐng)別打我)
最新的異步pyppeteer操控瀏覽器
優(yōu)點(diǎn)
- 調(diào)用瀏覽器是異步的,操控的單位是tab頁(yè),速度更快
- 留給讀者遐想的空間
本人覺得的缺點(diǎn)
- 因?yàn)閜yppeteer是python版puppeteer,所以puppeteer的一些毛病,pyppeteer無可避免的完美繼承
- 筆者試過將pyppeteer整合至scrapy中,在異步中,scrapy跑起來爬蟲,總會(huì)偶爾timeout之類的…
anyway,上面兩個(gè)都是不錯(cuò)的替代,有興趣的讀者可以試一波
7. scrapy整合selenium的一些缺點(diǎn)
- selenium是阻塞的,所以速度會(huì)慢些
- 對(duì)于一些稍微簡(jiǎn)單的動(dòng)態(tài)頁(yè)面,最好還是自己去解析一下接口,不要太過依賴selenium,因?yàn)閟elenium帶來便利的同時(shí),是更多資源的占用
- 整合selenium的scrapy項(xiàng)目不宜大規(guī)模的爬取,比如你在自己的機(jī)子上寫好了一個(gè)一個(gè)的爬蟲,跑起來也沒毛病,速度也能接受,然后你很開心地在服務(wù)器上部署了你項(xiàng)目上的100+個(gè)爬蟲(里面有50%左右的爬蟲啟用了selenium),當(dāng)他們跑起來的時(shí)候,服務(wù)器就原地爆炸了… 為啥? 因?yàn)橄喈?dāng)于服務(wù)器同時(shí)開了50多個(gè)瀏覽器在跑,內(nèi)存頂不住?。ㄍ梁篮雎浴?/li>
到此這篇關(guān)于scrapy結(jié)合selenium解析動(dòng)態(tài)頁(yè)面的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)scrapy selenium解析動(dòng)態(tài)頁(yè)面內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
利用20行Python 代碼實(shí)現(xiàn)加密通信
這篇文章主要介紹了利用Python 代碼實(shí)現(xiàn)加密通信,本文用 20 行 Python 代碼來演示加密、解密、簽名、驗(yàn)證的功能。大家依樣畫葫蘆,不僅能理解加密技術(shù),更能自己實(shí)現(xiàn)一套加密通信機(jī)制,需要的朋友可以參考一下2022-03-03numpy如何取出對(duì)角線元素、計(jì)算對(duì)角線元素和np.diagonal
這篇文章主要介紹了numpy如何取出對(duì)角線元素、計(jì)算對(duì)角線元素和np.diagonal問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06如何解決tensorflow恢復(fù)模型的特定值時(shí)出錯(cuò)
今天小編就為大家分享一篇如何解決tensorflow恢復(fù)模型的特定值時(shí)出錯(cuò),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-02-02Python 實(shí)現(xiàn)Image和Ndarray互相轉(zhuǎn)換
今天小編就為大家分享一篇Python 實(shí)現(xiàn)Image和Ndarray互相轉(zhuǎn)換,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-02-02Python隨機(jī)數(shù)函數(shù)代碼實(shí)例解析
這篇文章主要介紹了Python隨機(jī)數(shù)函數(shù)代碼實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02python聚類算法解決方案(rest接口/mpp數(shù)據(jù)庫(kù)/json數(shù)據(jù)/下載圖片及數(shù)據(jù))
這篇文章主要介紹了python聚類算法解決方案(rest接口/mpp數(shù)據(jù)庫(kù)/json數(shù)據(jù)/下載圖片及數(shù)據(jù)),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-08-08Python+Selenium定位不到元素常見原因及解決辦法(報(bào):NoSuchElementException)
這篇文章主要介紹了Python+Selenium定位不到元素常見原因及解決辦法(報(bào):NoSuchElementException),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03詳解python ThreadPoolExecutor異常捕獲
本文主要介紹了詳解python ThreadPoolExecutor異常捕獲,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01