scrapy結(jié)合selenium解析動(dòng)態(tài)頁面的實(shí)現(xiàn)
1. 問題
雖然scrapy能夠完美且快速的抓取靜態(tài)頁面,但是在現(xiàn)實(shí)中,目前絕大多數(shù)網(wǎng)站的頁面都是動(dòng)態(tài)頁面,動(dòng)態(tài)頁面中的部分內(nèi)容是瀏覽器運(yùn)行頁面中的JavaScript腳本動(dòng)態(tài)生成的,爬取相對困難;
比如你信心滿滿的寫好了一個(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字典啊,然后才能拿到你想要的東西
妹的就不能對我們這些小爬爬友好一點(diǎn)嗎?
于是大家伙肯定想過,“為啥不能瀏覽器看到是咋樣的html頁面,我們爬蟲得到的也是同樣的html頁面呢? 要是可以,那得多么美滋滋啊”

2. 解決方案
既然是想要得到和瀏覽器一模一樣的html頁面,那我們就先用瀏覽器渲染一波目標(biāo)網(wǎng)頁,然后再將瀏覽器渲染后的html拿給scrapy進(jìn)行進(jìn)一步解析不就好了嗎

2.1 獲取瀏覽器渲染后的html
有了思路,肯定是網(wǎng)上搜一波然后開干啊,搜python操作瀏覽器的庫啊
貨比三家之后,找到了selenium這貨
selenium可以模擬真實(shí)瀏覽器,自動(dòng)化測試工具,支持多種瀏覽器,爬蟲中主要用來解決JavaScript渲染問題。
臥槽,這就是我們要的東西啦
先試一波看看效果如何,目標(biāo)網(wǎng)址http://quotes.toscrape.com/js/

別著急,先來看一下網(wǎng)頁源碼

我們想要的div.quote被硬編碼在js代碼中
用selenium試一下看能不能獲取到瀏覽器渲染后的html

from selenium import webdriver
# 控制火狐瀏覽器
browser = webdriver.Firefox()
# 訪問我們的目標(biāo)網(wǎng)址
browser.get("http://quotes.toscrape.com/js/")
# 獲取渲染后的html頁面
html = browser.page_source
perfect,到這里我們已經(jīng)順利拿到瀏覽器渲染后的html了,selenium大法好啊?
2.2 通過下載器中間件返回渲染過后html的Response
這里先放一張scrapy的流程圖

所以我們只需要在scrapy下載網(wǎng)頁(downloader下載好網(wǎng)頁,構(gòu)造Response返回)之前,通過下載器中間件返回我們自己<通過渲染后html構(gòu)造的Response>不就可以了嗎?

道理我都懂,關(guān)鍵是在哪一步使用瀏覽器呢?
分析:
(1)我們的scrapy可能是有很多個(gè)爬蟲的,有些爬蟲處理的是純純的靜態(tài)頁面,而有些是處理的純純的動(dòng)態(tài)頁面,又有些是動(dòng)靜態(tài)結(jié)合的頁面(有可能列表頁是靜態(tài)的,正文頁是動(dòng)態(tài)的),如果把<瀏覽器調(diào)用代碼>放在下載器中間件中,那么除非特別區(qū)分哪些爬蟲需要selenium,否則每一個(gè)爬蟲都用selenium去下載解析頁面的話,實(shí)在是太浪費(fèi)資源了,就相當(dāng)于殺雞用牛刀了,所以得出結(jié)論,<瀏覽器調(diào)用代碼>應(yīng)該是放置于Spider類中更好一點(diǎn);
(2)如果放置于Spider類中,就意味著一個(gè)爬蟲占用一個(gè)瀏覽器的一個(gè)tab頁,如果這個(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)行解析頁面,相當(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)行解析頁面,相當(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è)置無頭模式,僅測試時(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策略(阻止對跟蹤器的存儲(chǔ)訪問)
profile.set_preference("network.cookie.cookieBehavior", 2)
# 默認(rèn)是無頭模式,意思是瀏覽器將會(huì)在后臺(tái)運(yùn)行,也是為了加速scrapy
# 我們可不想跑著爬蟲時(shí),旁邊還顯示著瀏覽器訪問的頁面
# 調(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.比如等待瀏覽器頁面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html;
2.比如將頁面切換進(jìn)iframe中的頁面;
在需要使用的子類中要重寫該方法,并利用self.browser操作瀏覽器
"""
pass
def closed(self, reason):
# 在爬蟲關(guān)閉后,關(guān)閉瀏覽器的所有tab頁,并關(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)并選擇對應(yīng)操作系統(tǒng)的瀏覽器驅(qū)動(dòng)
額… 所以上面spider的代碼是精簡過的版本
備注: 針對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.比如等待瀏覽器頁面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html;
#2.比如將頁面切換進(jìn)iframe中的頁面;
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 對象、返回一個(gè) Request 對象或raise IgnoreRequest 。
- 如果其返回 Response 對象,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之前,做一些事情(比如…那種…很騷的那種…你懂的)
- 比如等待瀏覽器頁面中的某個(gè)元素出現(xiàn)后,再返回渲染后的html;
- 比如將頁面切換進(jìn)iframe中的頁面,然后返回iframe里面的html(夠騷嗎);
等待某個(gè)元素出現(xiàn),然后再返回渲染后的html這種操作很常見的,比如你訪問一篇文章,它的正文是ajax加載然后js添加到html里的,ajax是需要時(shí)間的,但是selenium并不會(huì)等待所有請求都完畢后再返回
解決方法:
- 您可以通過browser.implicitly_wait(30),來強(qiáng)制selenium等待30秒(無論元素是否加載出來,都必須等待30秒)
- 可以通過等待,直到某個(gè)元素出現(xiàn),然后再返回html
所以筆者對<等待某個(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)站,他的列表頁是靜態(tài)的,但是內(nèi)容頁是動(dòng)態(tài)的
所以,用selenium試一下,目標(biāo)是扣出內(nèi)容頁的#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ā)起請求,記錄頁碼
"""
start_url = self.url_format.format(page_num=1)
meta = dict(page_num=1)
# 列表頁是靜態(tài)的,所以不需要啟用selenium,用普通的scrapy.Request就可以了
yield scrapy.Request(start_url, meta=meta, callback=self.parse)
def parse(self, response):
"""
從列表頁解析出正文的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)容頁是動(dòng)態(tài)的,#content是ajax動(dòng)態(tài)加載的,所以啟用一波selenium
yield SeleniumRequest(url=content_url, meta=meta, callback=self.parse_content)
# 翻頁
meta['page_num'] += 1
next_url = self.url_format.format(page_num=meta['page_num'])
# 列表頁是靜態(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)容頁的目標(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)頁面別的方法)
scrapy官方推薦的scrapy_splash
優(yōu)點(diǎn)
- 是異步的
- 可以將部署scrapy的服務(wù)器與部署splash的服務(wù)器分離開
- 留給讀者遐想的空間
本人覺得的缺點(diǎn)
- 喂喂,lua腳本很麻煩好嗎…(大牛請別打我)
最新的異步pyppeteer操控瀏覽器
優(yōu)點(diǎn)
- 調(diào)用瀏覽器是異步的,操控的單位是tab頁,速度更快
- 留給讀者遐想的空間
本人覺得的缺點(diǎn)
- 因?yàn)閜yppeteer是python版puppeteer,所以puppeteer的一些毛病,pyppeteer無可避免的完美繼承
- 筆者試過將pyppeteer整合至scrapy中,在異步中,scrapy跑起來爬蟲,總會(huì)偶爾timeout之類的…
anyway,上面兩個(gè)都是不錯(cuò)的替代,有興趣的讀者可以試一波
7. scrapy整合selenium的一些缺點(diǎn)
- selenium是阻塞的,所以速度會(huì)慢些
- 對于一些稍微簡單的動(dòng)態(tài)頁面,最好還是自己去解析一下接口,不要太過依賴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)頁面的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)scrapy selenium解析動(dòng)態(tài)頁面內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Python異步編程之新舊協(xié)程的實(shí)現(xiàn)對比
Python中新舊協(xié)程的實(shí)現(xiàn)方式在協(xié)程發(fā)展史上有一段交集,并且舊協(xié)程基于生成器的協(xié)程語法讓生成器和協(xié)程兩個(gè)概念混淆,所以對學(xué)習(xí)者會(huì)造成一定的困擾,本文主要說明兩種協(xié)程的實(shí)現(xiàn)方式的差異,需要的可以了解下2024-01-01
用python + openpyxl處理excel2007文檔思路以及心得
最近要幫做RA的老姐寫個(gè)合并excel工作表的腳本……源數(shù)據(jù)是4000+個(gè)excel 工作表,分布在9個(gè)xlsm文件里,文件內(nèi)容是中英文混雜的一些數(shù)據(jù),需要從每張表中提取需要的部分,分門別類合并到多個(gè)大的表里。2014-07-07
python采用requests庫模擬登錄和抓取數(shù)據(jù)的簡單示例
這篇文章主要介紹了python采用requests庫模擬登錄和抓取數(shù)據(jù)的簡單示例,代碼簡單卻功能強(qiáng)大!需要的朋友可以參考下2014-07-07
python實(shí)現(xiàn)兩個(gè)文件夾的同步
這篇文章主要為大家詳細(xì)介紹了利用python實(shí)現(xiàn)兩個(gè)文件夾的同步,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-08-08
plt.figure()參數(shù)使用詳解及運(yùn)行演示
這篇文章主要介紹了plt.figure()參數(shù)使用詳解及運(yùn)行演示,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01
Python利用pyecharts實(shí)現(xiàn)數(shù)據(jù)可視化的示例代碼
Pyecharts是一個(gè)用于生成 Echarts 圖表的 Python 庫,Echarts 是一個(gè)由百度開源的數(shù)據(jù)可視化工具,它提供的圖表種類豐富,交互性強(qiáng),兼容性好,非常適合用于數(shù)據(jù)分析結(jié)果的展示,本文將給大家介紹Python利用pyecharts實(shí)現(xiàn)數(shù)據(jù)可視化,需要的朋友可以參考下2024-09-09
pytorch 修改預(yù)訓(xùn)練model實(shí)例
今天小編就為大家分享一篇pytorch 修改預(yù)訓(xùn)練model實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-01-01

