Selenium是一款強(qiáng)大的基于瀏覽器的開源自動化測試工具碍舍,最初由 Jason Huggins 于 2004 年在 ThoughtWorks 發(fā)起片橡,它提供了一套簡單易用的 API捧书,模擬瀏覽器的各種操作,方便各種 應(yīng)用的自動化測試爆哑。它的取名很有意思揭朝,因?yàn)楫?dāng)時最流行的一款自動化測試工具叫做 QTP潭袱,是由 Mercury 公司開發(fā)的商業(yè)應(yīng)用屯换。Mercury 是化學(xué)元素汞彤悔,而 Selenium 是化學(xué)元素硒,汞有劇毒掌眠,而硒可以解汞毒,它對汞有拮抗作用望拖。
Selenium 的核心組件叫做 Selenium-RC(Remote Control)说敏,簡單來說它是一個代理服務(wù)器盔沫,瀏覽器啟動時通過將它設(shè)置為代理架诞,它可以修改請求響應(yīng)報文并向其中注入 Javascript谴忧,通過注入的 JS 可以模擬瀏覽器操作沾谓,從而實(shí)現(xiàn)自動化測試戳鹅。但是注入 JS 的方法存在很多限制枫虏,譬如無法模擬鍵盤和鼠標(biāo)事件模软,處理不了對話框燃异,不能繞過 JavaScript 沙箱等等回俐。就在這個時候,于 2006 年左右的工程師 Simon Stewart 發(fā)起了 WebDriver 項(xiàng)目碘举,WebDriver 通過調(diào)用瀏覽器提供的原生自動化 API 來驅(qū)動瀏覽器引颈,解決了 Selenium 的很多疑難雜癥蝙场。不過 WebDriver 也有它不足的地方售滤,它不能支持所有的瀏覽器完箩,需要針對不同的瀏覽器來開發(fā)不同的 WebDriver弊知,因?yàn)椴煌臑g覽器提供的 API 也不盡相同氏仗,好在經(jīng)過不斷的發(fā)展皆尔,各種主流瀏覽器都已經(jīng)有相應(yīng)的 WebDriver 了珊拼。最終 Selenium 和 WebDriver 合并在一起流炕,這就是 Selenium 2.0每辟,有的地方也直接把它稱作 WebDriver渠欺。
一、Selenium 爬蟲入門
Selenium 的初衷是打造一款優(yōu)秀的自動化測試工具编整,但是慢慢的人們就發(fā)現(xiàn)掌测,Selenium 的自動化用來做爬蟲正合適汞斧。我們知道断箫,傳統(tǒng)的爬蟲通過直接模擬 HTTP 請求來爬取站點(diǎn)信息,由于這種方式和瀏覽器訪問差異比較明顯剑勾,很多站點(diǎn)都采取了一些反爬的手段虽另,而 Selenium 是通過模擬瀏覽器來爬取信息捂刺,其行為和用戶幾乎一樣族展,反爬策略也很難區(qū)分出請求到底是來自 Selenium 還是真實(shí)用戶仪缸。而且通過 Selenium 來做爬蟲恰画,不用去分析每個請求的具體參數(shù)拴还,比起傳統(tǒng)的爬蟲開發(fā)起來更容易片林。Selenium 爬蟲唯一的不足是慢拇厢,如果你對爬蟲的速度沒有要求访敌,那使用 Selenium 是個非常不錯的選擇衣盾。Selenium 提供了多種語言的支持不論你是用哪種語言開發(fā)爬蟲阻塑,Selenium 都適合你陈莽。
我們第一節(jié)先通過 Python 學(xué)習(xí) Selenium 的基礎(chǔ)知識走搁,后面幾節(jié)再介紹我在使用 Selenium 開發(fā)瀏覽器爬蟲時遇到的一些問題和解決方法私植。
1.1 Hello World
一個最簡單的 Selenium 程序像下面這樣:
from selenium import webdriver
browser = webdriver.Chrome()
browser.get('http://www.baidu.com/')
這段代碼理論上會打開 Chrome 瀏覽器曲稼,并訪問百度首頁贫悄。但事實(shí)上清女,如果你第一次使用 Selenium嫡丙,很可能會遇到下面這樣的報錯:
selenium.common.exceptions.WebDriverException:
Message: 'chromedriver' executable needs to be in PATH.
Please see https://sites.google.com/a/chromium.org/chromedriver/home
報錯提示很明確,要使用 Chrome 瀏覽器父泳,必須得有 chromedriver惠窄,而且 chromedriver 文件位置必須得配置到 PATH 環(huán)境變量中杆融。chromedriver 文件可以通過錯誤提示中的地址下載脾歇。不過在生產(chǎn)環(huán)境藕各,我并不推薦這樣的做法激况,使用下面的方法可以手動指定 chromedriver 文件的位置:
from selenium import webdriver browser=webdriver.Chrome(executable_path="./drivers/chromedriver.exe")
browser.get('http://www.baidu.com/')
這里給出的例子是 Chrome 瀏覽器宦棺,Selenium 同樣可以驅(qū)動 Firefox、IE成黄、Safari 等奋岁。這里列出了幾個流行瀏覽器webdriver的下載地址闻伶。Selenium 的官網(wǎng)也提供了大多數(shù)瀏覽器驅(qū)動的下載信息蓝翰,你可以參考 Third Party Drivers, Bindings, and Plugins 一節(jié)。
1.2 輸入和輸出
通過上面的一節(jié)爆雹,我們已經(jīng)可以自動的通過瀏覽器打開某個頁面了钙态,作為爬蟲册倒,我們還需要和頁面進(jìn)行更多的交互剩失,歸結(jié)起來可以分為兩大類:輸入和輸出拴孤。
輸入指的是用戶對瀏覽器的所有操作鞭执,譬如上面的直接訪問某個頁面也是一種輸入兄纺,或者在輸入框填寫估脆,下拉列表選擇疙赠,點(diǎn)擊某個按鈕等等圃阳;
輸出指的是根據(jù)輸入操作,對瀏覽器所產(chǎn)生的數(shù)據(jù)進(jìn)行解析锣夹,得到我們需要的數(shù)據(jù)晕城;這里 瀏覽器所產(chǎn)生的數(shù)據(jù) 不僅包括可見的內(nèi)容砖顷,如頁面上顯示的信息豌熄,也還包括不可見的內(nèi)容锣险,如 HTML 源碼芯肤,甚至瀏覽器所發(fā)生的所有 HTTP 請求報文崖咨。
下面還是以百度為例击蹲,介紹幾種常見的輸入輸出方式。
1.2.1 輸入
我們打開百度進(jìn)行搜索类咧,如果是人工操作轮听,一般有兩種方式:第一種萧锉,在輸入框中輸入搜索文字柿隙,然后回車禀崖;第二種艺晴,在輸入框中輸入搜索文字封寞,然后點(diǎn)擊搜索按鈕狈究。Selenium 和人工操作完全一樣亿眠,可以模擬這兩種方式:
方式一 send keys with return
from selenium.webdriver.common.keys import Keys
kw = browser.find_element_by_id("kw")
kw.send_keys("Selenium", Keys.RETURN)
其中 find_element_by_id 方法經(jīng)常用到纳像,它根據(jù)元素的 ID 來查找頁面某個元素。類似的方法還有 find_element_by_name潭兽、find_element_by_class_name山卦、find_element_by_css_selector、find_element_by_xpath 等铸本,都是用于定位頁面元素的箱玷。另外,也可以同時定位多個元素舶得,例如 find_elements_by_name纫骑、find_elements_by_class_name 等惧磺,就是把 find_element 換成 find_elements磨隘,具體的 API 可以參考 Selenium 中文翻譯文檔中的 查找元素 一節(jié)。
通過 find_element_by_id 方法拿到元素之后设预,就可以對這個元素進(jìn)行操作,也可以獲取元素的屬性或者它的文本宾符。kw 這個元素是一個 input 輸入框魏烫,可以通過 send_keys 來模擬按鍵輸入哄褒。不僅可以模擬輸入可見字符,也可以模擬一些特殊按鍵煌张,譬如回車 Keys.RETURN呐赡,可模擬的所有特殊鍵可以參考 這里。
針對不同的元素骏融,有不同的操作罚舱,譬如按鈕绎谦,可以通過 click 方法來模擬點(diǎn)擊,如下粥脚。
方式二 send keys then click submit button
kw = browser.find_element_by_id("kw")
su = browser.find_element_by_id("su")
kw.send_keys("Selenium")
su.click()
如果這個元素是在一個表單(form)中窃肠,還可以通過 submit 方法來模擬提交表單。
方式三 send keys then submit form
kw = browser.find_element_by_id("kw")
kw.send_keys("Selenium")
kw.submit()
submit 方法不僅可以直接應(yīng)用在 form 元素上刷允,也可以應(yīng)用在 form 元素里的所有子元素上冤留,submit 會自動查找離該元素最近的父 form 元素然后提交碧囊。這種方式是程序特有的,有點(diǎn)類似于直接在 Console 里執(zhí)行 $('form').submit() JavaScript 代碼纤怒。由此糯而,我們引出第四種輸入方法,也是最最強(qiáng)大的輸入方法泊窘,可以說幾乎是無所不能熄驼,直接在瀏覽器里執(zhí)行 JavaScript 代碼:
方式四 execute javascript
browser.execute_script(
''' var kw = document.getElementById('kw');
var su = document.getElementById('su');
kw.value = 'Selenium';
su.click();
''')
這和方式二非常相似,但是要注意的是烘豹,方式四是完全通過 JavaScript 來操作頁面瓜贾,所以靈活性是無限大的,幾乎可以做任何操作携悯。除了這些輸入方式祭芦,當(dāng)然還有其他方式,譬如憔鬼,先在輸入框輸入搜索文字龟劲,然后按 Tab 鍵將焦點(diǎn)切換到提交按鈕,然后按回車轴或,原理都是大同小異昌跌,此處不再贅述,你可以自己寫程序試一試侮叮。
另外避矢,對于 select 元素,Selenium 單獨(dú)提供了一個類 selenium.webdriver.support.select.Select 可以方便元素的選取囊榜。其他類型的元素审胸,都可以通過上述四種方式來處理。
1.2.2 輸出
有輸入就有輸出卸勺,當(dāng)點(diǎn)擊搜索按鈕之后砂沛,如果我們要爬取頁面上的搜索結(jié)果,我們有幾種不同的方法曙求。
方式一 parse page_source
html = browser.page_source
results = parse_html(html)
第一種方式最原始碍庵,和傳統(tǒng)爬蟲幾無二致,直接拿到頁面源碼悟狱,然后通過源碼解析出我們需要的數(shù)據(jù)静浴。但是這種方式存在缺陷,如果頁面數(shù)據(jù)是通過 Ajax 動態(tài)加載的挤渐,browser.page_source 獲取到的是最初返回的 HTML 頁面苹享,這個 HTML 頁面可能啥都沒有。這種情況浴麻,我們可以通過遍歷頁面元素來獲取數(shù)據(jù)得问,如下:
方式二 find & parse elements
results = browser.find_elements_by_css_selector("#content_left .c-container")
for result in results:
link = result.find_element_by_xpath(".//h3/a")
print(link.text)
這種方式需要充分利用上面介紹的 查找元素 技巧囤攀,譬如這里如果要解析百度的搜索頁面,我們可以根據(jù) #content_left .c-container 這個 CSS 選擇器定位出每一條搜索結(jié)果的元素節(jié)點(diǎn)宫纬。然后在每個元素下焚挠,通過 XPath .//h3/a 來取到搜索結(jié)果的標(biāo)題的文本。XPath 在定位一些沒有特殊標(biāo)志的元素時特別有用漓骚。
方式三 intercept & parse ajax
方式二在大多數(shù)情況下都沒問題蝌衔,但是有時候還是有局限的。譬如頁面通過 Ajax 請求動態(tài)加載认境,某些數(shù)據(jù)在 Ajax 請求的響應(yīng)中有胚委,但在頁面上并沒有體現(xiàn),而我們恰恰想要爬取 Ajax 響應(yīng)中的那些數(shù)據(jù)叉信,這種情況上面兩種方式都無法實(shí)現(xiàn)亩冬。我們能不能攔截這些 Ajax 請求,并對其響應(yīng)進(jìn)行解析呢硼身?這個問題我們放在后面一節(jié)再講硅急。
1.3 處理 Ajax 頁面
上面也提到過,如果頁面上有 Ajax 請求佳遂,使用 browser.page_source 得到的是頁面最原始的源碼营袜,無法爬到百度搜索的結(jié)果。事實(shí)上丑罪,不僅如此荚板,如果你試過上面 方式二 find & parse elements 的例子,你會發(fā)現(xiàn)用這個方式程序也爬不到搜索結(jié)果吩屹。這是因?yàn)?browser.get() 方法并不會等待頁面完全加載完畢跪另,而是等到瀏覽器的 onload 方法執(zhí)行完就返回了,這個時候頁面上的 Ajax 可能還沒加載完煤搜。如果你想確保頁面完全加載完畢免绿,當(dāng)然可以用 time.sleep() 來強(qiáng)制程序等待一段時間再處理頁面元素,但是這種方法顯然不夠優(yōu)雅擦盾〔莞辏或者自己寫一個 while 循環(huán)定時檢測某個元素是否已加載完姨丈,這個做法也沒什么問題蓄拣,但是我們最推薦的還是使用 Selenium 提供的 WebDriverWait 類银还。
WebDriverWait 類經(jīng)常和 expected_conditions 搭配使用,注意 expected_conditions 并不是一個類腐碱,而是一個文件榕暇,它下面有很多類,都是小寫字母,看起來可能有點(diǎn)奇怪彤枢,但是這些類代表了各種各樣的等待條件。譬如下面這個例子:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions
WebDriverWait(browser, 10).until(
expected_conditions.presence_of_element_located((By.ID, "kw")))
代碼的可讀性很好筒饰,基本上能看明白這是在等待一個 id 為 kw 的元素出現(xiàn)缴啡,超時時間為 10s。不過代碼看起來還是怪怪的瓷们,往往我們會給 expected_conditions 取個別名业栅,譬如 Expect,這樣代碼看起來更精簡了:
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait as Wait
from selenium.webdriver.support import expected_conditions as ExpectWait(browser, 10).until(
Expect.presence_of_element_located((By.ID, "kw"))
)
我們再以一個實(shí)際的例子來看看 expected_conditions 的強(qiáng)大之處谬晕,譬如在 途牛網(wǎng)上搜索上海到首爾的航班碘裕,這個頁面的航班結(jié)果都是以 Ajax 請求動態(tài)加載的,我們?nèi)绾蔚却桨嗳考虞d完畢之后再開始爬取我們想要的航班結(jié)果呢攒钳?通過觀察可以發(fā)現(xiàn)帮孔,在 “開始搜索”、“搜索中” 以及 “搜索結(jié)束” 這幾個階段不撑,頁面顯示的內(nèi)容存在比較明顯的差異文兢,如下圖所示:
我們就可以通過這些差異來寫等待條件。要想等到航班加載完畢焕檬,頁面上應(yīng)該會顯示 “共搜索xx個航班” 這樣的文本姆坚,而這個文本在 id 為 loadingStatus 的元素中。expected_conditions 提供的類 text_to_be_present_in_element 正滿足我們的要求实愚,可以像下面這樣:
Wait(browser, 60).until(
Expect.text_to_be_present_in_element((By.ID, "loadingStatus"), u"共搜索")
)
下面是完整的代碼兼呵,可見一個瀏覽器爬蟲跟傳統(tǒng)爬蟲比起來還是有些差異的,瀏覽器爬蟲關(guān)注點(diǎn)更多的在頁面元素的處理上腊敲。
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait as Wait
from selenium.webdriver.support import expected_conditions as Expect
browser = webdriver.Chrome(executable_path="./drivers/chromedriver.exe")
browser.get('http://www.tuniu.com/flight/intel/sha-sel')
Wait(browser, 60).until(
Expect.text_to_be_present_in_element((By.ID, "loadingStatus"), u"共搜索")
)
flight_items = browser.find_elements_by_class_name("flight-item")
for flight_item in flight_items:
flight_price_row = flight_item.find_element_by_class_name("flight-price-row")
print(flight_price_row.get_attribute("data-no"))
除了上面提到的 presence_of_element_located 和 text_to_be_present_in_element 這兩個等待條件击喂,Selenium 還提供了很多有用的條件類,參見 Selenium 的 WebDriver API兔仰。
二茫负、Selenium 如何使用代理服務(wù)器?
通過上一節(jié)的介紹乎赴,相信你也可以用 Selenium 寫一個簡單的爬蟲了忍法。雖然 Selenium 完全模擬了人工操作,給反爬增加了點(diǎn)困難榕吼,但是如果網(wǎng)站對請求頻率做限制的話饿序,Selenium 爬蟲爬快了一樣會遭遇被封殺,所以還得有代理羹蚣。
代理是爬蟲開發(fā)人員永恒的話題原探。所以接下來的問題就是怎么在 Selelium 里使用代理,防止被封殺?我在很久之前寫過幾篇關(guān)于傳統(tǒng)爬蟲的博客咽弦,其中也講到了代理的話題徒蟆,有興趣的同學(xué)可以參考一下 Java 和 HTTP 的那些事(二) 使用代理。
在寫代碼之前型型,我們要了解一點(diǎn)段审,Selenium 本身是和代理沒關(guān)系的,我們是要給瀏覽器設(shè)置代理而不是給 Selenium 設(shè)置闹蒜,所以我們首先要知道瀏覽器是怎么設(shè)置代理的寺枉。瀏覽器大抵有五種代理設(shè)置方式,第一種是直接使用系統(tǒng)代理绷落,第二種是使用瀏覽器自己的代理配置姥闪,第三種通過自動檢測網(wǎng)絡(luò)的代理配置,這種方式利用的是 WPAD 協(xié)議砌烁,讓瀏覽器自動發(fā)現(xiàn)代理服務(wù)器筐喳,第四種是使用插件控制代理配置,譬如 Chrome 瀏覽器的 Proxy SwitchyOmega 插件往弓,最后一種比較少見疏唾,是通過命令行參數(shù)指定代理。這五種方式并不是每一種瀏覽器都支持函似,而且設(shè)置方式可能也不止這五種槐脏,如果還有其他的方式,歡迎討論撇寞。
直接使用系統(tǒng)代理無需多講顿天,這在生產(chǎn)環(huán)境也是行不通的,除非寫個腳本不斷的切換系統(tǒng)代理蔑担,或者使用自動撥號的機(jī)器牌废,也未嘗不可,但這種方式不夠 programmatically啤握。而瀏覽器自己的配置一般來說基本上都會對應(yīng)命令行的某個參數(shù)開關(guān)鸟缕,譬如 Chrome 瀏覽器可以通過 --proxy-server 參數(shù)來指定代理:
chrome.exe http://www.ip138.com --proxy-server=127.0.0.1:8118
注:執(zhí)行這個命令之前,要先將現(xiàn)有的 Chrome 瀏覽器窗口全部關(guān)閉排抬,如果你的 Chrome 安裝了代理配置的插件如 SwitchyOmega懂从,還需要再加一個參數(shù) --disable-extensions 將插件禁用掉,要不然命令行參數(shù)不會生效蹲蒲。
2.1 通過命令行參數(shù)指定代理
使用 Selenium 啟動瀏覽器時番甩,也可以指定瀏覽器的啟動參數(shù)。像下面這樣即可:
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--proxy-server=127.0.0.1:8118')
browser = webdriver.Chrome(
executable_path="./drivers/chromedriver.exe",
chrome_options=chrome_options
)
browser.get('http://ip138.com')
這里的 --proxy-server 參數(shù)格式為 ip:port届搁,注意它不支持這種帶用戶名密碼的格式 username:password@ip:port缘薛,所以如果代理服務(wù)器需要認(rèn)證窍育,訪問網(wǎng)頁時就會彈出一個認(rèn)證對話框來。雖然使用 Selenium 也可以在對話框中填入用戶名和密碼宴胧,不過這種方式略顯麻煩漱抓,而且每次 Selenium 啟動瀏覽器時,都會彈出代理認(rèn)證的對話框恕齐。更好的做法是辽旋,把代理的用戶名和密碼都提前設(shè)置好,對于 Chrome 瀏覽器來說檐迟,我們可以通過它的插件來實(shí)現(xiàn)。
2.2 使用插件控制代理**
Chrome 瀏覽器下最流行的代理配置插件是 Proxy SwitchyOmega码耐,我們可以先配置好 SwitchyOmega追迟,然后 Selenium 啟動時指定加載插件,Chrome 提供了下面的命令行參數(shù)用于加載一個或多個插件:
chrome.exe http://www.ip138.com --load extension=SwitchyOmega
不過要注意的是骚腥,--load-extension 參數(shù)只能加載插件目錄敦间,而不能加載打包好的插件 *.crx 文件,我們可以把它當(dāng)成 zip 文件直接解壓縮到 SwitchyOmega 目錄即可束铭。代碼如下:
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument('--load-extension=SwitchyOmega')
browser = webdriver.Chrome(
executable_path="./drivers/chromedriver.exe",
chrome_options=chrome_options
)
browser.get('http://ip138.com')
另外廓块,Selenium 的 ChromeOptions 類還提供了一個方法 add_extension 用于直接加載未解壓的插件文件,如下:
chrome_options.add_extension('SwitchyOmega.crx')
這種做法應(yīng)該是可行的契沫,不過我沒有具體去嘗試带猴,因?yàn)檫@種做法依賴于 SwitchyOmega 的配置,如何在加載插件之前先把代理都配好懈万?如何運(yùn)行時動態(tài)的切換代理拴清?這對爬蟲來說至關(guān)重要,以后有時候再去研究吧会通。不過很顯然口予,直接使用 SwitchyOmega 插件有點(diǎn)重了,我們能不能自己寫一個簡單的插件來實(shí)現(xiàn)代理控制呢涕侈?
當(dāng)然可以沪停。而且這個插件只需要兩行代碼即可。
關(guān)于 Chrome 插件的編寫裳涛,我之前有過兩篇博客:我的第一個Chrome擴(kuò)展:Search-faster 和 我的第二個Chrome擴(kuò)展:JSONView增強(qiáng)版木张,感興趣的同學(xué)可以先看看這兩篇了解下如何寫一個 Chrome 插件。這里略過不提调违,我們這個插件需要有兩個文件窟哺,一個是 manifest.json 文件,為插件的清單文件技肩,每個插件都要有且轨,另一個是 background.js 文件浮声,它是背景腳本,類似于后臺駐留進(jìn)程旋奢,它就是代理配置插件的核心泳挥。
下面我們就來看看這兩行代碼,第一行如下:
chrome.proxy.settings.set({
value: {
mode: "fixed_servers",
rules: {
singleProxy: {
scheme: "http",
host: "127.0.0.1",
port: 8118
},
bypassList: ["foobar.com"]
}
},
scope: "regular"
}, function() {});
chrome.proxy 是用于管理 Chrome 瀏覽器的代理服務(wù)器設(shè)置的 API至朗,上面的代碼通過其提供的方法 chrome.proxy.settings.set() 設(shè)置了一個代理服務(wù)器地址屉符,mode 的值為 fixed_servers 表示根據(jù)下面的 rules 來指定某個固定的代理服務(wù)器,代理類型可以是 HTTP 或 HTTPS锹引,還可以是 SOCKS 代理矗钟。mode 的值還可以是 direct(無需代理),auto_detect(通過 WPAD 協(xié)議自動檢測代理)嫌变,pac_script(通過 PAC 腳本動態(tài)選取代理)和 system(使用系統(tǒng)代理)吨艇。關(guān)于這個 API 的詳細(xì)說明可以參看 Chrome 的 官方文檔,這里有一份 中文翻譯腾啥。
通過上面的代碼也只是設(shè)置了代理服務(wù)器的 IP 地址和端口而已东涡,用戶名和密碼還沒有設(shè)置,這和使用命令行參數(shù)沒什么區(qū)別倘待。所以還需要下面的第二行代碼:
chrome.webRequest.onAuthRequired.addListener(
function (details) {
return {
authCredentials: {
username: "username",
password: "password"
}
};
},
{ urls: ["<all_urls>"] },
[ 'blocking' ]
);
我們先看看下面這張圖疮跑,了解下 Chrome 瀏覽器接受網(wǎng)絡(luò)請求的整個流程,一個成功的請求會經(jīng)歷一系列的事件(圖片來源):
這些事件都是由 chrome.webRequest API 提供凸舵,其中的 onAuthRequired 最值得我們注意祖娘,它是用于代理身份認(rèn)證的關(guān)鍵。所有的事件都可以通過 addListener 方法注冊一個回調(diào)函數(shù)作為監(jiān)聽器贞间,當(dāng)請求需要身份認(rèn)證時贿条,回調(diào)函數(shù)返回代理的用戶名和密碼。除了回調(diào)方法增热,addListener 第二個參數(shù)用于指定該代理適用于哪些 url整以,這里的 <all_urls> 是固定的特殊語法,表示所有的 url峻仇,第三個參數(shù)字符串 blocking 表示請求將被阻塞公黑,回調(diào)函數(shù)將以同步的方式執(zhí)行。這個 API 也可以參考 Chrome 的 官方文檔摄咆,這里是 中文翻譯凡蚜。
綜上,我們就可以寫一個簡單的代理插件了吭从,甚至將插件做成動態(tài)生成的朝蜘,然后 Selenium 動態(tài)的加載生成的插件。
三涩金、Selenium 如何過濾非必要請求谱醇?
Selenium 配合代理暇仲,你的爬蟲幾乎已經(jīng)無所不能了。上面說過副渴,Selenium 爬蟲雖然好用奈附,但有個最大的特點(diǎn)是慢,有時候太慢了也不是辦法煮剧。由于每次打開一個頁面 Selenium 都要等待頁面加載完成斥滤,包括頁面上的圖片資源,JS 和 CSS 文件的加載勉盅,而且更頭疼的是佑颇,如果頁面上有一些墻外資源,比如來自 Google 或 Facebook 等站點(diǎn)的鏈接草娜,如果不使用境外代理漩符,瀏覽器要一直等到這些資源連接超時才算頁面加載完成,而這些資源對我們的爬蟲沒有任何用處驱还。
我們能不能讓 Selenium 過濾掉那些我們不需要的請求呢?
Yi Zeng 在他的一篇博客 Exclude Selenium WebDriver traffic from Google Analytics 上總結(jié)了很多種方法來過濾 Google Analytics 的請求凸克,雖然他的博客是專門針對 Google Analytics 的請求议蟆,但其中有很多思路還是很值得我們借鑒的。其中有下面的幾種解決方案:
通過修改 hosts 文件萎战,將 google.com咐容、facebook.com 等重定向到本地,這種方法需要修改系統(tǒng)文件蚂维,不方便程序的部署戳粒,而且不能動態(tài)的添加要過濾的請求;
禁用瀏覽器的 JavaScript 功能虫啥,譬如 Chrome 支持參數(shù) --disable-javascript 來禁用 JavaScript蔚约,但這種方法有很大的局限性,圖片和 CSS 資源還是沒有過濾掉涂籽,而且頁面上少了 JavaScript苹祟,可能站點(diǎn)的很多功能無法使用了;
使用瀏覽器插件评雌,Yi Zeng 的博客中只提到了 Google-Analytics-Opt-out-Add-on 插件用于禁用 Google Analytics树枫,實(shí)際上我們很容易想到 AdBlock 插件,這個插件用來過濾頁面上的一些廣告景东,這和我們想要的效果有些類似砂轻。我們可以自己寫一個插件,攔截不需要的請求斤吐,相信通過上一節(jié)的介紹搔涝,也可以做出來厨喂。
使用代理服務(wù)器 BrowserMob Proxy,通過代理服務(wù)器來攔截不需要的請求体谒,除了 BrowserMob Proxy杯聚,還有很多代理軟件也具有攔截請求的功能,譬如 Fiddler 的 AutoResponder 或者 通過 whistle 設(shè)置 Rules 都可以攔截或修改請求抒痒;
這里雖然方法有很多幌绍,但我只推薦最后一種:使用代理服務(wù)器 BrowserMob Proxy,BrowserMob Proxy 簡稱 BMP故响,可以這么說傀广,BMP 絕對是為 Selenium 為生的,Selenium + BMP 的完美搭配彩届,可以實(shí)現(xiàn)很多你絕對想象不出來的功能伪冰。
我之所以推薦 BMP,是由于 BMP 的理念非常巧妙樟蠕,和傳統(tǒng)的代理服務(wù)器不一樣贮聂,它并不是一個簡單的代理,而是一個 RESTful 的代理服務(wù)寨辩,通過 BMP 提供的一套 RESTful 接口吓懈,你可以創(chuàng)建或移除代理,設(shè)置黑名單或白名單靡狞,設(shè)置過濾器規(guī)則等等耻警,可以說它是一個可編程式的代理服務(wù)器。BMP 是使用 Java 語言編寫的甸怕,它前后經(jīng)歷了兩個大版本的迭代甘穿,其核心也是從最初的 Jetty 演變?yōu)?LittleProxy,使得它更小巧和穩(wěn)定梢杭,你可以從 這里下載 BMP 的可執(zhí)行文件温兼,在 Windows 系統(tǒng)上,我們直接雙擊執(zhí)行 bin 目錄下的 browsermob-proxy.bat 文件武契。
BMP 啟動后妨托,默認(rèn)在 8080 端口創(chuàng)建代理服務(wù),此時 BMP 還不是一個代理服務(wù)器吝羞,需要你先創(chuàng)建一個代理:
curl -X POST http://localhost:8080/proxy
/proxy 接口發(fā)送 POST 請求兰伤,可以創(chuàng)建一個代理服務(wù)器。此時钧排,我們在瀏覽器訪問 http://localhost:8080/proxy 這個地址敦腔,可以看到我們已經(jīng)有了一個代理服務(wù)器,端口號為 8081恨溜,現(xiàn)在我們就可以使用 127.0.0.1:8081 這個代理了符衔。
接下來我們要把 Google 的請求攔截掉找前,BMP 提供了一個 /proxy/[port]/blacklist 接口可以使用,如下:
curl -X PUT -d 'regex=.google.&status=404' http://localhost:8080/proxy/8081/blacklist
這樣所有匹配到 .google. 正則的 url判族,都將直接返回 404 Not Found躺盛。
知道了 BMP 怎么用,再接下來形帮,就是編寫代碼了槽惫。當(dāng)然我們可以自己寫代碼來調(diào)用 BMP 提供的 RESTful 接口,不過俗話說得好辩撑,前人栽樹界斜,后人乘涼,早就有人將 BMP 的接口封裝好給我們直接使用合冀,譬如 browsermob-proxy-py 是 Python 的實(shí)現(xiàn)各薇,我們就來試試它。
from selenium import webdriver
from browsermobproxy import Server
server = Server("D:/browsermob-proxy-2.1.4/bin/browsermob-proxy")
server.start()
proxy = server.create_proxy()
proxy.blacklist(".google.", 404)
proxy.blacklist(".yahoo.", 404)
proxy.blacklist(".facebook.", 404)
proxy.blacklist(".twitter.", 404)
chrome_options = webdriver.ChromeOptions()
chrome_options.add_argument("--proxy-server={0}".format(proxy.proxy))
browser = webdriver.Chrome(
executable_path="./drivers/chromedriver.exe",
chrome_options = chrome_options
)
browser.get('http://www.flypeach.com/pc/hk')
server.stop()
browser.quit()
關(guān)鍵代碼在前面幾句君躺,首先創(chuàng)建代理峭判,再通過 proxy.blacklist() 將 google、yahoo棕叫、facebook朝抖、twitter 的資源攔截掉。后面的代碼和前一節(jié)的代理設(shè)置完全一樣谍珊。執(zhí)行程序,體會一下急侥,現(xiàn)在這個頁面的打開速度快了多少砌滞?
BMP 不僅可以攔截請求,也可以修改請求坏怪,這對爬蟲來說可能意義不大贝润,但在自動化測試時,可以通過它偽造測試數(shù)據(jù)還是很有意義的铝宵。它提供了兩個接口
/proxy/[port]/filter/request 和 /proxy/[port]/filter/response 用于修改 HTTP 的請求和響應(yīng)打掘,具體的用法可以參考 官網(wǎng)的文檔,此處略過鹏秋。
proxy.request_interceptor(
'''
request.headers().remove('User-Agent');
request.headers().add('User-Agent', 'My-Custom-User-Agent-String 1.0');
'''
)
proxy.response_interceptor(
'''
if (messageInfo.getOriginalUrl().contains("remote/searchFlights")) {
contents.setTextContents('Hello World');
}
'''
)
四尊蚁、Selenium 如何爬取 Ajax 請求?
到這里侣夷,問題變得越來越有意思了横朋。而且我們發(fā)現(xiàn),用 Selenium 做爬蟲百拓,中途確實(shí)會遇到各種各樣的問題琴锭,但隨著問題的發(fā)現(xiàn)到解決晰甚,我們花在 Selenium 上面的時間越來越少了,更多的是在研究其他的東西决帖,如瀏覽器的特性厕九,瀏覽器插件的編寫,可編程式的代理服務(wù)器地回,以此來輔助 Selenium 做的更好扁远。
還記得前面提到的一個問題嗎?如果要爬取的內(nèi)容在 Ajax 請求的響應(yīng)中落君,而在頁面上并沒有體現(xiàn)穿香,這種情況該如何爬取呢?我們可以直接爬 Ajax 請求嗎绎速?事實(shí)上皮获,我們很難做到,但不是做不到纹冤。
通過上一節(jié)對 BMP 的介紹洒宝,我們了解到 BMP 可以攔截并修改請求的報文,我們可以進(jìn)一步猜想萌京,既然它可以修改報文雁歌,那肯定也可以拿到報文,只是這個報文我們的程序該如何得到知残?上一節(jié)我們提到了兩個接口 /proxy/[port]/filter/request 和 /proxy/[port]/filter/response靠瞎,它們可以接受一段 JS 代碼來修改 HTTP 的請求和響應(yīng),其中我們可以通過 contents.getTextContents() 來訪問響應(yīng)的報文求妹,只是這段代碼運(yùn)行在遠(yuǎn)程服務(wù)器上乏盐,和我們的代碼在兩個完全不同的世界里,如何把它傳給我們呢制恍?而且父能,這段 JS 代碼的限制非常嚴(yán)格,我們想通過這個地方拿到這個報文幾乎是不可能的净神。
但何吝,路總是有的。
我們回過頭來看 BMP 的文檔鹃唯,發(fā)現(xiàn) BMP 提供了兩種模式供我們使用:獨(dú)立模式(Standalone)和 嵌入模式(Embedded Mode)爱榕。獨(dú)立模式就是像上面那樣,BMP 作為一個獨(dú)立的應(yīng)用服務(wù)坡慌,我們的程序通過 RESTful 接口與其交互呆细。而嵌入模式則不需要下載 BMP 可執(zhí)行文件,直接通過包的形式引入到我們的程序中來⌒跻可惜的是趴酣,嵌入模式只支持 Java 語言,但這也聊勝于無坑夯,于是我使用 Java 寫了個測試程序嘗試了一把岖寞。
首先引入 browsermob-core 包,
<dependency>
<groupId>net.lightbody.bmp</groupId>
<artifactId>browsermob-core</artifactId>
<version>2.1.5</version>
</dependency>
然后參考官網(wǎng)文檔寫下下面的代碼(完整代碼見 這里)柜蜈,這里就可以看到嵌入模式的好處了仗谆,用于 BMP 攔截處理的代碼和我們自己的代碼處于同一個環(huán)境下,而且 Java 語言具有閉包的特性淑履,我們可以很簡單的取到 Ajax 請求的響應(yīng)報文:
BrowserMobProxyproxyServer=newBrowserMobProxyServer();
proxyServer.start(0);
proxyServer.addRequestFilter((request,contents,messageInfo)->{
System.out.println("請求開始:"+messageInfo.getOriginalUrl());
returnnull;
});
StringajaxContent=null;
proxyServer.addResponseFilter((response,contents,messageInfo)->{
System.out.println("請求結(jié)束:"+messageInfo.getOriginalUrl());
if(messageInfo.getOriginalUrl().contains("ajax")){
ajaxContent=contents.getTextContents();
}
});
如果你是個 .Net guy隶垮,可以使用 Fiddler 提供的 FiddlerCore,F(xiàn)iddlerCore 就相當(dāng)于 BMP 的嵌入模式秘噪,和這里的方法類似狸吞。這里有一篇很好的文章講解了如何使用 .Net 和 FiddlerCore 攔截請求。
既然在 Java 環(huán)境下解決了這個問題指煎,那么 Python 應(yīng)該也沒問題蹋偏,但是 BMP 的嵌入模式并不支持 Python 怎么辦呢?于是我一直在尋找一款基于 Python 的能替代 BMP 的工具至壤,可惜一直不如愿威始,未能找到滿意的。到最后像街,我?guī)缀跻陆Y(jié)論:Python + Selenium 很難實(shí)現(xiàn) Ajax 請求的爬取黎棠。
天無絕人之路,直到我遇到了 har镰绎。
有一天我靜下心來把 BMP 的文檔翻來覆去看了好幾遍脓斩,之前我看文檔的習(xí)慣都是用時再查,但這次把 BMP 的文檔從頭到尾看了幾遍跟狱,也是希望能從中尋找點(diǎn)蛛絲馬跡。而事實(shí)上户魏,還真被我發(fā)現(xiàn)了點(diǎn)什么驶臊。因?yàn)?Python 只能通過 RESTful 接口與 BMP 交互,那么每一個接口我都不能放過叼丑,有一個接口引起了我的注意:/proxy/[port]/har关翎。
這個接口雖然之前也掃過幾眼,但當(dāng)時并不知道這個 har 是什么意思鸠信,所以都是一掠而過纵寝。但那天心血來潮,特意去查了一下 har 的資料星立,才發(fā)現(xiàn)這是一種特殊的 JSON 格式的歸檔文件爽茴。HAR 全稱 HTTP Archive Format葬凳,通常用于記錄瀏覽器訪問網(wǎng)站的所有交互請求,絕大多數(shù)瀏覽器和 Web 代理都支持這種格式的歸檔文件室奏,用于分析 HTTP 請求火焰,因?yàn)閺V泛的應(yīng)用,W3C 甚至還提出 HAR 的規(guī)范胧沫,目前還在草稿階段昌简。
/proxy/[port]/har 接口用于創(chuàng)建一份新的 har 文件,Selenium 啟動瀏覽器后所有的請求都將被記錄到這份 har 文件中绒怨,然后通過 GET 請求纯赎,可以獲取到這份 har 文件的內(nèi)容(JSON 格式)。har 文件的內(nèi)容類似于下面這樣:
{
"log": {
"version" : "1.2",
"creator" : {},
"browser" : {},
"pages": [],
"entries": [],
"comment": ""
}
}
其中 entries 數(shù)組包含了所有 HTTP 請求的列表南蹂,默認(rèn)情況下 BMP 創(chuàng)建的 har 文件并不包含請求的響應(yīng)內(nèi)容犬金,我們可以通過 captureContent 參數(shù)來讓 BMP 記錄響應(yīng)內(nèi)容:
curl -X PUT -d 'captureContent=true' http://localhost:8080/proxy/8081/har
萬事俱備,只欠東風(fēng)碎紊。我們開始寫代碼佑附,首先通過 proxy.new_har() 創(chuàng)建一份 har 文件:
proxy.new_har(options={'captureContent': True })
然后啟動瀏覽器,訪問要爬取的頁面仗考,等待頁面加載結(jié)束音同,這時我們就可以通過 proxy.har 來訪問 har 文件中的請求報文了(完整代碼在 這里):
for entry in proxy.har['log']['entries']:
if 'remote/searchFlights' in entry['request']['url']:
result = json.loads(entry['response']['content']['text'])
for key, item in result['data']['flightInfo'].items():
print(key)
總結(jié)
這篇博客總結(jié)了 Selenium 的一些基礎(chǔ)語法,并嘗試使用 Python + Selenium 開發(fā)瀏覽器爬蟲秃嗜。本文還分享了我在實(shí)際開發(fā)過程中遇到的幾個常見問題权均,并提供了一種或多種解決方案,包括代理的使用锅锨,攔截瀏覽器請求叽赊,爬取 Ajax 請求等等。實(shí)踐出真知必搞,通過一系列問題的提出必指,到研究,到解決恕洲,我學(xué)習(xí)到了非常多的東西塔橡。不僅意識到知識廣度的重要性,而且更重要的是知識的聚合和熔煉霜第。我一直認(rèn)為知識的廣度比深度更重要葛家,只有你懂的越多,你才有可能接觸更多的東西泌类,你的思路才更放得開癞谒;深度固然也重要,但往往會讓人局限于自己的漩渦之中。但知識的廣度不是天馬行空弹砚,需要不斷的總結(jié)提煉双仍,融會貫通,形成自己的知識體系迅栅,這樣才不至于被繁多的知識點(diǎn)所困擾殊校。
另外,我也意識到閱讀項(xiàng)目文檔的重要性读存,心平氣和的將項(xiàng)目文檔從頭到尾閱讀一遍为流,遇到不懂的,就去查找資料让簿,而不是只挑自己知道或感興趣的敬察,這樣會得到意想不到的收獲。