1. 程式人生 > >盤點selenium phantomJS使用的坑

盤點selenium phantomJS使用的坑

效率 scrip gen time self. doc http width 並發

說到python爬蟲,剛開始主要用urllib庫,雖然接口比較繁瑣,但也能實現基本功能。等見識了requests庫的威力後,便放棄urllib庫,並且也不打算回去了。但對一些動態加載的網站,經常要先分析請求,再用requests模擬,比較麻煩。直到遇到了selenium庫,才發現爬動態網頁也可以這麽簡單,果斷入坑!

selenium是python的一個第三方自動化測試庫,雖然是測試庫,卻也非常適合用來寫爬蟲,而phantomJS是其子包webdriver下面的一個瀏覽器。phantomJS本身是一個無頭瀏覽器(headless browser),也稱無界面瀏覽器。可以在通過官網下載運行phantomjs.exe,簡單幾行代碼也能訪問網頁,爬取數據。但本文主要討論通過python的selenium庫使用phantomJS。除了phantomJS瀏覽器,webdriver還整合了Chrome、Firefox、IE等瀏覽器,並提供了操作這些瀏覽器的接口。

由於phantomJS是無界面瀏覽器,不需要界面的同時占用的內存也相對較小,更適用於大規模多進程爬數據(試想,如果開幾十個Chrome進程爬數據,那真是內存噩夢!)。本文主要討論使用selenium phantomJS過程中遇到的bug,而不是selenium phantomJS使用教程,有需要了解selenium基本用法的同學,請移步官方文檔。

個人用phantomJS爬數據有一段時間了,爬蟲程序也大致完工了,過程中遇到了很多坑,統一總結如下。

1. 查看phantomJS文檔

前面提到,phantomJS是selenium子包webdriver下面多個瀏覽器中的一個,而selenium包對不同的瀏覽器都提供了統一的接口,所以直接查看selenium的官方文檔即可,也有對應的中文文檔。文檔內容不多,但很全面。遇到不懂的問題,先看文檔肯定沒錯。

這裏需要註意的是,百度搜索phantomJS得到的結果只是phantomJS的官方文檔,而phantomJS是一個獨立的無界面瀏覽器,也稱JS模擬器,本來就獨立於python。我們需要的是phantomJS的python接口,也就是通過python調用phantomJS,所以只需查看selenium的webdriver文檔。

當然,官方文檔很全面,但也相對繁雜。python有個查看文檔的小技巧,直接使用help()就能查看某個對象的幫助文檔。比如help(driver)即可直接查看driver這個對象的文檔,包括其內部函數、變量的說明。如果driver是一個phantomJS對象,那麽會顯示phantomJS瀏覽器對象的函數和變量的文檔,具體內容和官方文檔一樣。對所有的python對象都可以這樣幹,非常便捷。似乎現在各種IDE也有這個功能:當鼠標懸停在某個對象上,就顯示該對象的幫助文檔。不過多掌握個方法總歸沒錯。

2. phantomJS的配置問題

selenium官方文檔中,phantomJS對象的幫助文檔很詳細,但當涉及phantomJS瀏覽器的配置,比如user-agent偽裝、代理、超時返回等選項時,有用的信息就非常少了。結合網上的資料和自己遇到的各種坑,我總結了常用的phantomJS配置選項,對普通的爬蟲來說,應該夠用了。

from selenium import webdriver
# 引入配置對象DesiredCapabilities
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
dcap = dict(DesiredCapabilities.PHANTOMJS)
#從USER_AGENTS列表中隨機選一個瀏覽器頭,偽裝瀏覽器
dcap["phantomjs.page.settings.userAgent"] = (random.choice(USER_AGENTS))
# 不載入圖片,爬頁面速度會快很多
dcap["phantomjs.page.settings.loadImages"] = False
# 設置代理
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秒,可以自己調節
driver.implicitly_wait(5)
# 設置10秒頁面超時返回,類似於requests.get()的timeout選項,driver.get()沒有timeout選項
# 以前遇到過driver.get(url)一直不返回,但也不報錯的問題,這時程序會卡住,設置超時選項能解決這個問題。
driver.set_page_load_timeout(10)
# 設置10秒腳本超時時間
driver.set_script_timeout(10)

3. phantomJS的並發問題

phantomJS爬數據比較慢,並發編程幾乎是必選項。最初,我考慮采用多線程/協程的方式,畢竟對於這種IO密集型的程序,多線程/協程比較合適。但多次測試下來,程序卻遇到各種問題,有時能成功運行,有時卻不能。嘗試將phantomJS改成Chrome,程序居然能正常運行,這基本確定是phantomJS的鍋了。所以,如果需要並發編程提高效率,用Chrome比較好,雖然內存占用相對較多,況且經下面簡友提醒,在沒界面的主機上也可以跑Chrome,那自然更好了。

