Selenium的Webdriver爬取動(dòng)態(tài)網(wǎng)頁(yè)效果雖然不錯(cuò)俩滥,但效率方面并不如人意躬它。最近一直研究如何提高動(dòng)態(tài)頁(yè)面爬蟲(chóng)的效率巍实,方法無(wú)非高并發(fā)和分布式兩種滓技。過(guò)程中有很多收獲,也踩了不少坑棚潦,在此一并做個(gè)總結(jié)令漂。以下大致是這段時(shí)間的學(xué)習(xí)路線。
一丸边、 Scrapy+phantomJS
Scrapy是一個(gè)高效的異步爬蟲(chóng)框架叠必,使用比較廣泛,文檔也很完備妹窖,開(kāi)發(fā)人員能快速地實(shí)現(xiàn)高性能爬蟲(chóng)纬朝。關(guān)于Scrapy的基本使用這里就不再贅述了, 這篇Scrapy讀書筆記挺不錯(cuò)的骄呼。然而Scrapy在默認(rèn)的情況下只能獲取靜態(tài)的網(wǎng)頁(yè)內(nèi)容共苛,因此必須進(jìn)一步定制開(kāi)發(fā)。
Scrapy結(jié)合phantomJS似乎是個(gè)不錯(cuò)的選擇谒麦。phantomJS是一個(gè)沒(méi)有頁(yè)面的瀏覽器俄讹,能渲染動(dòng)態(tài)頁(yè)面并且相對(duì)輕量。因此绕德,我們需要修改Scrapy的網(wǎng)頁(yè)請(qǐng)求模塊患膛,讓phantomJS請(qǐng)求網(wǎng)頁(yè),以達(dá)到獲取動(dòng)態(tài)網(wǎng)頁(yè)的目的耻蛇。一番調(diào)研之后踪蹬,發(fā)現(xiàn)大致有三種定制方法:
1. 每個(gè)url請(qǐng)求兩次。在回調(diào)函數(shù)中舍棄掉返回的response
內(nèi)容臣咖,然后用phantomJS再次請(qǐng)求response.url
跃捣,這次的請(qǐng)求由于沒(méi)有構(gòu)造Request
對(duì)象,當(dāng)然就沒(méi)有回調(diào)函數(shù)了夺蛇,然后阻塞等待結(jié)果返回即可疚漆。這個(gè)方法會(huì)對(duì)同一個(gè)url請(qǐng)求兩次,第一次是Scrapy默認(rèn)的HTTP請(qǐng)求,第二次則是phantomJS的請(qǐng)求娶聘,當(dāng)然第二次獲取到的就是動(dòng)態(tài)網(wǎng)頁(yè)了闻镶。這個(gè)方法比較適合快速實(shí)現(xiàn)小規(guī)模動(dòng)態(tài)爬蟲(chóng),在默認(rèn)的Scrapy項(xiàng)目基礎(chǔ)上丸升,只需要簡(jiǎn)單修改回調(diào)函數(shù)就可以了铆农。
2. 自定義下載中間件(downloadMiddleware
)。downloadMiddleware
對(duì)從scheduler
送來(lái)的Request
對(duì)象在請(qǐng)求之前進(jìn)行預(yù)處理狡耻,可以實(shí)現(xiàn)添加headers
墩剖,user_agent
,還有cookie
等功能 夷狰。但也可以通過(guò)中間件直接返回HtmlResponse
對(duì)象岭皂,略過(guò)請(qǐng)求的模塊,直接扔給response
的回調(diào)函數(shù)處理孵淘。代碼如下:
class CustomMetaMiddleware(object):
def process_request(self,request,spider):
dcap = dict(DesiredCapabilities.PHANTOMJS)
dcap["phantomjs.page.settings.loadImages"] = False
dcap["phantomjs.page.settings.resourceTimeout"] = 10
driver = webdriver.PhantomJS("E:xx\xx\xx",desired_capabilities=dcap)
driver.get(request.url)
body = driver.page_source.encode('utf8')
url = driver.current_url
driver.quit()
return HtmlResponse(request.url,body=body)
改完代碼后蒲障,記得修改settings配置。但這個(gè)方法有個(gè)很大的問(wèn)題——不能實(shí)現(xiàn)異步爬取瘫证。由于直接在下載中間件中請(qǐng)求網(wǎng)頁(yè)揉阎,而Scrapy在這里卻不是異步的,只能實(shí)現(xiàn)阻塞式的逐個(gè)網(wǎng)頁(yè)下載背捌。當(dāng)然毙籽,如果不追求高并發(fā)的話,這也是個(gè)快速部署動(dòng)態(tài)爬蟲(chóng)的方法毡庆。
3.自定義downloader
坑赡。downloader
是Scrapy發(fā)起HTTP請(qǐng)求的模塊,這模塊實(shí)現(xiàn)了異步請(qǐng)求么抗,因此自定義downloader
是最完美的實(shí)現(xiàn)毅否。但是要編寫一個(gè)自定義的downloader
比較麻煩,必須按照Twisted的一些規(guī)范蝇刀,所幸網(wǎng)上有一些開(kāi)源的downloader
螟加,在這基礎(chǔ)上改改就比較容易了。 這篇文章詳解了downloader
的開(kāi)發(fā)吞琐,非常不錯(cuò)捆探!
一些坑和心得
通過(guò)代碼運(yùn)行Scrapy是個(gè)很有用的方法,即通過(guò)
CrawlerProcess
類運(yùn)行爬蟲(chóng)站粟,但是給Spider傳遞settings
參數(shù)卻是一個(gè)很大的坑黍图,這個(gè)問(wèn)題繞了我很長(zhǎng)時(shí)間,最后的解決方法是修改PYTHONPATH
和SCRAPY_SETTINGS_MODULE
環(huán)境變量奴烙,加上爬蟲(chóng)項(xiàng)目的目錄助被,這樣Python才能找到配置文件剖张。設(shè)置
DOWNLOAD_TIMEOUT
選項(xiàng),其默認(rèn)值是180秒恰起,相對(duì)較長(zhǎng)修械,可以設(shè)置得短一些提高效率。PhantomJS對(duì)多進(jìn)程的支持極不穩(wěn)定检盼。具體表現(xiàn)在如果一主機(jī)同時(shí)開(kāi)了多個(gè)phantomJS進(jìn)程,單個(gè)phantomJS運(yùn)行結(jié)果就會(huì)時(shí)好時(shí)壞翘单,經(jīng)常出現(xiàn)一些莫名其妙的報(bào)錯(cuò)吨枉,官方git的issue上也提到phantomJS對(duì)多進(jìn)程的支持很不好。如果真要多進(jìn)程爬蟲(chóng)的話哄芜,推薦chromedriver貌亭。
Scrapy的優(yōu)勢(shì)在于高效的異步請(qǐng)求框架,由于其本身并不支持動(dòng)態(tài)頁(yè)面爬取认臊,如果對(duì)爬蟲(chóng)的效率沒(méi)有特別高的要求圃庭,也沒(méi)有必要一定用這個(gè)框架,畢竟熟悉框架要一定的時(shí)間成本失晴,在框架下編程限制也比較多剧腻,對(duì)一些比較簡(jiǎn)單的爬蟲(chóng),有時(shí)還不如自己手?jǐn)]一個(gè)涂屁。
二书在、 Scrapy-splash
由于phantomJS的多并發(fā)短板,Scrapy+phantomJS的效率受限拆又,因此儒旬,這并不是一個(gè)特別好的選擇。
又一番調(diào)研后帖族,發(fā)現(xiàn)splash似乎是個(gè)不錯(cuò)的選擇栈源。Splash是一個(gè)Javascript渲染服務(wù)。它是用Python實(shí)現(xiàn)的竖般,同時(shí)使用了Twisted和QT甚垦,并且實(shí)現(xiàn)了HTTP API的輕量瀏覽器,Twisted(QT)用來(lái)讓服務(wù)具有異步處理能力捻激,以發(fā)揮webkit的并發(fā)能力制轰。
在Scrapy中使用splash也很簡(jiǎn)單,詳見(jiàn)http://www.cnblogs.com/zhonghuasong/p/5976003.html 胞谭。
一般來(lái)說(shuō)垃杖,在Scrapy中只需要返回一個(gè)SplashRequest
對(duì)象即可。比如:
yield SplashRequest(url='http://'+url,callback=self.parse,endpoint='render.html',
args={'wait':2},errback=self.errback_fun, meta={ })
同樣也可以返回帶POST參數(shù)的Request
對(duì)象丈屹。更簡(jiǎn)單地调俘,用urllib
等庫(kù)構(gòu)造POST請(qǐng)求也沒(méi)問(wèn)題伶棒,因?yàn)檫@本質(zhì)上是一個(gè)端口代理,可以接受任何的HTTP請(qǐng)求彩库。
splash的內(nèi)存占用相對(duì)較少肤无,但多并發(fā)仍然會(huì)出現(xiàn)些問(wèn)題,請(qǐng)求的失敗率會(huì)大大提高骇钦,頁(yè)面渲染結(jié)果偶爾會(huì)出現(xiàn)一些問(wèn)題宛渐,同時(shí)受制于服務(wù)器主機(jī)的帶寬,速度受限眯搭,但總體表現(xiàn)不錯(cuò)窥翩,足以應(yīng)對(duì)小規(guī)模的動(dòng)態(tài)爬蟲(chóng)。
Splash的優(yōu)點(diǎn)也很顯著鳞仙,通過(guò)HTTP API寇蚊,其他分布式節(jié)點(diǎn)能很容易地獲得動(dòng)態(tài)頁(yè)面,并且使得服務(wù)器和其他節(jié)點(diǎn)之間的耦合降到了最低棍好,擴(kuò)展變得特別方便仗岸。另外,分布式節(jié)點(diǎn)不用配置環(huán)境就能獲得動(dòng)態(tài)頁(yè)面借笙,相對(duì)phantomJS復(fù)雜的配置來(lái)說(shuō)簡(jiǎn)單太多了扒怖!如果想簡(jiǎn)單地實(shí)現(xiàn)動(dòng)態(tài)頁(yè)面爬蟲(chóng),splash是一個(gè)非常好的選擇提澎,但受制于單個(gè)服務(wù)器帶寬姚垃,速度有限,并且有時(shí)渲染效果不是很理想盼忌。
三积糯、 chromedriver并發(fā)
無(wú)論phantomJS還是splash,穩(wěn)定性是一方面谦纱,在渲染效果和速度上都不及chromedriver看成,畢竟V8引擎不是蓋的!但chromedriver缺點(diǎn)也很明顯——特別耗內(nèi)存跨嘉,而且是有界面的川慌!
有段時(shí)間為了爬百度搜索結(jié)果,我一開(kāi)始用requests
庫(kù)模擬POST請(qǐng)求祠乃,雖然效率沒(méi)問(wèn)題梦重,但經(jīng)常被百度封,于是試著改用phantomJS亮瓷,當(dāng)時(shí)覺(jué)得盡管效率低了點(diǎn)琴拧,但畢竟是真正的瀏覽器,百度應(yīng)該不會(huì)封嘱支。后來(lái)發(fā)現(xiàn)作用也不大蚓胸,還是經(jīng)常被封挣饥,并且phantomJS自身不太穩(wěn)定,經(jīng)常報(bào)錯(cuò)沛膳,多進(jìn)程并發(fā)更是沒(méi)辦法運(yùn)行扔枫。看來(lái)只能試一試chromedriver了锹安。以前一直忌憚?dòng)趦?nèi)存殺手chrome(開(kāi)一個(gè)chrome瀏覽器短荐,任務(wù)管理器里就有很多個(gè)chrome進(jìn)程),最后無(wú)奈只能祭出這大殺器了八毯。跑了一段時(shí)間之后搓侄,發(fā)現(xiàn)chrome的效率還挺不錯(cuò),占用的內(nèi)存也沒(méi)有想象中的大话速,多并發(fā)支持非常好,在我的電腦上同時(shí)開(kāi)20來(lái)個(gè)也沒(méi)問(wèn)題芯侥,穩(wěn)定性也不錯(cuò)泊交,而且百度居然就沒(méi)封!(震驚V椤@蟆!chrome居然自帶反反爬蟲(chóng)光環(huán)0ぁ)研乒。但由于程序主要在阿里云主機(jī)上跑,有界面的chromedriver當(dāng)時(shí)便沒(méi)有考慮在內(nèi)淋硝,前不久才知道原來(lái)可以通過(guò)引入虛擬界面雹熬,讓chrome在沒(méi)有界面的主機(jī)上跑.....
Python的pyvirtualdisplay
庫(kù)就能引入虛擬界面。
代碼實(shí)現(xiàn)也非常簡(jiǎn)單:
from pyvirtualdisplay import Display
display = Display(visible=0,size=(800,600))
display.start()
driver = webdriver.Chrome()
driver.get('http://www.baidu.com')
經(jīng)個(gè)人測(cè)試谣膳,發(fā)現(xiàn)chrome對(duì)多進(jìn)程的支持非常好竿报,渲染速度快,就是內(nèi)存占用相對(duì)較大继谚,可以多進(jìn)程+分布式提高效率烈菌,關(guān)鍵chrome不容易被封。
PS. 常用的chromedriver關(guān)閉圖片選項(xiàng)代碼:
chromeOptions = webdriver.ChromeOptions()
prefs = {"profile.managed_default_content_settings.images":2}
chromeOptions.add_experimental_option("prefs",prefs)
driver = webdriver.Chrome(chromedriver_path,chrome_options=chromeOptions)
driver.get(url)
四花履、Selenium Grid
Selenium Grid是Selenium的單機(jī)擴(kuò)展芽世,允許用戶將測(cè)試案例分布在幾臺(tái)機(jī)器上并行執(zhí)行。當(dāng)然诡壁,能實(shí)現(xiàn)分布式測(cè)試济瓢,分布式爬蟲(chóng)當(dāng)然沒(méi)問(wèn)題。
Selenium Grid的機(jī)制如圖欢峰。首先啟動(dòng)一個(gè)中央節(jié)點(diǎn)(Hub)葬荷,然后啟動(dòng)多個(gè)遠(yuǎn)程控制節(jié)點(diǎn)(rc)涨共,并讓rc在Hub上注冊(cè)自己的信息,包括rc自身的系統(tǒng)宠漩、支持的webdriver举反、最大并發(fā)數(shù)量等,這樣Hub節(jié)點(diǎn)就知道了所有的rc信息扒吁,方便以后調(diào)度火鼻。
運(yùn)行環(huán)境搭建好之后,測(cè)試或爬蟲(chóng)腳本請(qǐng)求Hub的服務(wù)端口雕崩,Hub主機(jī)根據(jù)注冊(cè)的rc節(jié)點(diǎn)的當(dāng)前狀態(tài)魁索,結(jié)合負(fù)載均衡原則,將這些測(cè)試用例分發(fā)到指定的rc節(jié)點(diǎn)盼铁,rc節(jié)點(diǎn)接到命令之后便執(zhí)行粗蔚。
from selenium import webdriver
url = "http://localhost:4444/wd/hub"
driver = webdriver.Remote(command_executor = url, desired_capabilities = {'browserName':'chrome'})
driver.get("http://www.baidu.com")
print driver.title
如下圖,我在本地建立了一個(gè)Hub節(jié)點(diǎn)饶火,默認(rèn)端口是4444鹏控,接著用本機(jī)注冊(cè)了兩個(gè)rc節(jié)點(diǎn),端口分別為5555肤寝、6666当辐。通過(guò)hub服務(wù)端口的控制臺(tái)可以看到,每個(gè)節(jié)點(diǎn)可以支持5個(gè)Firefox實(shí)例鲤看、一個(gè)IE實(shí)例和5個(gè)Chrome實(shí)例(可以自定義)缘揪。由于本機(jī)沒(méi)有安裝Opera瀏覽器,當(dāng)然也就沒(méi)有Opera實(shí)例了义桂。
Selenium Grid是個(gè)很好的實(shí)現(xiàn)分布式測(cè)試/動(dòng)態(tài)爬蟲(chóng)的框架找筝,原理和操作也不復(fù)雜,有興趣的同學(xué)可以多了解了解澡刹。
五呻征、 總結(jié)
以上各軟件或框架的特點(diǎn)簡(jiǎn)要如下:
- phantomJS比較輕量,但對(duì)多并發(fā)支持非常差
- chromedriver渲染速度快罢浇,多并發(fā)支持較好陆赋,但占用內(nèi)存大
- splash實(shí)現(xiàn)了HTTP API,分布式擴(kuò)展容易嚷闭,頁(yè)面渲染能力一般
- Selenium Grid是專業(yè)的測(cè)試框架攒岛,擴(kuò)展容易,支持負(fù)載均衡等高級(jí)特性
所以胞锰,分布式Scrapy+chromedriver或Selenium Grid是實(shí)現(xiàn)分布式動(dòng)態(tài)爬蟲(chóng)較好的選擇灾锯。