使用 Python 編寫多執行緒爬蟲抓取百度貼吧郵箱與手機號

@昌維發表於2017-02-02

不知道大家過年都是怎麼過的,反正欄主是在家睡了一天,醒來的時候登QQ發現有人找我要一份貼吧爬蟲的原始碼,想起之前練手的時候寫過一個抓取百度貼吧發帖記錄中的郵箱與手機號的爬蟲,於是開源分享給大家學習與參考。

需求分析:

本爬蟲主要是對百度貼吧中各種帖子的內容進行抓取,並且分析帖子內容將其中的手機號和郵箱地址抓取出來。主要流程在程式碼註釋中有詳細解釋。

測試環境:

程式碼在Windows7 64bit,python 2.7 64bit(安裝mysqldb擴充套件)以及centos 6.5,python 2.7(帶mysqldb擴充套件)環境下測試通過

環境準備:

工欲善其事必先利其器,大家可以從截圖看出我的環境是Windows 7 + PyCharm。我的Python環境是Python 2.7 64bit。這是比較適合新手使用的開發環境。然後我再建議大家安裝一個easy_install,聽名字就知道這是一個安裝器,它是用來安裝一些擴充套件包的,比如說在python中如果我們要操作mysql資料庫的話,python原生是不支援的,我們必須安裝mysqldb包來讓python可以操作mysql資料庫,如果有easy_install的話我們只需要一行命令就可以快速安裝號mysqldb擴充套件包,他就像php中的composer,centos中的yum,Ubuntu中的apt-get一樣方便。

相關工具可在我的github中找到:cw1997/python-tools,其中easy_install的安裝只需要在python命令列下執行那個py指令碼然後稍等片刻即可,他會自動加入Windows的環境變數,在Windows命令列下如果輸入easy_install有回顯說明安裝成功。

環境選擇的細節說明:

至於電腦硬體當然是越快越好,記憶體起碼8G起步,因為爬蟲本身需要大量儲存和解析中間資料,尤其是多執行緒爬蟲,在碰到抓取帶有分頁的列表和詳情頁,並且抓取資料量很大的情況下使用queue佇列分配抓取任務會非常佔記憶體。包括有的時候我們抓取的資料是使用json,如果使用mongodb等nosql資料庫儲存,也會很佔記憶體。

網路連線建議使用有線網,因為市面上一些劣質的無線路由器和普通的民用無線網路卡線上程開的比較大的情況下會出現間歇性斷網或者資料丟失,掉包等情況,這個我親有體會。

