0x00 前言
網絡爬蟲(Web crawler),是一種“自動化瀏覽網絡”的程序,或者說是一種網絡機器人。它們被廣泛用於互聯網搜索引擎或其他類似網站,以獲取或更新這些網站的內容和檢索方式。它們可以自動採集所有其能夠訪問到的頁面內容,以便程序做下一步的處理。
在WEB2.0時代,動態網頁盛行起來。那麼爬蟲就應該能在頁面內爬到這些有javascript生成的鏈接。當然動態解析頁面只是爬蟲的一個技術點。下面,我將按照如下順序分享下面的這些內容的一些個人經驗(編程語言爲Python)。
1、爬蟲架構。
2、頁面下載與解析。
3、URL去重方法。
4、URL相似性算法。
5、併發操作。
6、數據存儲
7、動態爬蟲源碼分享。
8、參考文章
0x01 爬蟲架構
談到爬蟲架構,不得不提的是Scrapy的爬蟲架構。Scrapy,是Python開發的一個快速,高層次的爬蟲框架,用於抓取web站點並從頁面中提取結構化的數據。Scrapy用途廣泛,可以用於數據挖掘、監測和自動化測試。Scrapy吸引人的地方在於它是一個框架,任何人都可以根據需求方便的修改。它也提供了多種類型爬蟲的基類,如BaseSpider、sitemap爬蟲等。
上圖是Scrapy的架構圖,綠線是數據流向,首先從初始URL 開始,Scheduler 會將其交給 Downloader 進行下載,下載之後會交給 Spider 進行分析,需要保存的數據則會被送到Item Pipeline,那是對數據進行後期處理。另外,在數據流動的通道里還可以安裝各種中間件,進行必要的處理。 因此在開發爬蟲的時候,最好也先規劃好各種模塊。我的做法是單獨規劃下載模塊,爬行模塊,調度模塊,數據存儲模塊。
0x02 頁面下載與解析
頁面下載
頁面下載分爲靜態和動態兩種下載方式。
傳統爬蟲利用的是靜態下載方式,靜態下載的優勢是下載過程快,但是頁面只是一個枯燥的html,因此頁面鏈接分析中獲取的只是< a >標籤的href屬性或者高手可以自己分析js,form之類的標籤捕獲一些鏈接。在python中可以利用urllib2模塊或requests模塊實現功能。 動態爬蟲在web2.0時代則有特殊的優勢,由於網頁會使用javascript處理,網頁內容通過Ajax異步獲取。所以,動態爬蟲需要分析經過javascript處理和ajax獲取內容後的頁面。目前簡單的解決方法是通過基於webkit的模塊直接處理。PYQT4、Splinter和Selenium這三個模塊都可以達到目的。對於爬蟲而言,瀏覽器界面是不需要的,因此使用一個headless browser是非常划算的,HtmlUnit和phantomjs都是可以使用的headless browser。
以上這段代碼是訪問新浪網主站。通過對比靜態抓取頁面和動態抓取頁面的長度和對比靜態抓取頁面和動態抓取頁面內抓取的鏈接個數。
在靜態抓取中,頁面的長度是563838,頁面內抓取的鏈接數量只有166個。而在動態抓取中,頁面的長度增長到了695991,而鏈接數達到了1422,有了近10倍的提升。
抓鏈接表達式
正則:re.compile("href=\"([^\"]*)\"")
Xpath:xpath('//*[@href]')
頁面解析
頁面解析是實現抓取頁面內鏈接和抓取特定數據的模塊,頁面解析主要是對字符串的處理,而html是一種特殊的字符串,在Python中re、beautifulsoup、HTMLParser、lxml等模塊都可以解決問題。對於鏈接,主要抓取a標籤下的href屬性,還有其他一些標籤的src屬性。
0x03 URL去重
URL去重是爬蟲運行中一項關鍵的步驟,由於運行中的爬蟲主要阻塞在網絡交互中,因此避免重複的網絡交互至關重要。爬蟲一般會將待抓取的URL放在一個隊列中,從抓取後的網頁中提取到新的URL,在他們被放入隊列之前,首先要確定這些新的URL沒有被抓取過,如果之前已經抓取過了,就不再放入隊列了。
Hash表
利用hash表做去重操作一般是最容易想到的方法,因爲hash表查詢的時間複雜度是O(1),而且在hash表足夠大的情況下,hash衝突的概率就變得很小,因此URL是否重複的判斷準確性就非常高。利用hash表去重的這個做法是一個比較簡單的解決方法。但是普通hash表也有明顯的缺陷,在考慮內存的情況下,使用一張大的hash表是不妥的。Python中可以使用字典這一數據結構。
URL壓縮
如果hash表中,當每個節點儲存的是一個str形式的具體URL,是非常佔用內存的,如果把這個URL進行壓縮成一個int型變量,內存佔用程度上便有了3倍以上的縮小。因此可以利用Python的hashlib模塊來進行URL壓縮。 思路:把hash表的節點的數據結構設置爲集合,集合內儲存壓縮後的URL。
Bloom Filter
Bloom Filter是通過極少的錯誤換取了存儲空間的極大節省。Bloom Filter 是通過一組k 個定義在n 個輸入key 上的Hash Function,將上述n 個key 映射到m 位上的數據容器。
上圖很清楚的說明了Bloom Filter的優勢,在可控的容器長度內,所有hash函數對同一個元素計算的hash值都爲1時,就判斷這個元素存在。 Python中hashlib,自帶多種hash函數,有MD5,sha1,sha224,sha256,sha384,sha512。代碼中還可以進行加鹽處理,還是很方便的。 Bloom Filter也會產生衝突的情況,具體內容查看文章結尾的參考文章。
在Python編程過程中,可以使用jaybaird提供的BloomFilter接口,或者自己造輪子。
小細節
有個小細節,在建立hash表的時候選擇容器很重要。hash表佔用空間太大是個很不爽的問題,因此針對爬蟲去重,下列方法可以解決一些問題。
上面這段代碼簡單驗證了生成容器的運行時間。
由上圖可以看出,建立一個長度爲1億的容器時,選擇list容器程序的運行時間花費了7.2s,而選擇字符串作爲容器時,才花費了0.2s的運行時間。
接下來看看內存的佔用情況。
如果建立1億的列表佔用了794660k內存。
而建立1億長度的字符串卻佔用了109720k內存,空間佔用大約減少了700000k。
0x04 URL相似性
初級算法
對於URL相似性,我只是實踐一個非常簡單的方法。
在保證不進行重複爬去的情況下,還需要對類似的URL進行判斷。我採用的是sponge和ly5066113提供的思路。具體資料在參考文章裏。
下列是一組可以判斷爲相似的URL組
http://auto.sohu.com/7/0903/70/column213117075.shtml
http://auto.sohu.com/7/0903/95/column212969565.shtml
http://auto.sohu.com/7/0903/96/column212969687.shtml
http://auto.sohu.com/7/1103/61/column216206148.shtml
http://auto.sohu.com/s2007/0155/s254359851/index1.shtml
http://auto.sohu.com/s2007/5730/s249066842/index2.shtml
http://auto.sohu.com/s2007/5730/s249067138/index3.shtml
http://auto.sohu.com/s2007/5730/s249067983/index4.shtml
按照預期,以上URL歸併後應該爲
http://auto.sohu.com/7/0903/70/column213117075.shtml
http://auto.sohu.com/s2007/0155/s254359851/index1.shtml
思路如下,需要提取如下特徵
1,host字符串
2,目錄深度(以’/’分割)
3,尾頁特徵
具體算法
算法本身很菜,各位一看就能懂。
實際效果:
上圖顯示了把8個不一樣的url,算出了2個值。通過實踐,在一張千萬級的hash表中,衝突的情況是可以接受的。
0x05 併發操作
Python中的併發操作主要涉及的模型有:多線程模型、多進程模型、協程模型。Elias專門寫了一篇文章,來比較常用的幾種模型併發方案的性能。對於爬蟲本身來說,限制爬蟲速度主要來自目標服務器的響應速度,因此選擇一個控制起來順手的模塊纔是對的。
多線程模型
多線程模型,是最容易上手的,Python中自帶的threading模塊能很好的實現併發需求,配合Queue模塊來實現共享數據。
多進程模型
多進程模型和多線程模型類似,multiprocessing模塊中也有類似的Queue模塊來實現數據共享。在linux中,用戶態的進程可以利用多核心的優勢,因此在多核背景下,能解決爬蟲的併發問題。
協程模型
協程模型,在Elias的文章中,基於greenlet實現的協程程序的性能僅次於Stackless Python,大致比Stackless Python慢一倍,比其他方案快接近一個數量級。因此基於gevent(封裝了greenlet)的併發程序會有很好的性能優勢。
具體說明下gevent(非阻塞異步IO)。,“Gevent是一種基於協程的Python網絡庫,它用到Greenlet提供的,封裝了libevent事件循環的高層同步API。”
從實際的編程效果來看,協程模型確實表現非常好,運行結果的可控性明顯強了不少, gevent庫的封裝易用性極強。
0x06 數據存儲
數據存儲本身設計的技術就非常多,作爲小菜不敢亂說,但是工作還是有一些小經驗是可以分享的。
前提:使用關係數據庫,測試中選擇的是mysql,其他類似sqlite,SqlServer思路上沒有區別。
當我們進行數據存儲時,目的就是減少與數據庫的交互操作,這樣可以提高性能。通常情況下,每當一個URL節點被讀取,就進行一次數據存儲,對於這樣的邏輯進行無限循環。其實這樣的性能體驗是非常差的,存儲速度非常慢。
進階做法,爲了減少與數據庫的交互次數,每次與數據庫交互從之前傳送1個節點變成傳送10個節點,到傳送100個節點內容,這樣效率變有了10倍至100倍的提升,在實際應用中,效果是非常好的。:D
0x07 動態爬蟲源碼分享
爬蟲模型
目前這個爬蟲模型如上圖,調度模塊是核心模塊。調度模塊分別與下載模塊,析取模塊,存儲模塊共享三個隊列,下載模塊與析取模塊共享一個隊列。數據傳遞方向如圖示。
爬蟲源碼
實現了以下功能:
動態下載
gevent處理
BloomFilter過濾
URL相似度過濾
關鍵字過濾
爬取深度
Github地址:https://github.com/manning23/MSpider
代碼總體來說難度不大,各位輕噴。
0x08 參考文章
感謝以下分享的文章與討論
http://security.tencent.com/index.php/blog/msg/34 http://www.pnigos.com/?p=217
http://security.tencent.com/index.php/blog/msg/12 http://wenku.baidu.com/view/7fa3ad6e58fafab069dc02b8.html
http://wenku.baidu.com/view/67fa6feaaeaad1f346933f28.html
http://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
http://www.elias.cn/Python/PyConcurrency?from=Develop.PyConcurrency
http://blog.csdn.net/HanTangSongMing/article/details/24454453
http://blog.csdn.net/historyasamirror/article/details/6746217 http://www.spongeliu.com/399.html
http://xlambda.com/gevent-tutorial/ http://simple-is-better.com/news/334
http://blog.csdn.net/jiaomeng/article/details/1495500 http://bbs.chinaunix.net/forum.php?mod=viewthread&tid=1337181
http://www.tuicool.com/articles/nieEVv http://www.zhihu.com/question/21652316 http://code.rootk.com/entry/crawler