使用Python + Selenium打造瀏覽器爬蟲


   Selenium  是一款強大的基於瀏覽器的開源自動化測試工具,最初由 Jason Huggins 於 2004 年在 ThoughtWorks 發起,它提供了一套簡單易用的 API,模擬瀏覽器的各種操作,方便各種 Web 應用的自動化測試。它的取名很有意思,因為當時最流行的一款自動化測試工具叫做QTP,是由 Mercury 公司開發的商業應用。Mercury 是化學元素汞,而 Selenium 是化學元素硒,汞有劇毒,而硒可以解汞毒,它對汞有拮抗作用。
  Selenium 的核心組件叫做 Selenium-RC(Remote Control),簡單來說它是一個代理服務器,瀏覽器啟動時通過將它設置為代理,它可以修改請求響應報文並向其中注入 Javascript,通過注入的 JS 可以模擬瀏覽器操作,從而實現自動化測試。但是注入 JS 的方法存在很多限制,譬如無法模擬鍵盤和鼠標事件,處理不了對話框,不能繞過 JavaScript 沙箱等等。就在這個時候,於 2006 年左右, Google  的工程師 Simon Stewart 發起了 WebDriver 項目,WebDriver 通過調用瀏覽器提供的原生自動化 API 來驅動瀏覽器,解決了 Selenium 的很多疑難雜症。不過 WebDriver 也有它不足的地方,它不能支持所有的瀏覽器,需要針對不同的瀏覽器來開發不同的 WebDriver,因為不同的瀏覽器提供的 API 也不盡相同,好在經過不斷的發展,各種主流瀏覽器都已經有相應的 WebDriver 了。最終 Selenium 和 WebDriver 合並在一起,這就是 Selenium 2.0,有的地方也直接把它稱作 WebDriver。Selenium 目前最新的版本已經是 3.9 了,WebDriver 仍然是 Selenium 的核心。
   一、Selenium 爬蟲入門
  Selenium 的初衷是打造一款優秀的自動化測試工具,但是慢慢的人們就發現,Selenium 的自動化用來做爬蟲正合適。我們知道,傳統的爬蟲通過直接模擬 HTTP 請求來爬取站點信息,由於這種方式和瀏覽器訪問差異比較明顯,很多站點都采取了一些反爬的手段,而 Selenium 是通過模擬瀏覽器來爬取信息,其行為和用戶幾乎一樣,反爬策略也很難區分出請求到底是來自 Selenium 還是真實用戶。而且通過 Selenium 來做爬蟲,不用去分析每個請求的具體參數,比起傳統的爬蟲開發起來更容易。Selenium 爬蟲唯一的不足是慢,如果你對爬蟲的速度沒有要求,那使用 Selenium 是個非常不錯的選擇。Selenium 提供了多種語言的支持( Java、.NET、 PythonRuby  等),不論你是用哪種語言開發爬蟲,Selenium 都適合你。
  我們第一節先通過 Python 學習 Selenium 的基礎知識,后面幾節再介紹我在使用 Selenium 開發瀏覽器爬蟲時遇到的一些問題和解決方法。
   1.1 Hello World
  一個最簡單的 Selenium 程序像下面這樣:
  from selenium import webdriver
  browser = webdriver.Chrome()
  browser.get('http://www.baidu.com/')
  這段代碼理論上會打開 Chrome 瀏覽器,並訪問 百度首頁。但事實上,如果你第一次使用 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 環境變量中。chromedriver 文件可以通過錯誤提示中的地址下載。不過在生產環境,我並不推薦這樣的做法,使用下面的方法可以手動指定 chromedriver 文件的位置:
  from selenium import webdriver
  browser = webdriver.Chrome(executable_path="./drivers/chromedriver.exe")
  browser.get('http://www.baidu.com/')
  這里給出的例子是 Chrome 瀏覽器,Selenium 同樣可以驅動 Firefox、IE、Safari 等。這里列出了幾個流行瀏覽器webdriver的下載地址。Selenium 的官網也提供了大多數瀏覽器驅動的下載信息,你可以參考 Third Party Drivers, Bindings, and Plugins 一節。
   1.2 輸入和輸出
  通過上面的一節,我們已經可以自動的通過瀏覽器打開某個頁面了,作為爬蟲,我們還需要和頁面進行更多的交互,歸結起來可以分為兩大類:輸入和輸出。
  輸入指的是用戶對瀏覽器的所有操作,譬如上面的直接訪問某個頁面也是一種輸入,或者在輸入框填寫,下拉列表選擇,點擊某個按鈕等等;
  輸出指的是根據輸入操作,對瀏覽器所產生的數據進行解析,得到我們需要的數據;這里 瀏覽器所產生的數據 不僅包括可見的內容,如頁面上顯示的信息,也還包括不可見的內容,如 HTML 源碼,甚至瀏覽器所發生的所有 HTTP 請求報文。
  下面還是以百度為例,介紹幾種常見的輸入輸出方式。
   1.2.1 輸入
  我們打開百度進行搜索,如果是人工操作,一般有兩種方式:第一種,在輸入框中輸入搜索文字,然后回車;第二種,在輸入框中輸入搜索文字,然后點擊搜索按鈕。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 方法經常用到,它根據元素的 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 中文翻譯文檔中的 查找元素 一節。
  通過 find_element_by_id 方法拿到元素之后,就可以對這個元素進行操作,也可以獲取元素的屬性或者它的文本。kw 這個元素是一個 input 輸入框,可以通過 send_keys 來模擬按鍵輸入。不僅可以模擬輸入可見字符,也可以模擬一些特殊按鍵,譬如回車 Keys.RETURN,可模擬的所有特殊鍵可以參考 這里。
  針對不同的元素,有不同的操作,譬如按鈕,可以通過 click 方法來模擬點擊,如下。
   方式二 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 方法不僅可以直接應用在 form 元素上,也可以應用在 form 元素里的所有子元素上,submit 會自動查找離該元素最近的父 form 元素然后提交。這種方式是程序特有的,有點類似於直接在 Console 里執行 $('form').submit() JavaScript 代碼。由此,我們引出第四種輸入方法,也是最最強大的輸入方法,可以說幾乎是無所不能,直接在瀏覽器里執行 JavaScript 代碼:
   方式四 execute javascript
  browser.execute_script(
  '''
  var kw = document.getElementById('kw');
  var su = document.getElementById('su');
  kw.value = 'Selenium';
  su.click();
  '''
  )
  這和方式二非常相似,但是要注意的是,方式四是完全通過 JavaScript 來操作頁面,所以靈活性是無限大的,幾乎可以做任何操作。除了這些輸入方式,當然還有其他方式,譬如,先在輸入框輸入搜索文字,然后按 Tab 鍵將焦點切換到提交按鈕,然后按回車,原理都是大同小異,此處不再贅述,你可以自己寫程序試一試。
  另外,對於 select 元素,Selenium 單獨提供了一個類 selenium.webdriver.support.select.Select 可以方便元素的選取。其他類型的元素,都可以通過上述四種方式來處理。
   1.2.2 輸出
  有輸入就有輸出,當點擊搜索按鈕之后,如果我們要爬取頁面上的搜索結果,我們有幾種不同的方法。
   方式一 parse page_source
  html = browser.page_source
  results = parse_html(html)
  第一種方式最原始,和傳統爬蟲幾無二致,直接拿到頁面源碼,然后通過源碼解析出我們需要的數據。但是這種方式存在缺陷,如果頁面數據是通過 Ajax 動態加載的,browser.page_source 獲取到的是最初返回的 HTML 頁面,這個 HTML 頁面可能啥都沒有。這種情況,我們可以通過遍歷頁面元素來獲取數據,如下:
   方式二 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)
  這種方式需要充分利用上面介紹的 查找元素 技巧,譬如這里如果要解析百度的搜索頁面,我們可以根據 #content_left .c-container 這個 CSS 選擇器定位出每一條搜索結果的元素節點。然后在每個元素下,通過 XPath .//h3/a 來取到搜索結果的標題的文本。XPath 在定位一些沒有特殊標志的元素時特別有用。
   方式三 intercept & parse ajax
  方式二在大多數情況下都沒問題,但是有時候還是有局限的。譬如頁面通過 Ajax 請求動態加載,某些數據在 Ajax 請求的響應中有,但在頁面上並沒有體現,而我們恰恰想要爬取 Ajax 響應中的那些數據,這種情況上面兩種方式都無法實現。我們能不能攔截這些 Ajax 請求,並對其響應進行解析呢?這個問題我們放在后面一節再講。
   1.3 處理 Ajax 頁面
  上面也提到過,如果頁面上有 Ajax 請求,使用 browser.page_source 得到的是頁面最原始的源碼,無法爬到百度搜索的結果。事實上,不僅如此,如果你試過上面 方式二 find & parse elements 的例子,你會發現用這個方式程序也爬不到搜索結果。這是因為 browser.get() 方法並不會等待頁面完全加載完畢,而是等到瀏覽器的 onload 方法執行完就返回了,這個時候頁面上的 Ajax 可能還沒加載完。如果你想確保頁面完全加載完畢,當然可以用 time.sleep() 來強制程序等待一段時間再處理頁面元素,但是這種方法顯然不夠優雅。或者自己寫一個 while 循環定時檢測某個元素是否已加載完,這個做法也沒什么問題,但是我們最推薦的還是使用 Selenium 提供的 WebDriverWait 類。
  WebDriverWait 類經常和 expected_conditions 搭配使用,注意 expected_conditions 並不是一個類,而是一個文件,它下面有很多類,都是小寫字母,看起來可能有點奇怪,但是這些類代表了各種各樣的等待條件。譬如下面這個例子:
  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 的元素出現,超時時間為 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 Expect
  Wait(browser, 10).until(
  Expect.presence_of_element_located((By.ID, "kw"))
  )
  我們再以一個實際的例子來看看 expected_conditions 的強大之處,譬如在 途牛網上搜索上海到首爾的航班,這個頁面的航班結果都是以 Ajax 請求動態加載的,我們如何等待航班全部加載完畢之后再開始爬取我們想要的航班結果呢?通過觀察可以發現,在 “開始搜索”、“搜索中” 以及 “搜索結束” 這幾個階段,頁面顯示的內容存在比較明顯的差異,如下圖所示:
 
  我們就可以通過這些差異來寫等待條件。要想等到航班加載完畢,頁面上應該會顯示 “共搜索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"共搜索")
  )
  下面是完整的代碼,可見一個瀏覽器爬蟲跟傳統爬蟲比起來還是有些差異的,瀏覽器爬蟲關注點更多的在頁面元素的處理上。
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 如何使用代理服務器?
  通過上一節的介紹,相信你也可以用 Selenium 寫一個簡單的爬蟲了。雖然 Selenium 完全模擬了人工操作,給反爬增加了點困難,但是如果網站對請求頻率做限制的話,Selenium 爬蟲爬快了一樣會遭遇被封殺,所以還得有代理。
  代理是爬蟲開發人員永恆的話題。所以接下來的問題就是怎么在 Selelium 里使用代理,防止被封殺?我在很久之前寫過幾篇關於傳統爬蟲的博客,其中也講到了代理的話題,有興趣的同學可以參考一下 Java 和 HTTP 的那些事(二) 使用代理。
  在寫代碼之前,我們要了解一點,Selenium 本身是和代理沒關系的,我們是要給瀏覽器設置代理而不是給 Selenium 設置,所以我們首先要知道瀏覽器是怎么設置代理的。瀏覽器大抵有五種代理設置方式,第一種是直接使用系統代理,第二種是使用瀏覽器自己的代理配置,第三種通過自動檢測網絡的代理配置,這種方式利用的是 WPAD 協議,讓瀏覽器自動發現代理服務器,第四種是使用插件控制代理配置,譬如 Chrome 瀏覽器的 Proxy SwitchyOmega 插件,最后一種比較少見,是通過命令行參數指定代理。這五種方式並不是每一種瀏覽器都支持,而且設置方式可能也不止這五種,如果還有其他的方式,歡迎討論。
  直接使用系統代理無需多講,這在生產環境也是行不通的,除非寫個腳本不斷的切換系統代理,或者使用自動撥號的機器,也未嘗不可,但這種方式不夠 programmatically。而瀏覽器自己的配置一般來說基本上都會對應命令行的某個參數開關,譬如 Chrome 瀏覽器可以通過 --proxy-server 參數來指定代理:
  chrome.exe http://www.ip138.com --proxy-server=127.0.0.1:8118
  注:執行這個命令之前,要先將現有的 Chrome 瀏覽器窗口全部關閉,如果你的 Chrome 安裝了代理配置的插件如 SwitchyOmega,還需要再加一個參數 --disable-extensions 將插件禁用掉,要不然命令行參數不會生效。
   2.1 通過命令行參數指定代理
  使用 Selenium 啟動瀏覽器時,也可以指定瀏覽器的啟動參數。像下面這樣即可:
  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 參數格式為 ip:port,注意它不支持這種帶用戶名密碼的格式 username:password@ip:port,所以如果代理服務器需要認證,訪問網頁時就會彈出一個認證對話框來。雖然使用 Selenium 也可以在對話框中填入用戶名和密碼,不過這種方式略顯麻煩,而且每次 Selenium 啟動瀏覽器時,都會彈出代理認證的對話框。更好的做法是,把代理的用戶名和密碼都提前設置好,對於 Chrome 瀏覽器來說,我們可以通過它的插件來實現。
   2.2 使用插件控制代理
  Chrome 瀏覽器下最流行的代理配置插件是 Proxy SwitchyOmega,我們可以先配置好 SwitchyOmega,然后 Selenium 啟動時指定加載插件,Chrome 提供了下面的命令行參數用於加載一個或多個插件:
  chrome.exe http://www.ip138.com --load-extension=SwitchyOmega
  不過要注意的是,--load-extension 參數只能加載插件目錄,而不能加載打包好的插件 *.crx 文件,我們可以把它當成 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')
  這種做法應該是可行的,不過我沒有具體去嘗試,因為這種做法依賴於 SwitchyOmega 的配置,如何在加載插件之前先把代理都配好?如何運行時動態的切換代理?這對爬蟲來說至關重要,以后有時候再去研究吧。不過很顯然,直接使用 SwitchyOmega 插件有點重了,我們能不能自己寫一個簡單的插件來實現代理控制呢?
  當然可以。而且這個插件只需要兩行代碼即可。
  關於 Chrome 插件的編寫,我之前有過兩篇博客:我的第一個Chrome擴展:Search-faster 和 我的第二個Chrome擴展:JSONView增強版,感興趣的同學可以先看看這兩篇了解下如何寫一個 Chrome 插件。這里略過不提,我們這個插件需要有兩個文件,一個是 manifest.json 文件,為插件的清單文件,每個插件都要有,另一個是 background.js 文件,它是背景腳本,類似於后台駐留進程,它就是代理配置插件的核心。
  下面我們就來看看這兩行代碼,第一行如下:
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 瀏覽器的代理服務器設置的 API,上面的代碼通過其提供的方法 chrome.proxy.settings.set() 設置了一個代理服務器地址,mode 的值為 fixed_servers 表示根據下面的 rules 來指定某個固定的代理服務器,代理類型可以是 HTTP 或 HTTPS,還可以是 SOCKS 代理。mode 的值還可以是 direct(無需代理),auto_detect(通過 WPAD 協議自動檢測代理),pac_script(通過 PAC 腳本動態選取代理)和 system(使用系統代理)。關於這個 API 的詳細說明可以參看 Chrome 的 官方文檔,這里有一份 中文翻譯。
  通過上面的代碼也只是設置了代理服務器的 IP 地址和端口而已,用戶名和密碼還沒有設置,這和使用命令行參數沒什么區別。所以還需要下面的第二行代碼:
