說到python爬蟲,剛開始主要用urllib庫,雖然接口比較繁瑣,但也能實(shí)現(xiàn)基本功能。等見識(shí)了requests庫的威力后,便放棄urllib庫,并且也不打算回去了。但對一些動(dòng)態(tài)加載的網(wǎng)站,經(jīng)常要先分析請求,再用requests模擬,比較麻煩。直到遇到了selenium庫,才發(fā)現(xiàn)爬動(dòng)態(tài)網(wǎng)頁也可以這么簡單,果斷入坑!
selenium是python的一個(gè)第三方自動(dòng)化測試庫,雖然是測試庫,卻也非常適合用來寫爬蟲,而phantomJS是其子包webdriver下面的一個(gè)瀏覽器。phantomJS本身是一個(gè)無頭瀏覽器(headless browser),也稱無界面瀏覽器。可以在通過官網(wǎng)下載運(yùn)行phantomjs.exe,簡單幾行代碼也能訪問網(wǎng)頁,爬取數(shù)據(jù)。但本文主要討論通過python的selenium庫使用phantomJS。除了phantomJS瀏覽器,webdriver還整合了Chrome、Firefox、IE等瀏覽器,并提供了操作這些瀏覽器的接口。
由于phantomJS是無界面瀏覽器,不需要界面的同時(shí)占用的內(nèi)存也相對較小,更適用于大規(guī)模多進(jìn)程爬數(shù)據(jù)(試想,如果開幾十個(gè)Chrome進(jìn)程爬數(shù)據(jù),那真是內(nèi)存噩夢?。?。本文主要討論使用selenium phantomJS過程中遇到的bug,而不是selenium phantomJS使用教程,有需要了解selenium基本用法的同學(xué),請移步官方文檔。
個(gè)人用phantomJS爬數(shù)據(jù)有一段時(shí)間了,爬蟲程序也大致完工了,過程中遇到了很多坑,統(tǒng)一總結(jié)如下。
1. 查看phantomJS文檔
前面提到,phantomJS是selenium子包webdriver下面多個(gè)瀏覽器中的一個(gè),而selenium包對不同的瀏覽器都提供了統(tǒng)一的接口,所以直接查看selenium的官方文檔即可,也有對應(yīng)的中文文檔。文檔內(nèi)容不多,但很全面。遇到不懂的問題,先看文檔肯定沒錯(cuò)。
這里需要注意的是,百度搜索phantomJS得到的結(jié)果只是phantomJS的官方文檔,而phantomJS是一個(gè)獨(dú)立的無界面瀏覽器,也稱JS模擬器,本來就獨(dú)立于python。我們需要的是phantomJS的python接口,也就是通過python調(diào)用phantomJS,所以只需查看selenium的webdriver文檔。
當(dāng)然,官方文檔很全面,但也相對繁雜。python有個(gè)查看文檔的小技巧,直接使用help()就能查看某個(gè)對象的幫助文檔。比如help(driver)即可直接查看driver這個(gè)對象的文檔,包括其內(nèi)部函數(shù)、變量的說明。如果driver是一個(gè)phantomJS對象,那么會(huì)顯示phantomJS瀏覽器對象的函數(shù)和變量的文檔,具體內(nèi)容和官方文檔一樣。對所有的python對象都可以這樣干,非常便捷。似乎現(xiàn)在各種IDE也有這個(gè)功能:當(dāng)鼠標(biāo)懸停在某個(gè)對象上,就顯示該對象的幫助文檔。不過多掌握個(gè)方法總歸沒錯(cuò)。
2. phantomJS的配置問題
selenium官方文檔中,phantomJS對象的幫助文檔很詳細(xì),但當(dāng)涉及phantomJS瀏覽器的配置,比如user-agent偽裝、代理、超時(shí)返回等選項(xiàng)時(shí),有用的信息就非常少了。結(jié)合網(wǎng)上的資料和自己遇到的各種坑,我總結(jié)了常用的phantomJS配置選項(xiàng),對普通的爬蟲來說,應(yīng)該夠用了。
from selenium import webdriver
# 引入配置對象DesiredCapabilities
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
dcap = dict(DesiredCapabilities.PHANTOMJS)
#從USER_AGENTS列表中隨機(jī)選一個(gè)瀏覽器頭,偽裝瀏覽器
dcap["phantomjs.page.settings.userAgent"] = (random.choice(USER_AGENTS))
# 不載入圖片,爬頁面速度會(huì)快很多
dcap["phantomjs.page.settings.loadImages"] = False
# 設(shè)置代理
service_args = ['--proxy=127.0.0.1:9999','--proxy-type=socks5']
#打開帶配置信息的phantomJS瀏覽器
driver = webdriver.PhantomJS(phantomjs_driver_path, desired_capabilities=dcap,service_args=service_args)
# 隱式等待5秒,可以自己調(diào)節(jié)
driver.implicitly_wait(5)
# 設(shè)置10秒頁面超時(shí)返回,類似于requests.get()的timeout選項(xiàng),driver.get()沒有timeout選項(xiàng)
# 以前遇到過driver.get(url)一直不返回,但也不報(bào)錯(cuò)的問題,這時(shí)程序會(huì)卡住,設(shè)置超時(shí)選項(xiàng)能解決這個(gè)問題。
driver.set_page_load_timeout(10)
# 設(shè)置10秒腳本超時(shí)時(shí)間
driver.set_script_timeout(10)
3. phantomJS的并發(fā)問題
phantomJS爬數(shù)據(jù)比較慢,并發(fā)編程幾乎是必選項(xiàng)。最初,我考慮采用多線程/協(xié)程的方式,畢竟對于這種IO密集型的程序,多線程/協(xié)程比較合適。但多次測試下來,程序卻遇到各種問題,有時(shí)能成功運(yùn)行,有時(shí)卻不能。嘗試將phantomJS改成Chrome,程序居然能正常運(yùn)行,這基本確定是phantomJS的鍋了。所以,如果需要并發(fā)編程提高效率,用Chrome比較好,雖然內(nèi)存占用相對較多,況且經(jīng)下面簡友提醒,在沒界面的主機(jī)上也可以跑Chrome,那自然更好了。
在網(wǎng)上仔細(xì)查找了相關(guān)資料(這玩意的中文資料極少,只能去國外技術(shù)論壇潛水),原來phantomJS本身在多線程方面還有很多bug,建議使用多進(jìn)程,具體什么原因有時(shí)間再去了解。
關(guān)于多進(jìn)程,推薦使用multiprocessing庫,簡潔、高效!下面幾行代碼便實(shí)現(xiàn)了多進(jìn)程并發(fā)。
from multiprocessing import Pool
pool = Pool(8)
data_list = pool.map(get, url_list)
pool.close()
pool.join()
4. phantomJS進(jìn)程不自動(dòng)退出問題
話說,一開始我寫好程序后,先在本地測試了一段時(shí)間,確認(rèn)程序各方面都沒問題后,直接扔阿里云主機(jī)上跑了。過了一段時(shí)間,查了下程序運(yùn)行日志,很好,一切如常。于是我就高高興興地摸魚去了。
第二天準(zhǔn)備登錄主機(jī)驗(yàn)收程序時(shí),卻發(fā)現(xiàn)居然無法登錄!啥,無法登錄?難不成這個(gè)小爬蟲程序還能把主機(jī)搞崩?我先在阿里云后臺(tái)查看了主機(jī)的運(yùn)行日志,發(fā)現(xiàn)主機(jī)的內(nèi)存使用越來越高,應(yīng)該是內(nèi)存耗盡后,強(qiáng)制關(guān)機(jī)了。似乎是程序沒有回收內(nèi)存,導(dǎo)致占用的內(nèi)存越來越大。明確大致原因后,就是痛苦的查bug過程了。
由于bug涉及內(nèi)存的使用,我自然地想到了用top命令查看進(jìn)程的內(nèi)存使用情況。先運(yùn)行程序,然后運(yùn)行top命令,實(shí)時(shí)檢測程序的內(nèi)存使用情況。一開始程序占用內(nèi)存在正常范圍,只有一個(gè)phantomJS進(jìn)程在運(yùn)行,似乎沒有什么不對。但隨著時(shí)間的增長,內(nèi)存中居然同時(shí)有好幾個(gè)phantomJS進(jìn)程在運(yùn)行,內(nèi)存所??臻g越來越??!但根據(jù)程序的邏輯,任何時(shí)候都只有一個(gè)phantomJS進(jìn)程在爬數(shù)據(jù)。我意識(shí)到可能是由于phantomJS進(jìn)程沒有正常關(guān)閉,所以在內(nèi)存中駐留的phantomJS進(jìn)程越來越多,最終吃光了內(nèi)存。
帶著這個(gè)問題,我重新檢查了一次代碼,尤其在程序異常退出的地方。最終找到了類似下面的代碼:
try:
self.driver.get(url)
self.wait_()
return True
except Exception as e:
return False
程序的邏輯是:如果在打開url的過程中報(bào)錯(cuò),那么就返回False,反之返回True。
似乎直接return False的處理太粗心了。我嘗試著在return False前加上一行self.driver.quit()。再次運(yùn)行程序,并用top查看內(nèi)存使用情況,發(fā)現(xiàn)程序的內(nèi)存使用一直都在正常范圍內(nèi),并沒有出現(xiàn)多個(gè)phantomJS進(jìn)程的情況,問題搞定!后面在網(wǎng)上找到的資料也證實(shí)了我的猜想:主程序退出后,selenium不保證phantomJS也成功退出,最好手動(dòng)關(guān)閉phantomJS進(jìn)程。
5. 其他問題
5.1 不同frame間的轉(zhuǎn)換
有時(shí),phantomJS獲得的頁面源碼的確存在某元素,但通過find_element_by_xpath()等定位函數(shù)卻無法獲得該元素對象,總是提示“元素不存在”的錯(cuò)誤。遇到這種情況,除了檢查元素節(jié)點(diǎn)路徑是否正確外,還應(yīng)該分析頁面源碼,檢查元素是否被包裹在一個(gè)特定的frame中,如果是后者,那么在使用查找函數(shù)前,需要額外的處理。
比如網(wǎng)頁源碼中有如下代碼:
<iframe id="topmenuFrame" width="100%" scrolling="no" height="100%" src="topmenu.aspx?>
<div id="haha">text</div>
</iframe>
假如你想要獲取id="haha"的div標(biāo)簽,直接通過driver.find_element_by_id('haha')就會(huì)提示“元素不存在“的錯(cuò)誤。
這時(shí)需要使用driver.switch_to_frame(driver.find_element_by_id``("topmenuFrame")),即先進(jìn)入id為topmenuFrame的frame,然后再執(zhí)行driver.find_element_by_id("haha"),就能正確獲得該元素了。
需要注意的是,切換到這個(gè)frame之后,只能訪問當(dāng)前frame的內(nèi)容,如果想要回到默認(rèn)的內(nèi)容范圍,相當(dāng)于默認(rèn)的frame,還需要使用driver.switch_to_default_content()。
頁面中有多個(gè)frame時(shí),要注意frame之間的切換。
5.2 implicit_wait、WebDriverWait不一定靠譜
宿舍哥們用phantomJS爬數(shù)據(jù)時(shí),遇到了一個(gè)匪夷所思的bug。起初,他寫了個(gè)很簡單的程序,從個(gè)方面來看都沒問題,但實(shí)際運(yùn)行卻提示各種錯(cuò)誤,讓人十分費(fèi)解。折騰大半天之后,他直接注釋掉自己不太熟悉的implicit_wait(),改用time.sleep()作延時(shí),程序居然就能正確運(yùn)行了!原來implicit_wait()有bug。同樣的,對于WebDriverWait,大家使用時(shí)也要特別注意。
看來python的selenium庫不是很成熟,還存在一些問題,一些函數(shù)的實(shí)際運(yùn)行情況并不是預(yù)期的那樣,在查bug時(shí),要留意這些問題。
6. 總結(jié)
總的來說,selenium庫簡單,容易上手,是爬動(dòng)態(tài)網(wǎng)頁的殺手級武器,但對phantomJS瀏覽器的支持還不是特別完善。當(dāng)然,了解存在的問題,并找到對應(yīng)的解決方法,就能發(fā)揮phantomJS的威力了。
以上就是我個(gè)人這段時(shí)間的phantomJS使用小結(jié),雖然不是很全面,也不確保完全準(zhǔn)確,算是對我這段學(xué)習(xí)歷程的總結(jié)吧,希望對大家有用。