在網上仔細查找了相關資料(這玩意的中文資料極少,只能去國外技術論壇潛水),原來phantomJS本身在多線程方面還有很多bug,建議使用多進程,具體什麽原因有時間再去了解。

關於多進程,推薦使用multiprocessing庫,簡潔、高效!下面幾行代碼便實現了多進程並發。

from multiprocessing import Pool
pool = Pool(8)
data_list = pool.map(get, url_list)
pool.close()
pool.join()

4. phantomJS進程不自動退出問題

話說,一開始我寫好程序後,先在本地測試了一段時間,確認程序各方面都沒問題後,直接扔阿裏雲主機上跑了。過了一段時間,查了下程序運行日誌,很好,一切如常。於是我就高高興興地摸魚去了。

第二天準備登錄主機驗收程序時,卻發現居然無法登錄!啥,無法登錄?難不成這個小爬蟲程序還能把主機搞崩?我先在阿裏雲後臺查看了主機的運行日誌,發現主機的內存使用越來越高,應該是內存耗盡後,強制關機了。似乎是程序沒有回收內存,導致占用的內存越來越大。明確大致原因後,就是痛苦的查bug過程了。

由於bug涉及內存的使用,我自然地想到了用top命令查看進程的內存使用情況。先運行程序,然後運行top命令,實時檢測程序的內存使用情況。一開始程序占用內存在正常範圍,只有一個phantomJS進程在運行,似乎沒有什麽不對。但隨著時間的增長,內存中居然同時有好幾個phantomJS進程在運行,內存所剩空間越來越小!但根據程序的邏輯,任何時候都只有一個phantomJS進程在爬數據。我意識到可能是由於phantomJS進程沒有正常關閉,所以在內存中駐留的phantomJS進程越來越多,最終吃光了內存。

帶著這個問題,我重新檢查了一次代碼,尤其在程序異常退出的地方。最終找到了類似下面的代碼:

try:
    self.driver.get(url)
    self.wait_()
    return True
except Exception as e:
    return False

程序的邏輯是:如果在打開url的過程中報錯,那麽就返回False,反之返回True。

似乎直接return False的處理太粗心了。我嘗試著在return False前加上一行self.driver.quit()。再次運行程序,並用top查看內存使用情況,發現程序的內存使用一直都在正常範圍內,並沒有出現多個phantomJS進程的情況,問題搞定!後面在網上找到的資料也證實了我的猜想:主程序退出後,selenium不保證phantomJS也成功退出,最好手動關閉phantomJS進程。

5. 其他問題

5.1 不同frame間的轉換

有時,phantomJS獲得的頁面源碼的確存在某元素,但通過find_element_by_xpath()等定位函數卻無法獲得該元素對象,總是提示“元素不存在”的錯誤。遇到這種情況,除了檢查元素節點路徑是否正確外,還應該分析頁面源碼,檢查元素是否被包裹在一個特定的frame中,如果是後者,那麽在使用查找函數前,需要額外的處理。

比如網頁源碼中有如下代碼:

<iframe id="topmenuFrame" width="100%" scrolling="no" height="100%" src="topmenu.aspx?>
<div id="haha">text</div>
</iframe>

假如你想要獲取id="haha"的div標簽,直接通過driver.find_element_by_id(‘haha‘)就會提示“元素不存在“的錯誤。

這時需要使用driver.switch_to_frame(driver.find_element_by_id("topmenuFrame")),即先進入id為topmenuFrame的frame,然後再執行driver.find_element_by_id("haha"),就能正確獲得該元素了。

需要註意的是,切換到這個frame之後,只能訪問當前frame的內容,如果想要回到默認的內容範圍,相當於默認的frame,還需要使用driver.switch_to_default_content()

頁面中有多個frame時,要註意frame之間的切換。

5.2 implicit_wait、WebDriverWait不一定靠譜

宿舍哥們用phantomJS爬數據時,遇到了一個匪夷所思的bug。起初,他寫了個很簡單的程序,從個方面來看都沒問題,但實際運行卻提示各種錯誤,讓人十分費解。折騰大半天之後,他直接註釋掉自己不太熟悉的implicit_wait(),改用time.sleep()作延時,程序居然就能正確運行了!原來implicit_wait()有bug。同樣的,對於WebDriverWait,大家使用時也要特別註意。

看來python的selenium庫不是很成熟,還存在一些問題,一些函數的實際運行情況並不是預期的那樣,在查bug時,要留意這些問題。

6. 總結

總的來說,selenium庫簡單,容易上手,是爬動態網頁的殺手級武器,但對phantomJS瀏覽器的支持還不是特別完善。當然,了解存在的問題,並找到對應的解決方法,就能發揮phantomJS的威力了。

以上就是我個人這段時間的phantomJS使用小結,雖然不是很全面,也不確保完全準確,算是對我這段學習歷程的總結吧,希望對大家有用。

盤點selenium phantomJS使用的坑