抓取網頁的最佳語言 : Python
Friday, January 29, 2010最初
最早我用C/C++語言慢慢寫抓網頁的用它來抓網頁真的是程式,一開始甚至打算自己寫抓取網頁的函式庫,想說當做練習,可是HTTP協定 雖然不難,可是煩,要處理的細節太多了,後來受不了,轉而使用現成的Library : cUrl,但是C/C++語言開發這類東西的效率實在太慢了,我的程式不停的修改、不停的修改,光是編譯的時間就吃掉了不知道多少,字串的處理C/C++ 沒有內建正規表示法或一些好用的字串函數之類的,處理起來也礙手礙腳,當時,我想將我寫好的函數庫寫成能讓Lua呼叫的形式,或著甚是C/C++來呼叫Lua,因為C/C++有很多細節要處理,Memory leak有的沒有的雜事,我想要的只是專注在寫抓取網頁的程式,因此用Lua包裝似乎是不錯的選擇,但是開發時間太久了,事情一直沒有變好
直到
我下了一個結論,C/C++不適合寫抓取網頁的程式,我開始思考我需要什麼,我想我既然要包裝成其它語言將細節藏起來,為何不直接使用script語言? 我最早一直擔心的是效率的問題,但是到後來想想反正真正沒效率的部份包給C/C++去做事實上沒有太大的差別,而且又有動態語言的彈性、除錯上的方便等等好處,何樂不為? 於是我開始尋找一款合適的語言
Perl 如何?
很早以前我有用Perl寫過一些CGI程式、留言版、網站管理系統、文章管理系統等等,有人說Perl是只能寫一次的語言,它有很多很簡短的符號所構成的表示法,可讀性不是很好,模組化設計也沒有非常好的支援,OO也是一樣,新版的Perl遲遲沒有推出,似乎已經有點變成遺產的感覺,或許是上面的理由還是偏見,總而言之我不喜歡Perl
PHP?
做為一個以網頁為主要用途的語言,拿來當做其它用途總有種不太合適的感覺,從它的語法來看,很明顯是參考C語言、Perl等等而來的,但是卻沒有加以改進,我個人認為它可能沒有預料到PHP居然會紅成這樣,變成網頁程式設計的主流語言,後來有很多缺點就變得顯而易見,不夠嚴僅的語法、不夠好的模組化設計、不良的OO支援、容易寫出安全性有問題的程式等等,命名空間也是它一大缺點之一,光是看到一大堆前綴字開頭的函數就有種倒胃口的感覺,有人說
PHP is the BASIC of the 21st century
在這個影片裡,總合種種理由,做為抓取網頁的用途,PHP出局
Lua
Lua做為輕量級的語言相當的優秀,可是你不會想用Lua來寫大型的程式,我也不會想這麼做,它語言的設計都是以速度為優先考量,寫起來並不怎麼順手的感覺,再加上目前的資源不多,可能很多東西都得自行包裝,這樣就和我原先想做的事是一樣的,因此不考慮Lua
Java
Java是和網路一起成長的程式語言,做為抓取網頁的用途,它絕對有能力勝任,但是…,我嫌它太囉唆了,還有太癡肥,當一款語言太囉唆和太癡肥往往會令人討厭,歐! 想到Java我就想起eclipse在我那台只有256扣掉分給顯示記憶體的筆電上執行的情況,讓我想把電腦砸掉,不好意思,我不喜歡Java 在前面的影片裡的老兄一樣也有提到,有興趣可以看看
Java is the COBOL of the 21st century
Python
最後,我在PTT的程式設計討論版上描述了我的需求,有人推文說 Python,我抓了抓頭髮,Python? WTF? 這是什麼? 我從來沒有聽過這款語言,於是上網找了一下資料,和問了一些問題,發現這款語言正是我想要的,它很容易被擴充,因此效能不足可以用C/C++補強,你想得到的函式庫幾乎都已經有人寫好了,光從下載網頁這件工作來看,它的標準函式庫已經有了這樣的功能,你覺得不夠好還有其它很多的選擇,開箱即用的哲學,讓安裝函式庫非常簡單,不像C/C++的編譯惡夢讓你抓光頭髮,而它最優秀的地方之一就是它的可讀性,寫起來相當順手、優雅,讀起來也一樣順眼,重要的是很有趣,那麼開發大型的程式呢? script語言常見的問題就是對於開發大型程式來說很不適合,但是Python卻不是如此,良好的OO、模組化等等它都有良好的支援,再加上Google也是Python的愛用者,YouTube也是用Python開發的,有了這些大咖背書,證明這款語言的確是相當優秀,在決定使用Python之後我就立刻訂購了一本Learning Python,開始學習Python
愛上Python
Python並沒有讓我失望,能用Python寫的東西都不太想用C/C++去寫,開發效率非常高、寫起來很順手、豐富的資源,讓我覺得這真的是優秀的語言,它的確很適合拿來抓取網頁,不過抓取網頁還有更多東西要考慮
Twisted
用Python抓取網頁的HTML只是小菜一盤,用Python標準函數庫就辦得到,但不是那麼好用,最後我發現了Twisted,就改用Twisted來抓網頁,它有優秀的非同步事件驅動的架構,常見的協定都已經有實做,包括HTTP、SMTP等等,用它來抓網頁真的是再容易不過了
getPage("http://www.google.com").addCallback(printPage)
是的,一行就可以抓網頁,夠簡單吧,而且你想要傳POST或GET等參數,或是修改HTTP的header都沒有問題
BeautifulSoup
抓網頁事實上不是什麼難事,解析HTML要來得更麻煩,最初使用Python的標準函式庫內建的HTMLParser來解析網頁,但是功能太陽春,加上最頭痛的問題是,大部份的網頁都沒有完全尊照標準來寫,各種莫明奇妙的錯誤令人想要找出那個寫網頁的人痛打他一頓,為了解決容錯的問題,一開始我使用BeautifulSoup來抓取網頁,它是以容錯著名的HTML Parser,但是,它的效率很差,又或著說,找到目標HTML標籤的方式很沒效率,一般都用find等方式來找到所要的標籤
soup.find('div', dict(id='content'))
它真的很沒效率,當你抓取大一點的網頁時,多塞幾個一起抓和解析,你就會看見你的CPU使用率永遠是滿的狀態,原本我預計抓網頁的瓶頸都會落在網路IO上面,但是用它來抓取網頁卻超出我預料,沒想到它會這麼吃重,於是沒辦法,我開始尋找更好的選擇
lxml
我找到一個Blog的文章 : Python HTML Parser Performance,介紹了Python各種Parser的效能,效能最亮眼的,就是lxml,我最初擔心的是找到資料標籤會不會很困難,但是我發現它支援xpath,就試著改寫原本BeautifulSoup用find等等函數寫的尋找標籤程式,發現xpath遠比那種方式來得好用太多了,而且效率好太多了,BeautifulSoup的find極度的沒有效率,大部份的CPU時間都耗在一堆find函數走訪HTML樹上,而xpath篩選標籤的方式來得有效率多了,以下舉幾個我實際用在抓取網頁的案子中的例子
def getNextPageLink(self, tree): """Get next page link @param tree: tree to get link @return: Return url of next page, if there is no next page, return None """ paging = tree.xpath("//span[@class='paging']") if paging: links = paging[0].xpath("./a[(text(), '%s')]" % self.localText['next']) if links: return str(links[0].get('href')) return None listPrice = tree.xpath("//[@class='priceBlockLabel']/following-sibling::") if listPrice: detail['listPrice'] = self.stripMoney(listPrice[0].text)
原本使用BeautifulSoup在尋找標籤遇到麻煩的走訪羅輯上的問題還得寫程式解決,xpath本身就有豐富的語法可以提供各種篩選的條件,羅輯從程式碼被移到了xpath語法上,有了這樣的語法,尋找目標標籤輕鬆了許多,而且效率也很好,從此我就和BeautifulSoup說再見,改用lxml來找標籤
配合FireFox的工具
如果有一些工具可以幫助寫解析網頁的程式該有多好,這也是我希望能有的,使用了xpath之後,我找到了FireFox的插件,XPath
checker等xpath的工具,可以先用它來確定抓到的元素是正確的,然後FireBug在檢視網頁結構上也有很大的幫助

FireFox插件XPath checker畫面
使用FireBug檢視網頁元素
結論
就目前一路走過來的經驗來看,抓取網頁Python的確是最佳的選擇,不過我們到目前為止我們都只討論到工具,事實上還有設計上的問題要解決
