1.緣起
隨著移動網際網路的發展,現在寫web和我三年前剛開始寫爬蟲的時候已經改變了太多。特別是在node以及javascript/ruby社群的努力下,以往“伺服器端”做的事情都慢慢搬到了“瀏覽器”來實現,最極端的例子可能是meteor了 ,寫web程式無需劃分前端後端的時代已經到來了。。。
在這一方面,Google一向是最激進的。縱觀Google目前的產品線,社交的Google Plus,網站分析的Google Analytics,Google目前賴以生存的Google Adwords等,如果想下載原始碼,用ElementTree來解析網頁,那什麼都得不到,因為Google的資料都是通過Ajax呼叫經過資料混淆處理的資料,然後用JavaScript進行解析渲染到頁面上的。
本來這種事情也不算太多,忍一忍就行了,不過最近因業務需要,經常需要上Google的Keyword Tools來分析特定關鍵字的搜尋量。
圖為關鍵字搜尋的截圖
圖為Google經過混淆處理的Ajax返回結果。
要把這麼費勁的事情自動化倒也不難,因為Google也提供了API來做,Adwords專案的TargetingIdeaService就是來做這個的,問題是Google的API呼叫需要花錢,而如果能用爬蟲技術來爬取這個結果,就能省去不必要的額外開銷。
2. Selenium WebDriver
由於要解析執行復雜的JavaScript,必須有一個Full Stack的瀏覽器JavaScript環境,這種環境三年前的話,可能只能訴諸於於selenium,selenium是一款多語言的瀏覽器Driver,它最大的優點在於,提供了從命令列統一操控多種不同瀏覽器的方法,這極大地方便了web產品的相容性自動化測試。
2.1 在沒有圖形介面的伺服器上安裝和使用Selenium
安裝selenium非常簡單,pip install selenium 即可,但是要讓firefox/chrome工作,沒有圖形介面的話,還是要費一番功夫的。
推薦的做法是
1 2 3 |
apt-get install xvfb Xvfb :99 -ac -screen 0 1024x768x8& export DISPLAY=:99 |
Selenium的安裝和配置在此就不多說了,值得注意的是,如果是Ubuntu使用者,並且要使用Chrome的話,必須額外下載一個chromedriver,並且把安裝的chromium-browser連結到/usr/bin/google-chrome,否則將無法執行。
2.2 爬取Keywords
先總結一下Adwords的使用方法吧,要能正常使用Adwords,必須要有一個開通Adwords的Google Account,這倒不是很難,只要訪問 http://adwords.google.com ,Google會協助建立賬號,如果還沒有的話,其次就是登陸了。
通過分析登陸頁面,我們可以看到需要在id為Email的一個input框內輸入email,然後在id為Passwd的密碼框內輸入密碼,然後點選Sign in提交這個頁面內唯一的form。
首先別忘了開一個瀏覽器先
1 2 |
search = re.compile(r'(\?[^#]*)#').search(driver.current_url).group(1) kwurl='https://adwords.google.com/o/Targeting/Explorer'+search+'&__o=cues&ideaRequestType=KEYWORD_IDEAS' |
1 2 3 |
driver.find_element_by_id("Email").send_keys(email) driver.find_element_by_id("Passwd").send_keys(passwd) driver.find_element_by_id('signIn').submit() |
登陸後,我們發現需要訪問一個類似 https://adwords.google.com/o/Targeting/Explorer 的網頁才能跳轉到關鍵字工具,於是我們手動生成一下這個網頁
1 2 |
search = re.compile(r'(\?[^#]*)#').search(driver.current_url).group(1) kwurl='https://adwords.google.com/o/Targeting/Explorer'+search+'&__o=cues&ideaRequestType=KEYWORD_IDEAS' |
到了工具主頁以後,事情就變得Tricky起來了。因為整個關鍵字工具都是個客戶端App,在全部檔案載入完成以後,頁面不會直接渲染完畢,而是要經過複雜的JavaScript運算後頁面才會完整顯示。然而Selenium WebDriver並不知道這一點,所以我們要讓他知道。
在這裡,我們要等待Search按鈕在瀏覽器中出現以後,才能確認網頁載入完畢,Selenium WebDriver有兩種方式可以實現這一點,我偷懶用了全域性的預設等待機制:
1 |
driver.implicitly_wait(30) |
於是Selenium就會在找不到頁面元素的時候自動等候不超過30秒
接下來,等待輸入框和Search按鈕出現後提交搜尋iphone關鍵字的請求
1 2 |
driver.find_element_by_class_name("sEAB").send_keys("iphone") find_element_by_css_selector("button.gwt-Button").click() |
然後我們繼續等待class為sLNB的table的出現,並解析結果
1 2 3 4 5 |
result = {} texts = driver.find_elements_by_xpath('//table[@class="sLNB"]')\ [0].text.split() for i in range(1, len(texts)/4): result[ texts[i*4] ] = (texts[i*4+2], texts[i*4+3]) |
這裡我們使用了xpath來提取網頁特徵,這也算是寫爬蟲的必備吧。
完整的例子見: https://gist.github.com/3798896 替換email和passwd後直接就能用了
3. JavaScript Headless解決方案
隨著Node以及隨之而來的JavaScript社群的進化,如今的我們就幸福多了。遠的我們有phantomjs, 一個Headless的WebKit Driver,意味著可以無需GUI,完全模擬Chrome/Safari的操作。 近的有casperjs(基於phantomjs的好用封裝),zombie(相比phantomjs的優勢是可以和node整合)等。
其中非常可惜地是,zombiejs似乎對富JavaScript網站支援得有問題,所以後來我還是隻能用casperjs來進行測試。Headless的方案因為不需要渲染GUI,執行速度約為Selenium方案的三倍。
另外由於這是純JavaScript的方案,於是我們可以直接在例如Chrome的Console模式下寫程式碼控制瀏覽器,不存在如Selenium那樣還需要語義轉換,非常簡潔直觀。例如利用W3C Selectors API Level 1所提供的querySelector來快速選取元素,對錶單進行submit,對按鈕進行click,甚至可以執行自定義JavaScript指令碼以便按一定規律對頁面進行操控。
但是casperjs或者說phantomjs的弱點是不支援除了檔案讀寫和瀏覽器操作以外的一切*nix IPC慣用伎倆,socket神馬的統統不支援,1.4版本以後才加入了一個webserver用於和外界通訊,但是用httpserver來和外界通訊?我有點牴觸就是了。
廢話不說了,casperjs的程式碼看起來就是這樣,登陸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
var casper = require('casper').create({verbose:true,logLevel:"debug"}); casper.start('http://adwords.google.com'); casper.thenEvaluate(function login(email, passwd) { document.querySelector('#Email').setAttribute('value', email); document.querySelector('#Passwd').setAttribute('value', passwd); document.querySelector('form').submit(); }, {email:email, passwd:passwd}); casper.waitForSelector(".aw-cues-item", function() { kwurl = this.evaluate(function(){ var search = document.location.search; return 'https://adwords.google.com/o/Targeting/Explorer'+search+'&__o=cues&ideaRequestType=KEYWORD_IDEAS'; }) }); |
與Selenium類似,因為頁面都是Ajax呼叫的,我們需要明確地“等待某個元素出現”,即:waitForSelector,casperjs的文件既簡潔又漂亮,不妨多逛逛。
值得一提的是,casperjs一定要呼叫casper.run方法,之前的start, then等方法,只是把步驟封裝到了this._steps裡面,只有在run的時候才會真正執行,所以casperjs設計流程的時候會很痛苦,for/each之類的手法有時並不好用。
這個時候需要用JavaScript程式設計比較常用的遞迴化的方法,參見https://github.com/n1k0/casperjs/blob/master/samples/dynamic.js 這個例子。我在完整的casperjs程式碼裡面也是這麼做的。
具體邏輯的實現和selenium類似,我就不廢話了,完整的例子參見: https://gist.github.com/3798922
4. 綜上
介紹了selenium和casperjs兩種不同的終極爬蟲寫法,但是其實這篇文寫來只是太久沒更新了,寫點東西更新一下而已:)