chrome.webRequest.onAuthRequired.addListener(
function (details) {
return {
authCredentials: {
username: "username",
password: "password"
}
};
},
{ urls: ["<all_urls>"] },
[ 'blocking' ]
);
  我們先看看下面這張圖,了解下 Chrome 瀏覽器接受網絡請求的整個流程,一個成功的請求會經歷一系列的事件(圖片來源):
  
  這些事件都是由 chrome.webRequest API 提供,其中的 onAuthRequired 最值得我們注意,它是用於代理身份認證的關鍵。所有的事件都可以通過 addListener 方法注冊一個回調函數作為監聽器,當請求需要身份認證時,回調函數返回代理的用戶名和密碼。除了回調方法,addListener 第二個參數用於指定該代理適用於哪些 url,這里的 <all_urls> 是固定的特殊語法,表示所有的 url,第三個參數字符串 blocking 表示請求將被阻塞,回調函數將以同步的方式執行。這個 API 也可以參考 Chrome 的 官方文檔,這里是 中文翻譯。
  綜上,我們就可以寫一個簡單的代理插件了,甚至將插件做成動態生成的,然后 Selenium 動態的加載生成的插件。完整的源碼在 這里。
   三、Selenium 如何過濾非必要請求?
  Selenium 配合代理,你的爬蟲幾乎已經無所不能了。上面說過,Selenium 爬蟲雖然好用,但有個最大的特點是慢,有時候太慢了也不是辦法。由於每次打開一個頁面 Selenium 都要等待頁面加載完成,包括頁面上的圖片資源,JS 和 CSS 文件的加載,而且更頭疼的是,如果頁面上有一些牆外資源,比如來自 Google 或 Facebook 等站點的鏈接,如果不使用境外代理,瀏覽器要一直等到這些資源連接超時才算頁面加載完成,而這些資源對我們的爬蟲沒有任何用處。
  我們能不能讓 Selenium 過濾掉那些我們不需要的請求呢?
  Yi Zeng 在他的一篇博客 Exclude Selenium WebDriver traffic from Google Analytics 上總結了很多種方法來過濾 Google Analytics 的請求,雖然他的博客是專門針對 Google Analytics 的請求,但其中有很多思路還是很值得我們借鑒的。其中有下面的幾種解決方案:
  通過修改 hosts 文件,將 google.com、facebook.com 等重定向到本地,這種方法需要修改系統文件,不方便程序的部署,而且不能動態的添加要過濾的請求;
  禁用瀏覽器的 JavaScript 功能,譬如 Chrome 支持參數 --disable-javascript 來禁用 JavaScript,但這種方法有很大的局限性,圖片和 CSS 資源還是沒有過濾掉,而且頁面上少了 JavaScript,可能站點的很多功能無法使用了;
  使用瀏覽器插件,Yi Zeng 的博客中只提到了 Google-Analytics-Opt-out-Add-on 插件用於禁用 Google Analytics,實際上我們很容易想到 AdBlock 插件,這個插件用來過濾頁面上的一些廣告,這和我們想要的效果有些類似。我們可以自己寫一個插件,攔截不需要的請求,相信通過上一節的介紹,也可以做出來。
  使用代理服務器 BrowserMob Proxy,通過代理服務器來攔截不需要的請求,除了 BrowserMob Proxy,還有很多代理軟件也具有攔截請求的功能,譬如 Fiddler 的 AutoResponder 或者 通過 whistle 設置 Rules 都可以攔截或修改請求;
  這里雖然方法有很多,但我只推薦最后一種:使用代理服務器 BrowserMob Proxy,BrowserMob Proxy 簡稱 BMP,可以這么說,BMP 絕對是為 Selenium 為生的,Selenium + BMP 的完美搭配,可以實現很多你絕對想象不出來的功能。
  我之所以推薦 BMP,是由於 BMP 的理念非常巧妙,和傳統的代理服務器不一樣,它並不是一個簡單的代理,而是一個 RESTful 的代理服務,通過 BMP 提供的一套 RESTful 接口,你可以創建或移除代理,設置黑名單或白名單,設置過濾器規則等等,可以說它是一個可編程式的代理服務器。BMP 是使用 Java 語言編寫的,它前后經歷了兩個大版本的迭代,其核心也是從最初的 Jetty 演變為 LittleProxy,使得它更小巧和穩定,你可以從 這里下載 BMP 的可執行文件,在 Windows 系統上,我們直接雙擊執行 bin 目錄下的 browsermob-proxy.bat 文件。
  BMP 啟動后,默認在 8080 端口創建代理服務,此時 BMP 還不是一個代理服務器,需要你先創建一個代理:
  curl -X POST http://localhost:8080/proxy
  向 /proxy 接口發送 POST 請求,可以創建一個代理服務器。此時,我們在瀏覽器訪問 http://localhost:8080/proxy 這個地址,可以看到我們已經有了一個代理服務器,端口號為 8081,現在我們就可以使用 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 怎么用,再接下來,就是編寫代碼了。當然我們可以自己寫代碼來調用 BMP 提供的 RESTful 接口,不過俗話說得好,前人栽樹,后人乘涼,早就有人將 BMP 的接口封裝好給我們直接使用,譬如 browsermob-proxy-py 是 Python 的實現,我們就來試試它。
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()
  關鍵代碼在前面幾句,首先創建代理,再通過 proxy.blacklist() 將 google、yahoo、facebook、twitter 的資源攔截掉。后面的代碼和前一節的代理設置完全一樣。執行程序,體會一下,現在這個頁面的打開速度快了多少?
  BMP 不僅可以攔截請求,也可以修改請求,這對爬蟲來說可能意義不大,但在自動化測試時,可以通過它偽造測試數據還是很有意義的。它提供了兩個接口 