至於作業系統和python當然肯定是選擇64位。如果你使用的是32位的作業系統,那麼無法使用大記憶體。如果你使用的是32位的python,可能在小規模抓取資料的時候感覺不出有什麼問題,但是當資料量變大的時候,比如說某個列表,佇列,字典裡面儲存了大量資料,導致python的記憶體佔用超過2g的時候會報記憶體溢位錯誤。原因在我曾經segmentfault上提過的問題中依雲的回答有解釋(java – python只要佔用記憶體達到1.9G之後httplib模組就開始報記憶體溢位錯誤 – SegmentFault

如果你準備使用mysql儲存資料,建議使用mysql5.5以後的版本,因為mysql5.5版本支援json資料型別,這樣的話可以拋棄mongodb了。(有人說mysql會比mongodb穩定一點,這個我不確定。)

至於現在python都已經出了3.x版本了,為什麼我這裡還使用的是python2.7?我個人選擇2.7版本的原因是自己當初很早以前買的python核心程式設計這本書是第二版的,仍然以2.7為示例版本。並且目前網上仍然有大量的教程資料是以2.7為版本講解,2.7在某些方面與3.x還是有很大差別,如果我們沒有學過2.7,可能對於一些細微的語法差別不是很懂會導致我們理解上出現偏差,或者看不懂demo程式碼。而且現在還是有部分依賴包只相容2.7版本。我的建議是如果你是準備急著學python然後去公司工作,並且公司沒有老程式碼需要維護,那麼可以考慮直接上手3.x,如果你有比較充裕的時間,並且沒有很系統的大牛帶,只能依靠網上零零散散的部落格文章來學習,那麼還是先學2.7在學3.x,畢竟學會了2.7之後3.x上手也很快。

多執行緒爬蟲涉及到的知識點:

其實對於任何軟體專案而言,我們凡是想知道編寫這個專案需要什麼知識點,我們都可以觀察一下這個專案的主要入口檔案都匯入了哪些包。

現在來看一下我們這個專案,作為一個剛接觸python的人,可能有一些包幾乎都沒有用過,那麼我們在本小節就來簡單的說說這些包起什麼作用,要掌握他們分別會涉及到什麼知識點,這些知識點的關鍵詞是什麼。這篇文章並不會花費長篇大論來從基礎講起,因此我們要學會善用百度,搜尋這些知識點的關鍵詞來自學。下面就來一一分析一下這些知識點。

HTTP協議:

我們的爬蟲抓取資料本質上就是不停的發起http請求,獲取http響應,將其存入我們的電腦中。瞭解http協議有助於我們在抓取資料的時候對一些能夠加速抓取速度的引數能夠精準的控制,比如說keep-alive等。

threading模組(多執行緒):

我們平時編寫的程式都是單執行緒程式,我們寫的程式碼都在主執行緒裡面執行,這個主執行緒又執行在python程式中。關於執行緒和程式的解釋可以參考阮一峰的部落格:程式與執行緒的一個簡單解釋 – 阮一峰的網路日誌

在python中實現多執行緒是通過一個名字叫做threading的模組來實現。之前還有thread模組,但是threading對於執行緒的控制更強,因此我們後來都改用threading來實現多執行緒程式設計了。

關於threading多執行緒的一些用法,我覺得這篇文章不錯:[python] 專題八.多執行緒程式設計之thread和threading 大家可以參考參考。

簡單來說,使用threading模組編寫多執行緒程式,就是先自己定義一個類,然後這個類要繼承threading.Thread,並且把每個執行緒要做的工作程式碼寫到一個類的run方法中,當然如果執行緒本身在建立的時候如果要做一些初始化工作,那麼就要在他的__init__方法中編寫好初始化工作所要執行的程式碼,這個方法就像php,java中的構造方法一樣。

這裡還要額外講的一點就是執行緒安全這個概念。通常情況下我們單執行緒情況下每個時刻只有一個執行緒在對資源(檔案,變數)操作,所以不可能會出現衝突。但是當多執行緒的情況下,可能會出現同一個時刻兩個執行緒在操作同一個資源,導致資源損壞,所以我們需要一種機制來解決這種衝突帶來的破壞,通常有加鎖等操作,比如說mysql資料庫的innodb表引擎有行級鎖等,檔案操作有讀取鎖等等,這些都是他們的程式底層幫我們完成了。所以我們通常只要知道那些操作,或者那些程式對於執行緒安全問題做了處理,然後就可以在多執行緒程式設計中去使用它們了。而這種考慮到執行緒安全問題的程式一般就叫做“執行緒安全版本”,比如說php就有TS版本,這個TS就是Thread Safety執行緒安全的意思。下面我們要講到的Queue模組就是一種執行緒安全的佇列資料結構,所以我們可以放心的在多執行緒程式設計中使用它。

最後我們就要來講講至關重要的執行緒阻塞這個概念了。當我們詳細學習完threading模組之後,大概就知道如何建立和啟動執行緒了。但是如果我們把執行緒建立好了,然後呼叫了start方法,那麼我們會發現好像整個程式立馬就結束了,這是怎麼回事呢?其實這是因為我們在主執行緒中只有負責啟動子執行緒的程式碼,也就意味著主執行緒只有啟動子執行緒的功能,至於子執行緒執行的那些程式碼,他們本質上只是寫在類裡面的一個方法,並沒在主執行緒裡面真正去執行他,所以主執行緒啟動完子執行緒之後他的本職工作就已經全部完成了,已經光榮退場了。既然主執行緒都退場了,那麼python程式就跟著結束了,那麼其他執行緒也就沒有記憶體空間繼續執行了。所以我們應該是要讓主執行緒大哥等到所有的子執行緒小弟全部執行完畢再光榮退場,那麼線上程物件中有什麼方法能夠把主執行緒卡住呢?thread.sleep嘛?這確實是個辦法,但是究竟應該讓主執行緒sleep多久呢?我們並不能準確知道執行完一個任務要多久時間,肯定不能用這個辦法。所以我們這個時候應該上網查詢一下有什麼辦法能夠讓子執行緒“卡住”主執行緒呢?“卡住”這個詞好像太粗鄙了,其實說專業一點,應該叫做“阻塞”,所以我們可以查詢“python 子執行緒阻塞主執行緒”,如果我們會正確使用搜尋引擎的話,應該會查到一個方法叫做join(),沒錯,這個join()方法就是子執行緒用於阻塞主執行緒的方法,當子執行緒還未執行完畢的時候,主執行緒執行到含有join()方法的這一行就會卡在那裡,直到所有執行緒都執行完畢才會執行join()方法後面的程式碼。

Queue模組(佇列):

假設有一個這樣的場景,我們需要抓取一個人的部落格,我們知道這個人的部落格有兩個頁面,一個list.php頁面顯示的是此部落格的所有文章連結,還有一個view.php頁面顯示的是一篇文章的具體內容。

如果我們要把這個人的部落格裡面所有文章內容抓取下來,編寫單執行緒爬蟲的思路是:先用正規表示式把這個list.php頁面的所有連結a標籤的href屬性抓取下來,存入一個名字叫做article_list的陣列(在python中不叫陣列,叫做list,中文名列表),然後再用一個for迴圈遍歷這個article_list陣列,用各種抓取網頁內容的函式把內容抓取下來然後存入資料庫。

如果我們要編寫一個多執行緒爬蟲來完成這個任務的話,就假設我們的程式用10個執行緒把,那麼我們就要想辦法把之前抓取的article_list平均分成10份,分別把每一份分配給其中一個子執行緒。

但是問題來了,如果我們的article_list陣列長度不是10的倍數,也就是文章數量並不是10的整數倍,那麼最後一個執行緒就會比別的執行緒少分配到一些任務,那麼它將會更快的結束。

如果僅僅是抓取這種只有幾千字的部落格文章這看似沒什麼問題,但是如果我們一個任務(不一定是抓取網頁的任務,有可能是數學計算,或者圖形渲染等等耗時任務)的執行時間很長,那麼這將造成極大地資源和時間浪費。我們多執行緒的目的就是儘可能的利用一切計算資源並且計算時間,所以我們要想辦法讓任務能夠更加科學合理的分配。

並且我還要考慮一種情況,就是文章數量很大的情況下,我們要既能快速抓取到文章內容,又能儘快的看到我們已經抓取到的內容,這種需求在很多CMS採集站上經常會體現出來。

比如說我們現在要抓取的目標部落格,有幾千萬篇文章,通常這種情況下部落格都會做分頁處理,那麼我們如果按照上面的傳統思路先抓取完list.php的所有頁面起碼就要幾個小時甚至幾天,老闆如果希望你能夠儘快顯示出抓取內容,並且儘快將已經抓取到的內容展現到我們的CMS採集站上,那麼我們就要實現一邊抓取list.php並且把已經抓取到的資料丟入一個article_list陣列,一邊用另一個執行緒從article_list陣列中提取已經抓取到的文章URL地址,然後這個執行緒再去對應的URL地址中用正規表示式取到部落格文章內容。如何實現這個功能呢?

我們就需要同時開啟兩類執行緒,一類執行緒專門負責抓取list.php中的url然後丟入article_list陣列,另外一類執行緒專門負責從article_list中提取出url然後從對應的view.php頁面中抓取出對應的部落格內容。

但是我們是否還記得前面提到過執行緒安全這個概念?前一類執行緒一邊往article_list陣列中寫入資料,另外那一類的執行緒從article_list中讀取資料並且刪除已經讀取完畢的資料。但是python中list並不是執行緒安全版本的資料結構,因此這樣操作會導致不可預料的錯誤。所以我們可以嘗試使用一個更加方便且執行緒安全的資料結構,這就是我們的子標題中所提到的Queue佇列資料結構。

同樣Queue也有一個join()方法,這個join()方法其實和上一個小節所講到的threading中join()方法差不多,只不過在Queue中,join()的阻塞條件是當佇列不為空空的時候才阻塞,否則繼續執行join()後面的程式碼。在這個爬蟲中我便使用了這種方法來阻塞主執行緒而不是直接通過執行緒的join方式來阻塞主執行緒,這樣的好處是可以不用寫一個死迴圈來判斷當前任務佇列中是否還有未執行完的任務,讓程式執行更加高效,也讓程式碼更加優雅。

還有一個細節就是在python2.7中佇列模組的名字是Queue,而在python3.x中已經改名為queue,就是首字母大小寫的區別,大家如果是複製網上的程式碼,要記得這個小區別。

getopt模組:

如果大家學過c語言的話,對這個模組應該會很熟悉,他就是一個負責從命令列中的命令裡面提取出附帶引數的模組。比如說我們通常在命令列中操作mysql資料庫,就是輸入mysql -h127.0.0.1 -uroot -p,其中mysql後面的“-h127.0.0.1 -uroot -p”就是可以獲取的引數部分。

我們平時在編寫爬蟲的時候,有一些引數是需要使用者自己手動輸入的,比如說mysql的主機IP,使用者名稱密碼等等。為了讓我們的程式更加友好通用,有一些配置項是不需要硬編碼在程式碼裡面,而是在執行他的時候我們動態傳入,結合getopt模組我們就可以實現這個功能。

hashlib(雜湊):

雜湊本質上就是一類數學演算法的集合,這種數學演算法有個特性就是你給定一個引數,他能夠輸出另外一個結果,雖然這個結果很短,但是他可以近似認為是獨一無二的。比如說我們平時聽過的md5,sha-1等等,他們都屬於雜湊演算法。他們可以把一些檔案,文字經過一系列的數學運算之後變成短短不到一百位的一段數字英文混合的字串。

python中的hashlib模組就為我們封裝好了這些數學運算函式,我們只需要簡單的呼叫它就可以完成雜湊運算。

為什麼在我這個爬蟲中用到了這個包呢?因為在一些介面請求中,伺服器需要帶上一些校驗碼,保證介面請求的資料沒有被篡改或者丟失,這些校驗碼一般都是hash演算法,所以我們需要用到這個模組來完成這種運算。

json:

很多時候我們抓取到的資料不是html,而是一些json資料,json本質上只是一段含有鍵值對的字串,如果我們需要提取出其中特定的字串,那麼我們需要json這個模組來將這個json字串轉換為dict型別方便我們操作。

re(正規表示式):

有的時候我們抓取到了一些網頁內容,但是我們需要將網頁中的一些特定格式的內容提取出來,比如說電子郵箱的格式一般都是前面幾位英文數字字母加一個@符號加http://xxx.xxx的域名,而要像計算機語言描述這種格式,我們可以使用一種叫做正規表示式的表示式來表達出這種格式,並且讓計算機自動從一大段字串中將符合這種特定格式的文字匹配出來。

sys:

這個模組主要用於處理一些系統方面的事情,在這個爬蟲中我用他來解決輸出編碼問題。

time:

稍微學過一點英語的人都能夠猜出來這個模組用於處理時間,在這個爬蟲中我用它來獲取當前時間戳,然後通過在主執行緒末尾用當前時間戳減去程式開始執行時的時間戳,得到程式的執行時間。

如圖所示,開50個執行緒抓取100頁(每頁30個帖子,相當於抓取了3000個帖子)貼吧帖子內容並且從中提取出手機郵箱這個步驟共耗時330秒。

urllib和urllib2:

這兩個模組都是用於處理一些http請求,以及url格式化方面的事情。我的爬蟲http請求部分的核心程式碼就是使用這個模組完成的。

MySQLdb:

這是一個第三方模組,用於在python中操作mysql資料庫。

這裡我們要注意一個細節問題:mysqldb模組並不是執行緒安全版本,意味著我們不能在多執行緒中共享同一個mysql連線控制程式碼。所以大家可以在我的程式碼中看到,我在每個執行緒的建構函式中都傳入了一個新的mysql連線控制程式碼。因此每個子執行緒只會用自己獨立的mysql連線控制程式碼。

cmd_color_printers:

這也是一個第三方模組,網上能夠找到相關程式碼,這個模組主要用於向命令列中輸出彩色字串。比如說我們通常爬蟲出現錯誤,要輸出紅色的字型會比較顯眼,就要使用到這個模組。

自動化爬蟲的錯誤處理:

如果大家在網路質量不是很好的環境下使用該爬蟲,會發現有的時候會報如圖所示的異常,這是我為了偷懶並沒有寫各種異常處理的邏輯。

通常情況下我們如果要編寫高度自動化的爬蟲,那麼就需要預料到我們的爬蟲可能會遇到的所有異常情況,針對這些異常情況做處理。

比如說如圖所示的錯誤,我們就應該把當時正在處理的任務重新塞入任務佇列,否則我們就會出現遺漏資訊的情況。這也是爬蟲編寫的一個複雜點。

總結:

其實多執行緒爬蟲的編寫也不復雜,多看示例程式碼,多自己動手嘗試,多去社群,論壇交流,很多經典的書上對多執行緒程式設計也有非常詳細的解釋。這篇文章本質上主要還是一篇科普文章,內容講解的都不是很深入,大家還需要課外自己多結合網上各種資料自己學習。

相關文章