使用Python編寫多線程爬蟲抓取百度貼吧郵箱與手機(jī)號(hào)
不知道大家過年都是怎么過的,反正欄主是在家睡了一天,醒來的時(shí)候登QQ發(fā)現(xiàn)有人找我要一份貼吧爬蟲的源代碼,想起之前練手的時(shí)候?qū)戇^一個(gè)抓取百度貼吧發(fā)帖記錄中的郵箱與手機(jī)號(hào)的爬蟲,于是開源分享給大家學(xué)習(xí)與參考。
需求分析:
本爬蟲主要是對(duì)百度貼吧中各種帖子的內(nèi)容進(jìn)行抓取,并且分析帖子內(nèi)容將其中的手機(jī)號(hào)和郵箱地址抓取出來。主要流程在代碼注釋中有詳細(xì)解釋。
測(cè)試環(huán)境:
代碼在Windows7 64bit,python 2.7 64bit(安裝mysqldb擴(kuò)展)以及centos 6.5,python 2.7(帶mysqldb擴(kuò)展)環(huán)境下測(cè)試通過
環(huán)境準(zhǔn)備:
工欲善其事必先利其器,大家可以從截圖看出我的環(huán)境是Windows 7 + PyCharm。我的Python環(huán)境是Python 2.7 64bit。這是比較適合新手使用的開發(fā)環(huán)境。然后我再建議大家安裝一個(gè)easy_install,聽名字就知道這是一個(gè)安裝器,它是用來安裝一些擴(kuò)展包的,比如說在python中如果我們要操作mysql數(shù)據(jù)庫(kù)的話,python原生是不支持的,我們必須安裝mysqldb包來讓python可以操作mysql數(shù)據(jù)庫(kù),如果有easy_install的話我們只需要一行命令就可以快速安裝號(hào)mysqldb擴(kuò)展包,他就像php中的composer,centos中的yum,Ubuntu中的apt-get一樣方便。
相關(guān)工具可在我的github中找到:cw1997/python-tools,其中easy_install的安裝只需要在python命令行下運(yùn)行那個(gè)py腳本然后稍等片刻即可,他會(huì)自動(dòng)加入Windows的環(huán)境變量,在Windows命令行下如果輸入easy_install有回顯說明安裝成功。
環(huán)境選擇的細(xì)節(jié)說明:
至于電腦硬件當(dāng)然是越快越好,內(nèi)存起碼8G起步,因?yàn)榕老x本身需要大量存儲(chǔ)和解析中間數(shù)據(jù),尤其是多線程爬蟲,在碰到抓取帶有分頁(yè)的列表和詳情頁(yè),并且抓取數(shù)據(jù)量很大的情況下使用queue隊(duì)列分配抓取任務(wù)會(huì)非常占內(nèi)存。包括有的時(shí)候我們抓取的數(shù)據(jù)是使用json,如果使用mongodb等nosql數(shù)據(jù)庫(kù)存儲(chǔ),也會(huì)很占內(nèi)存。
網(wǎng)絡(luò)連接建議使用有線網(wǎng),因?yàn)槭忻嫔弦恍┝淤|(zhì)的無線路由器和普通的民用無線網(wǎng)卡在線程開的比較大的情況下會(huì)出現(xiàn)間歇性斷網(wǎng)或者數(shù)據(jù)丟失,掉包等情況,這個(gè)我親有體會(huì)。
至于操作系統(tǒng)和python當(dāng)然肯定是選擇64位。如果你使用的是32位的操作系統(tǒng),那么無法使用大內(nèi)存。如果你使用的是32位的python,可能在小規(guī)模抓取數(shù)據(jù)的時(shí)候感覺不出有什么問題,但是當(dāng)數(shù)據(jù)量變大的時(shí)候,比如說某個(gè)列表,隊(duì)列,字典里面存儲(chǔ)了大量數(shù)據(jù),導(dǎo)致python的內(nèi)存占用超過2g的時(shí)候會(huì)報(bào)內(nèi)存溢出錯(cuò)誤。原因在我曾經(jīng)segmentfault上提過的問題中依云的回答有解釋(java – python只要占用內(nèi)存達(dá)到1.9G之后httplib模塊就開始報(bào)內(nèi)存溢出錯(cuò)誤 – SegmentFault)
如果你準(zhǔn)備使用mysql存儲(chǔ)數(shù)據(jù),建議使用mysql5.5以后的版本,因?yàn)閙ysql5.5版本支持json數(shù)據(jù)類型,這樣的話可以拋棄mongodb了。(有人說mysql會(huì)比mongodb穩(wěn)定一點(diǎn),這個(gè)我不確定。)
至于現(xiàn)在python都已經(jīng)出了3.x版本了,為什么我這里還使用的是python2.7?我個(gè)人選擇2.7版本的原因是自己當(dāng)初很早以前買的python核心編程這本書是第二版的,仍然以2.7為示例版本。并且目前網(wǎng)上仍然有大量的教程資料是以2.7為版本講解,2.7在某些方面與3.x還是有很大差別,如果我們沒有學(xué)過2.7,可能對(duì)于一些細(xì)微的語(yǔ)法差別不是很懂會(huì)導(dǎo)致我們理解上出現(xiàn)偏差,或者看不懂demo代碼。而且現(xiàn)在還是有部分依賴包只兼容2.7版本。我的建議是如果你是準(zhǔn)備急著學(xué)python然后去公司工作,并且公司沒有老代碼需要維護(hù),那么可以考慮直接上手3.x,如果你有比較充裕的時(shí)間,并且沒有很系統(tǒng)的大牛帶,只能依靠網(wǎng)上零零散散的博客文章來學(xué)習(xí),那么還是先學(xué)2.7在學(xué)3.x,畢竟學(xué)會(huì)了2.7之后3.x上手也很快。
多線程爬蟲涉及到的知識(shí)點(diǎn):
其實(shí)對(duì)于任何軟件項(xiàng)目而言,我們凡是想知道編寫這個(gè)項(xiàng)目需要什么知識(shí)點(diǎn),我們都可以觀察一下這個(gè)項(xiàng)目的主要入口文件都導(dǎo)入了哪些包。
HTTP協(xié)議:
我們的爬蟲抓取數(shù)據(jù)本質(zhì)上就是不停的發(fā)起http請(qǐng)求,獲取http響應(yīng),將其存入我們的電腦中。了解http協(xié)議有助于我們?cè)谧ト?shù)據(jù)的時(shí)候?qū)σ恍┠軌蚣铀僮ト∷俣鹊膮?shù)能夠精準(zhǔn)的控制,比如說keep-alive等。
threading模塊(多線程):
我們平時(shí)編寫的程序都是單線程程序,我們寫的代碼都在主線程里面運(yùn)行,這個(gè)主線程又運(yùn)行在python進(jìn)程中。關(guān)于線程和進(jìn)程的解釋可以參考阮一峰的博客:進(jìn)程與線程的一個(gè)簡(jiǎn)單解釋 – 阮一峰的網(wǎng)絡(luò)日志
在python中實(shí)現(xiàn)多線程是通過一個(gè)名字叫做threading的模塊來實(shí)現(xiàn)。之前還有thread模塊,但是threading對(duì)于線程的控制更強(qiáng),因此我們后來都改用threading來實(shí)現(xiàn)多線程編程了。
關(guān)于threading多線程的一些用法,我覺得這篇文章不錯(cuò):[python] 專題八.多線程編程之thread和threading 大家可以參考參考。
簡(jiǎn)單來說,使用threading模塊編寫多線程程序,就是先自己定義一個(gè)類,然后這個(gè)類要繼承threading.Thread,并且把每個(gè)線程要做的工作代碼寫到一個(gè)類的run方法中,當(dāng)然如果線程本身在創(chuàng)建的時(shí)候如果要做一些初始化工作,那么就要在他的__init__方法中編寫好初始化工作所要執(zhí)行的代碼,這個(gè)方法就像php,java中的構(gòu)造方法一樣。
這里還要額外講的一點(diǎn)就是線程安全這個(gè)概念。通常情況下我們單線程情況下每個(gè)時(shí)刻只有一個(gè)線程在對(duì)資源(文件,變量)操作,所以不可能會(huì)出現(xiàn)沖突。但是當(dāng)多線程的情況下,可能會(huì)出現(xiàn)同一個(gè)時(shí)刻兩個(gè)線程在操作同一個(gè)資源,導(dǎo)致資源損壞,所以我們需要一種機(jī)制來解決這種沖突帶來的破壞,通常有加鎖等操作,比如說mysql數(shù)據(jù)庫(kù)的innodb表引擎有行級(jí)鎖等,文件操作有讀取鎖等等,這些都是他們的程序底層幫我們完成了。所以我們通常只要知道那些操作,或者那些程序?qū)τ诰€程安全問題做了處理,然后就可以在多線程編程中去使用它們了。而這種考慮到線程安全問題的程序一般就叫做“線程安全版本”,比如說php就有TS版本,這個(gè)TS就是Thread Safety線程安全的意思。下面我們要講到的Queue模塊就是一種線程安全的隊(duì)列數(shù)據(jù)結(jié)構(gòu),所以我們可以放心的在多線程編程中使用它。
***我們就要來講講至關(guān)重要的線程阻塞這個(gè)概念了。當(dāng)我們?cè)敿?xì)學(xué)習(xí)完threading模塊之后,大概就知道如何創(chuàng)建和啟動(dòng)線程了。但是如果我們把線程創(chuàng)建好了,然后調(diào)用了start方法,那么我們會(huì)發(fā)現(xiàn)好像整個(gè)程序立馬就結(jié)束了,這是怎么回事呢?其實(shí)這是因?yàn)槲覀冊(cè)谥骶€程中只有負(fù)責(zé)啟動(dòng)子線程的代碼,也就意味著主線程只有啟動(dòng)子線程的功能,至于子線程執(zhí)行的那些代碼,他們本質(zhì)上只是寫在類里面的一個(gè)方法,并沒在主線程里面真正去執(zhí)行他,所以主線程啟動(dòng)完子線程之后他的本職工作就已經(jīng)全部完成了,已經(jīng)光榮退場(chǎng)了。既然主線程都退場(chǎng)了,那么python進(jìn)程就跟著結(jié)束了,那么其他線程也就沒有內(nèi)存空間繼續(xù)執(zhí)行了。所以我們應(yīng)該是要讓主線程大哥等到所有的子線程小弟全部執(zhí)行完畢再光榮退場(chǎng),那么在線程對(duì)象中有什么方法能夠把主線程卡住呢?thread.sleep嘛?這確實(shí)是個(gè)辦法,但是究竟應(yīng)該讓主線程sleep多久呢?我們并不能準(zhǔn)確知道執(zhí)行完一個(gè)任務(wù)要多久時(shí)間,肯定不能用這個(gè)辦法。所以我們這個(gè)時(shí)候應(yīng)該上網(wǎng)查詢一下有什么辦法能夠讓子線程“卡住”主線程呢?“卡住”這個(gè)詞好像太粗鄙了,其實(shí)說專業(yè)一點(diǎn),應(yīng)該叫做“阻塞”,所以我們可以查詢“python 子線程阻塞主線程”,如果我們會(huì)正確使用搜索引擎的話,應(yīng)該會(huì)查到一個(gè)方法叫做join(),沒錯(cuò),這個(gè)join()方法就是子線程用于阻塞主線程的方法,當(dāng)子線程還未執(zhí)行完畢的時(shí)候,主線程運(yùn)行到含有join()方法的這一行就會(huì)卡在那里,直到所有線程都執(zhí)行完畢才會(huì)執(zhí)行join()方法后面的代碼。
Queue模塊(隊(duì)列):
假設(shè)有一個(gè)這樣的場(chǎng)景,我們需要抓取一個(gè)人的博客,我們知道這個(gè)人的博客有兩個(gè)頁(yè)面,一個(gè)list.php頁(yè)面顯示的是此博客的所有文章鏈接,還有一個(gè)view.php頁(yè)面顯示的是一篇文章的具體內(nèi)容。
如果我們要把這個(gè)人的博客里面所有文章內(nèi)容抓取下來,編寫單線程爬蟲的思路是:先用正則表達(dá)式把這個(gè)list.php頁(yè)面的所有鏈接a標(biāo)簽的href屬性抓取下來,存入一個(gè)名字叫做article_list的數(shù)組(在python中不叫數(shù)組,叫做list,中文名列表),然后再用一個(gè)for循環(huán)遍歷這個(gè)article_list數(shù)組,用各種抓取網(wǎng)頁(yè)內(nèi)容的函數(shù)把內(nèi)容抓取下來然后存入數(shù)據(jù)庫(kù)。
如果我們要編寫一個(gè)多線程爬蟲來完成這個(gè)任務(wù)的話,就假設(shè)我們的程序用10個(gè)線程把,那么我們就要想辦法把之前抓取的article_list平均分成10份,分別把每一份分配給其中一個(gè)子線程。
但是問題來了,如果我們的article_list數(shù)組長(zhǎng)度不是10的倍數(shù),也就是文章數(shù)量并不是10的整數(shù)倍,那么***一個(gè)線程就會(huì)比別的線程少分配到一些任務(wù),那么它將會(huì)更快的結(jié)束。
如果僅僅是抓取這種只有幾千字的博客文章這看似沒什么問題,但是如果我們一個(gè)任務(wù)(不一定是抓取網(wǎng)頁(yè)的任務(wù),有可能是數(shù)學(xué)計(jì)算,或者圖形渲染等等耗時(shí)任務(wù))的運(yùn)行時(shí)間很長(zhǎng),那么這將造成極大地資源和時(shí)間浪費(fèi)。我們多線程的目的就是盡可能的利用一切計(jì)算資源并且計(jì)算時(shí)間,所以我們要想辦法讓任務(wù)能夠更加科學(xué)合理的分配。
并且我還要考慮一種情況,就是文章數(shù)量很大的情況下,我們要既能快速抓取到文章內(nèi)容,又能盡快的看到我們已經(jīng)抓取到的內(nèi)容,這種需求在很多CMS采集站上經(jīng)常會(huì)體現(xiàn)出來。
比如說我們現(xiàn)在要抓取的目標(biāo)博客,有幾千萬篇文章,通常這種情況下博客都會(huì)做分頁(yè)處理,那么我們?nèi)绻凑丈厦娴膫鹘y(tǒng)思路先抓取完list.php的所有頁(yè)面起碼就要幾個(gè)小時(shí)甚至幾天,老板如果希望你能夠盡快顯示出抓取內(nèi)容,并且盡快將已經(jīng)抓取到的內(nèi)容展現(xiàn)到我們的CMS采集站上,那么我們就要實(shí)現(xiàn)一邊抓取list.php并且把已經(jīng)抓取到的數(shù)據(jù)丟入一個(gè)article_list數(shù)組,一邊用另一個(gè)線程從article_list數(shù)組中提取已經(jīng)抓取到的文章URL地址,然后這個(gè)線程再去對(duì)應(yīng)的URL地址中用正則表達(dá)式取到博客文章內(nèi)容。如何實(shí)現(xiàn)這個(gè)功能呢?
我們就需要同時(shí)開啟兩類線程,一類線程專門負(fù)責(zé)抓取list.php中的url然后丟入article_list數(shù)組,另外一類線程專門負(fù)責(zé)從article_list中提取出url然后從對(duì)應(yīng)的view.php頁(yè)面中抓取出對(duì)應(yīng)的博客內(nèi)容。
但是我們是否還記得前面提到過線程安全這個(gè)概念?前一類線程一邊往article_list數(shù)組中寫入數(shù)據(jù),另外那一類的線程從article_list中讀取數(shù)據(jù)并且刪除已經(jīng)讀取完畢的數(shù)據(jù)。但是python中l(wèi)ist并不是線程安全版本的數(shù)據(jù)結(jié)構(gòu),因此這樣操作會(huì)導(dǎo)致不可預(yù)料的錯(cuò)誤。所以我們可以嘗試使用一個(gè)更加方便且線程安全的數(shù)據(jù)結(jié)構(gòu),這就是我們的子標(biāo)題中所提到的Queue隊(duì)列數(shù)據(jù)結(jié)構(gòu)。
同樣Queue也有一個(gè)join()方法,這個(gè)join()方法其實(shí)和上一個(gè)小節(jié)所講到的threading中join()方法差不多,只不過在Queue中,join()的阻塞條件是當(dāng)隊(duì)列不為空空的時(shí)候才阻塞,否則繼續(xù)執(zhí)行join()后面的代碼。在這個(gè)爬蟲中我便使用了這種方法來阻塞主線程而不是直接通過線程的join方式來阻塞主線程,這樣的好處是可以不用寫一個(gè)死循環(huán)來判斷當(dāng)前任務(wù)隊(duì)列中是否還有未執(zhí)行完的任務(wù),讓程序運(yùn)行更加高效,也讓代碼更加優(yōu)雅。
還有一個(gè)細(xì)節(jié)就是在python2.7中隊(duì)列模塊的名字是Queue,而在python3.x中已經(jīng)改名為queue,就是首字母大小寫的區(qū)別,大家如果是復(fù)制網(wǎng)上的代碼,要記得這個(gè)小區(qū)別。
getopt模塊:
如果大家學(xué)過c語(yǔ)言的話,對(duì)這個(gè)模塊應(yīng)該會(huì)很熟悉,他就是一個(gè)負(fù)責(zé)從命令行中的命令里面提取出附帶參數(shù)的模塊。比如說我們通常在命令行中操作mysql數(shù)據(jù)庫(kù),就是輸入mysql -h127.0.0.1 -uroot -p,其中mysql后面的“-h127.0.0.1 -uroot -p”就是可以獲取的參數(shù)部分。
我們平時(shí)在編寫爬蟲的時(shí)候,有一些參數(shù)是需要用戶自己手動(dòng)輸入的,比如說mysql的主機(jī)IP,用戶名密碼等等。為了讓我們的程序更加友好通用,有一些配置項(xiàng)是不需要硬編碼在代碼里面,而是在執(zhí)行他的時(shí)候我們動(dòng)態(tài)傳入,結(jié)合getopt模塊我們就可以實(shí)現(xiàn)這個(gè)功能。
hashlib(哈希):
哈希本質(zhì)上就是一類數(shù)學(xué)算法的集合,這種數(shù)學(xué)算法有個(gè)特性就是你給定一個(gè)參數(shù),他能夠輸出另外一個(gè)結(jié)果,雖然這個(gè)結(jié)果很短,但是他可以近似認(rèn)為是***的。比如說我們平時(shí)聽過的md5,sha-1等等,他們都屬于哈希算法。他們可以把一些文件,文字經(jīng)過一系列的數(shù)學(xué)運(yùn)算之后變成短短不到一百位的一段數(shù)字英文混合的字符串。
python中的hashlib模塊就為我們封裝好了這些數(shù)學(xué)運(yùn)算函數(shù),我們只需要簡(jiǎn)單的調(diào)用它就可以完成哈希運(yùn)算。
為什么在我這個(gè)爬蟲中用到了這個(gè)包呢?因?yàn)樵谝恍┙涌谡?qǐng)求中,服務(wù)器需要帶上一些校驗(yàn)碼,保證接口請(qǐng)求的數(shù)據(jù)沒有被篡改或者丟失,這些校驗(yàn)碼一般都是hash算法,所以我們需要用到這個(gè)模塊來完成這種運(yùn)算。
json:
很多時(shí)候我們抓取到的數(shù)據(jù)不是html,而是一些json數(shù)據(jù),json本質(zhì)上只是一段含有鍵值對(duì)的字符串,如果我們需要提取出其中特定的字符串,那么我們需要json這個(gè)模塊來將這個(gè)json字符串轉(zhuǎn)換為dict類型方便我們操作。
re(正則表達(dá)式):
有的時(shí)候我們抓取到了一些網(wǎng)頁(yè)內(nèi)容,但是我們需要將網(wǎng)頁(yè)中的一些特定格式的內(nèi)容提取出來,比如說電子郵箱的格式一般都是前面幾位英文數(shù)字字母加一個(gè)@符號(hào)加http://xxx.xxx的域名,而要像計(jì)算機(jī)語(yǔ)言描述這種格式,我們可以使用一種叫做正則表達(dá)式的表達(dá)式來表達(dá)出這種格式,并且讓計(jì)算機(jī)自動(dòng)從一大段字符串中將符合這種特定格式的文字匹配出來。
sys:
這個(gè)模塊主要用于處理一些系統(tǒng)方面的事情,在這個(gè)爬蟲中我用他來解決輸出編碼問題。
time:
稍微學(xué)過一點(diǎn)英語(yǔ)的人都能夠猜出來這個(gè)模塊用于處理時(shí)間,在這個(gè)爬蟲中我用它來獲取當(dāng)前時(shí)間戳,然后通過在主線程末尾用當(dāng)前時(shí)間戳減去程序開始運(yùn)行時(shí)的時(shí)間戳,得到程序的運(yùn)行時(shí)間。
如圖所示,開50個(gè)線程抓取100頁(yè)(每頁(yè)30個(gè)帖子,相當(dāng)于抓取了3000個(gè)帖子)貼吧帖子內(nèi)容并且從中提取出手機(jī)郵箱這個(gè)步驟共耗時(shí)330秒。
urllib和urllib2:
這兩個(gè)模塊都是用于處理一些http請(qǐng)求,以及url格式化方面的事情。我的爬蟲http請(qǐng)求部分的核心代碼就是使用這個(gè)模塊完成的。
MySQLdb:
這是一個(gè)第三方模塊,用于在python中操作mysql數(shù)據(jù)庫(kù)。
這里我們要注意一個(gè)細(xì)節(jié)問題:mysqldb模塊并不是線程安全版本,意味著我們不能在多線程中共享同一個(gè)mysql連接句柄。所以大家可以在我的代碼中看到,我在每個(gè)線程的構(gòu)造函數(shù)中都傳入了一個(gè)新的mysql連接句柄。因此每個(gè)子線程只會(huì)用自己獨(dú)立的mysql連接句柄。
cmd_color_printers:
這也是一個(gè)第三方模塊,網(wǎng)上能夠找到相關(guān)代碼,這個(gè)模塊主要用于向命令行中輸出彩色字符串。比如說我們通常爬蟲出現(xiàn)錯(cuò)誤,要輸出紅色的字體會(huì)比較顯眼,就要使用到這個(gè)模塊。
自動(dòng)化爬蟲的錯(cuò)誤處理:
如果大家在網(wǎng)絡(luò)質(zhì)量不是很好的環(huán)境下使用該爬蟲,會(huì)發(fā)現(xiàn)有的時(shí)候會(huì)報(bào)如圖所示的異常,這是我為了偷懶并沒有寫各種異常處理的邏輯。
通常情況下我們?nèi)绻帉懜叨茸詣?dòng)化的爬蟲,那么就需要預(yù)料到我們的爬蟲可能會(huì)遇到的所有異常情況,針對(duì)這些異常情況做處理。
比如說如圖所示的錯(cuò)誤,我們就應(yīng)該把當(dāng)時(shí)正在處理的任務(wù)重新塞入任務(wù)隊(duì)列,否則我們就會(huì)出現(xiàn)遺漏信息的情況。這也是爬蟲編寫的一個(gè)復(fù)雜點(diǎn)。
總結(jié):
其實(shí)多線程爬蟲的編寫也不復(fù)雜,多看示例代碼,多自己動(dòng)手嘗試,多去社區(qū),論壇交流,很多經(jīng)典的書上對(duì)多線程編程也有非常詳細(xì)的解釋。這篇文章本質(zhì)上主要還是一篇科普文章,內(nèi)容講解的都不是很深入,大家還需要課外自己多結(jié)合網(wǎng)上各種資料自己學(xué)習(xí)。