/proxy/[port]/filter/request 和 /proxy/[port]/filter/response 用於修改 HTTP 的請求和響應,具體的用法可以參考 官網的文檔,此處略過。
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 請求?
  到這里,問題變得越來越有意思了。而且我們發現,用 Selenium 做爬蟲,中途確實會遇到各種各樣的問題,但隨着問題的發現到解決,我們花在 Selenium 上面的時間越來越少了,更多的是在研究其他的東西,如瀏覽器的特性,瀏覽器插件的編寫,可編程式的代理服務器,以此來輔助 Selenium 做的更好。
  還記得前面提到的一個問題嗎?如果要爬取的內容在 Ajax 請求的響應中,而在頁面上並沒有體現,這種情況該如何爬取呢?我們可以直接爬 Ajax 請求嗎?事實上,我們很難做到,但不是做不到。
  通過上一節對 BMP 的介紹,我們了解到 BMP 可以攔截並修改請求的報文,我們可以進一步猜想,既然它可以修改報文,那肯定也可以拿到報文,只是這個報文我們的程序該如何得到?上一節我們提到了兩個接口 /proxy/[port]/filter/request 和 /proxy/[port]/filter/response,它們可以接受一段 JS 代碼來修改 HTTP 的請求和響應,其中我們可以通過 contents.getTextContents() 來訪問響應的報文,只是這段代碼運行在遠程服務器上,和我們的代碼在兩個完全不同的世界里,如何把它傳給我們呢?而且,這段 JS 代碼的限制非常嚴格,我們想通過這個地方拿到這個報文幾乎是不可能的。
  但,路總是有的。
  我們回過頭來看 BMP 的文檔,發現 BMP 提供了兩種模式供我們使用:獨立模式(Standalone)和 嵌入模式(Embedded Mode)。獨立模式就是像上面那樣,BMP 作為一個獨立的應用服務,我們的程序通過 RESTful 接口與其交互。而嵌入模式則不需要下載 BMP 可執行文件,直接通過包的形式引入到我們的程序中來。可惜的是,嵌入模式只支持 Java 語言,但這也聊勝於無,於是我使用 Java 寫了個測試程序嘗試了一把。
  首先引入 browsermob-core 包,
  <dependency>
  <groupId>net.lightbody.bmp</groupId>
  <artifactId>browsermob-core</artifactId>
  <version>2.1.5</version>
  </dependency>
  然后參考官網文檔寫下下面的代碼(完整代碼見 這里),這里就可以看到嵌入模式的好處了,用於 BMP 攔截處理的代碼和我們自己的代碼處於同一個環境下,而且 Java 語言具有閉包的特性,我們可以很簡單的取到 Ajax 請求的響應報文:
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("請求結束:"+messageInfo.getOriginalUrl());
if(messageInfo.getOriginalUrl().contains("ajax")){
ajaxContent=contents.getTextContents();
}
});
  如果你是個 .Net guy,可以使用 Fiddler 提供的 FiddlerCore,FiddlerCore 就相當於 BMP 的嵌入模式,和這里的方法類似。這里有一篇很好的文章講解了如何使用 .Net 和 FiddlerCore 攔截請求。
  既然在 Java 環境下解決了這個問題,那么 Python 應該也沒問題,但是 BMP 的嵌入模式並不支持 Python 怎么辦呢?於是我一直在尋找一款基於 Python 的能替代 BMP 的工具,可惜一直不如願,未能找到滿意的。到最后,我幾乎要下結論:Python + Selenium 很難實現 Ajax 請求的爬取。
  天無絕人之路,直到我遇到了 har。
  有一天我靜下心來把 BMP 的文檔翻來覆去看了好幾遍,之前我看文檔的習慣都是用時再查,但這次把 BMP 的文檔從頭到尾看了幾遍,也是希望能從中尋找點蛛絲馬跡。而事實上,還真被我發現了點什么。因為 Python 只能通過 RESTful 接口與 BMP 交互,那么每一個接口我都不能放過,有一個接口引起了我的注意:/proxy/[port]/har。
  這個接口雖然之前也掃過幾眼,但當時並不知道這個 har 是什么意思,所以都是一掠而過。但那天心血來潮,特意去查了一下 har 的資料,才發現這是一種特殊的 JSON 格式的歸檔文件。HAR 全稱 HTTP Archive Format,通常用於記錄瀏覽器訪問網站的所有交互請求,絕大多數瀏覽器和 Web 代理都支持這種格式的歸檔文件,用於分析 HTTP 請求,因為廣泛的應用,W3C 甚至還提出 HAR 的規范,目前還在草稿階段。
  /proxy/[port]/har 接口用於創建一份新的 har 文件,Selenium 啟動瀏覽器后所有的請求都將被記錄到這份 har 文件中,然后通過 GET 請求,可以獲取到這份 har 文件的內容(JSON 格式)。har 文件的內容類似於下面這樣:
  {
  "log": {
  "version" : "1.2",
  "creator" : {},
  "browser" : {},
  "pages": [],
  "entries": [],
  "comment": ""
  }
  }
  其中 entries 數組包含了所有 HTTP 請求的列表,默認情況下 BMP 創建的 har 文件並不包含請求的響應內容,我們可以通過 captureContent 參數來讓 BMP 記錄響應內容:
  curl -X PUT -d 'captureContent=true' http://localhost:8080/proxy/8081/har
  萬事俱備,只欠東風。我們開始寫代碼,首先通過 proxy.new_har() 創建一份 har 文件:
  proxy.new_har(options={
  'captureContent': True
  })
  然后啟動瀏覽器,訪問要爬取的頁面,等待頁面加載結束,這時我們就可以通過 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)
   總結
  這篇博客總結了 Selenium 的一些基礎語法,並嘗試使用 Python + Selenium 開發瀏覽器爬蟲。本文還分享了我在實際開發過程中遇到的幾個常見問題,並提供了一種或多種解決方案,包括代理的使用,攔截瀏覽器請求,爬取 Ajax 請求等等。實踐出真知,通過一系列問題的提出,到研究,到解決,我學習到了非常多的東西。不僅意識到知識廣度的重要性,而且更重要的是知識的聚合和熔煉。我一直認為知識的廣度比深度更重要,只有你懂的越多,你才有可能接觸更多的東西,你的思路才更放得開;深度固然也重要,但往往會讓人局限於自己的漩渦之中。但知識的廣度不是天馬行空,需要不斷的總結提煉,融會貫通,形成自己的知識體系,這樣才不至於被繁多的知識點所困擾。
  另外,我也意識到閱讀項目文檔的重要性,心平氣和的將項目文檔從頭到尾閱讀一遍,遇到不懂的,就去查找資料,而不是只挑自己知道或感興趣的,這樣會得到意想不到的收獲。
  本文所有源碼都在我的 GitHub 上,你可以從 這里 查看完整源碼。本人能力有限,文中如有錯誤,歡迎斧正,望不吝賜教。如有好的想法和問題,也歡迎留言評論。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM