前一兩年抓過某工商資訊網站,幾三週時間大約抓了過千萬多萬張頁面。那時由於公司沒啥經費,報銷又拖得很久,不想花錢在很多機器和頻寬上,所以當時花了較多精力研究如何讓一臺爬蟲機器達到抓取極限。
本篇偏爬蟲技術細節,先周知。
Python爬蟲這兩年貌似成為了一項必備技能,無論是搞技術的,做產品的,資料分析的,金融的,初創公司做冷啟動的,都想去抓點資料回來玩玩。這裡面絕大多數一共都只抓幾萬或幾十萬條資料,這個數量級其實大可不必寫爬蟲,使用 chrome 外掛 web scraper 或者讓 selenium 驅動 chrome 就好了,會為你節省很多分析網頁結構或研究如何登陸的時間。
本篇只關注如何讓爬蟲的抓取效能最大化上,沒有使用scrapy等爬蟲框架,就是多執行緒+Python requests庫搞定。
對一個網站定向抓取幾十萬張頁面一般只用解決訪問頻率限制問題就好了。對機器記憶體,硬碟空間,URL去重,網路效能,抓取間隙時間調優一般都不會在意。如果要設計一個單臺每天抓取上百萬張網頁,共有一億張頁面的網站時,訪問頻率限制問題就不是最棘手的問題了,上述每一項都要很好解決才行。硬碟儲存,記憶體,網路效能等問題我們一項項來拆解。
一、優化硬碟儲存
所以千萬級網頁的抓取是需要先設計的,先來做一個計算題。共要抓取一億張頁面,一般一張網頁的大小是400KB左右,一億張網頁就是1億X200KB=36TB 。這麼大的儲存需求,一般的電腦和硬碟都是沒法儲存的。所以肯定要對網頁做壓縮後儲存,可以用zlib壓縮,也可以用壓縮率更好的bz2或pylzma 。
但是這樣還不夠,我們拿天眼查的網頁來舉例。天眼查一張公司詳情頁的大小是700KB 。
對這張網頁zlib壓縮後是100KB。
一億個100KB(9TB)還是太大,要對網頁特殊處理一下,可以把網頁的頭和尾都去掉,只要body部分再壓縮。因為一張html頁面裡<head></head>和<footer></footer>大都是公共的頭尾資訊和js/css程式碼,對你以後做正文內容抽取不會影響(也可以以後做內容抽取時把頭尾資訊補回去就好)。
來看一下去掉頭尾後的html頁面大小是300KB,壓縮後是47KB。
一億張就是4T,差不多算是能接受了。京東上一個4T硬碟600多元。
二、優化記憶體,URL去重
再來說記憶體佔用問題,做爬蟲程式為了防止重複抓取URL,一般要把URL都載入進記憶體裡,放在set()裡面。拿天眼查的URL舉例:
https://www.tianyancha.com/company/23402373
這個完整URL有44個位元組,一億個URL就是4G,一億個URL就要佔用4G記憶體,這還沒有算存這一億個URL需要的資料結構記憶體,還有待抓取URL,已抓取URL還儲存在記憶體中的html等等消耗的記憶體。
所以這樣直接用set()儲存URL是不建議的,除非你的記憶體有十幾個G。
一個取巧的辦法是截斷URL。只把URL:
https://www.tianyancha.com/company/23402373
的字尾:23402373放進set()裡,23402373只佔8個位元組,一億個URL佔700多M記憶體。
但是如果你是用的野雲主機,用來不斷撥號用的非正規雲主機,這700多M記憶體也是吃不消的,機器會非常卡。
就還需要想辦法壓縮URL的記憶體佔用,可以使用BloomFilter演算法,是一個很經典的演算法,非常適用海量資料的排重過濾,佔用極少的記憶體,查詢效率也非常的高。它的原理是把一個字串對映到一個bit上,剛才23402373佔8個位元組,現在只佔用1個bit(1位元組=8bit),記憶體節省了近64倍,以前700M記憶體,現在只需要10多M了。
BloomFilter呼叫也非常簡單,當然需要先install 安裝bloom_filter:
from bloom_filter import BloomFilter
bloombloom = BloomFilter(max_elements=100000000, error_rate=0.1)
bloom.add('https://www.tianyancha.com/company/23402373')
bloombloom.__contains__('https://www.tianyancha.com/company/23402373')
不過奇怪,bloom裡沒有公有方法來判斷URL是否重複,我用的__contains__()方法,也可能是我沒用對,不過判重效果是一樣的。
三、反抓取訪問頻率限制
單臺機器,單個IP大家都明白,短時間內訪問一個網站幾十次後肯定會被遮蔽的。每個網站對IP的解封策略也不一樣,有的1小時候後又能重新訪問,有的要一天,有的要幾個月去了。突破抓取頻率限制有兩種方式,一種是研究網站的反爬策略。有的網站不對列表頁做頻率控制,只對詳情頁控制。有的針對特定UA,referer,或者微信的H5頁面的頻率控制要弱很多。我在這兩篇文章有講到《爬蟲小偏方:繞開登陸和訪問頻率控制》《 爬蟲小偏方二:修改referer後可以不用登入了》。
另一種方式就是多IP抓取,多IP抓取又分IP代理池和adsl撥號兩種,我這裡說adsl撥號的方式,IP代理池相對於adsl來說,我覺得收費太貴了。要穩定大規模抓取肯定是要用付費的,一個月也就100多塊錢。
adsl的特點是可以短時間內重新撥號切換IP,IP被禁止了重新撥號一下就可以了。這樣你就可以開足馬力瘋狂抓取了,但是一天只有24小時合86400秒,要如何一天抓過百萬網頁,讓網路效能最大化也是需要下一些功夫的,後面我再詳說。
至於有哪些可以adsl撥號的野雲主機,你在百度搜”vps adsl”,能選擇的廠商很多的。大多宣稱有百萬級IP資源可撥號,我曾測試過一段時間,把每次撥號的IP記錄下來,有真實二三十萬IP的就算不錯了。
選adsl的一個注意事項是,有的廠商撥號IP只能播出C段和D段IP,110(A段).132(B段).3(C段).2(D段),A和B段都不會變,靠C,D段IP高頻次抓取對方網站,有可能對方網站把整個C/D段IP都封掉。
C/D段加一起255X255就是6萬多個IP全都報廢,所以要選撥號IP範圍較寬的廠商。 你要問我哪家好,我也不知道,這些都是野雲主機,質量和穩定性本就沒那麼好。只有多試一試,試的成本也不大,買一臺玩玩一個月也就一百多元,還可以按天買。
上面我為什麼說不用付費的IP代理池?
因為比adsl撥號貴很多,因為全速抓取時,一個反爬做得可以的網站10秒內就會封掉這個IP,所以10秒就要換一個IP,理想狀況下一天86400秒,要換8640個IP。
如果用付費IP代理池的話,一個代理IP收費4分錢,8640個IP一天就要345元。 adsl撥號的主機一個月才100多元。
adsl撥號Python程式碼
怎麼撥號廠商都會提供的,建議是用廠商提供的方式,這裡只是示例:
windows下用os呼叫rasdial撥號:
import os
# 撥號斷開
os.popen('rasdial 網路連結名稱 /disconnect')
# 撥號
os.popen('rasdial 網路連結名稱 adsl賬號 adsl密碼')
linux下撥號:
import os
# 撥號斷開
code = os.system('ifdown 網路連結名稱')
# 撥號
code = os.system('ifup 網路連結名稱')
四、網路效能,抓取技術細節調優
上面步驟做完了,每天能達到抓取五萬網頁的樣子,要達到百萬級規模,還需把網路效能和抓取技術細節調優。
1.除錯開多少個執行緒,多長時間撥號切換IP一次最優。
每個網站對短時間內訪問次數的遮蔽策略不一樣,這需要實際測試,找出抓取效率最大化的時間點。先開一個執行緒,一直抓取到IP被遮蔽,記錄下抓取耗時,總抓取次數,和成功抓取次數。 再開2個執行緒,重複上面步驟,記錄抓取耗時,總的和成功的抓取次數。再開4個執行緒,重複上面步驟。整理成一個表格如下,下圖是我抓天眼查時,統計抓取極限和細節調優的表格:
從上圖比較可以看出,當有6個執行緒時,是比較好的情況。耗時6秒,成功抓取80-110次。雖然8個執行緒只耗時4秒,但是成功抓取次數已經在下降了。所以執行緒數可以設定為開6個。
開多少個執行緒除錯出來了,那多久撥號一次呢?
從上面的圖片看到,貌似每隔6秒撥號是一個不錯的選擇。可以這樣做,但是我選了另一個度量單位,就是每總抓取120次就重新撥號。為什麼這樣選呢?從上圖也能看到,基本抓到120次左右就會被遮蔽,每隔6秒撥號其實誤差比較大,因為網路延遲等各種問題,導致6秒內可能抓100次,也可能抓120次。
2.requests請求優化
要優化requests.get(timeout=1.5)的超時時間,不設定超時的話,有可能get()請求會一直掛起等待。而且野雲主機本身效能就不穩定,長時間不回請求很正常。如果要追求抓取效率,超時時間設定短一點,設定10秒超時完全沒有意義。對於超時請求失敗的,大不了以後再二次請求,也比設定10秒的抓取效率高很多。
3.優化adsl撥號等待時間
上面步驟已算把單臺機器的抓取技術問題優化到一個高度了,還剩一個優化野雲主機的問題。就是每次斷開撥號後,要等待幾秒鐘再撥號,太短時間內再撥號有可能又撥到上一個IP,還有可能撥號失敗,所以要等待6秒鐘(測試值)。所以要把撥號程式碼改一下:
import os
# 斷開撥號
os.popen('rasdial 網路名稱 /disconnect')
time.sleep(6)
# 撥號
os.popen('rasdial 網路名稱 adsl賬號名 adsl密碼')
而且 os.popen(‘rasdial 網路名稱 adsl賬號名 adsl密碼’) 撥號完成後,你還不能馬上使用,那時外網還是不可用的,你需要檢測一下外網是否聯通。
我使用 ping 功能來檢測外網連通性:
import os
code = os.system('ping www.baidu.com')
code為0時表示聯通,不為0時還要重新撥號。而ping也很耗時間的,一個ping命令會ping 4次,就要耗時4秒。
上面撥號等待6秒加上 ping 的4秒,消耗了10秒鐘。上面猿人學Python說了,抓120次才用6秒,每撥號一次要消耗10秒,而且是每抓120次就要重撥號,想下這個時間太可惜了,每天8萬多秒有一半時間都消耗在撥號上面了,但是也沒辦法。
當然好點的野雲主機,除了上面說的IP範圍的差異,就是撥號質量差異。好的撥號等待時間更短一點,撥號出錯的概率要小一點。
通過上面我們可以輕鬆計算出一組抓取的耗時是6秒,撥號耗時10秒,總耗時16秒。一天86400秒,就是5400組抓取,上面說了一組抓取是120次。一天就可以抓取5400X120=64萬張網頁。
按照上述的設計就可以做到一天抓60多萬張頁面,如果你把adsl撥號耗時再優化一點,每次再節約2-3秒,就趨近於百萬抓取量級了。
另外野雲主機一個月才100多,很便宜,所以你可以再開一臺adsl撥號主機,用兩臺一起抓取,一天就能抓一百多萬張網頁。幾天時間就能映象一個過千萬網頁的網站。
知識Tips:
1.為什麼不用非同步抓取?
沒必要,這裡的整個抓取關鍵是網路效能,而不是程式效能。用非同步把程式效能提高了,單位時間的抓取次數是提高了,但是這樣反而會擊中對方網站的訪問頻率控制策略。
2.要計算對方的頻寬壓力,不要抓取太過分了
抓取歸抓取,但不要影響對方網站,把對方網站頻寬都打滿了。
一箇中小型網站的頻寬在5M以內,大一點的網站頻寬可能10-30M,超大型的另算。
一張網頁300KB,對方一般會壓縮後傳輸給瀏覽器,就按壓縮後30KB算,你的爬蟲一秒請求20次,頻寬就是600KB。可能一個網站每天都有幾十個爬蟲都在爬,我們按有10個爬蟲在同時抓取,就是這些爬蟲一秒內就要消耗600KBX10=6M頻寬。
再加上還有正規爬蟲,人家網站上的正常使用者訪問這些,算下來可能一共要消耗10M頻寬。一般的大中型網站都是吃不消的。
終於寫完了,3000多字手都打酸了。
一直有讀者在問我教不教Python,教不教爬蟲。這兩天想了一下,是可以教的:
一對一教學Python爬蟲,學會如何設計抓取海量資料的爬蟲;
學會Python Web開發,設計一個支撐千萬交易額的電商網站後端;
傳授我用技術的掙錢經驗,我的職場經驗。
全以實戰為主,學完能自己真實動手開發那種。
你可以在公眾號選單欄-聯絡我,找到我的微信。
關於我的故事
爬蟲文章擴充閱讀
我的公眾號:猿人學 Python 上會分享更多心得體會,敬請關注。
***版權申明:若沒有特殊說明,文章皆是猿人學 yuanrenxue.com 原創,沒有猿人學授權,請勿以任何形式轉載。***