本爬蟲主要是對百度貼吧中各種帖子的內容進行抓取,並且分析帖子內容將其中的手機號碼和郵箱地址抓取出來。主要流程在程式碼註解中有詳細解釋。
測試環境:
程式碼在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的時候會報記憶體溢出錯誤。
如果你準備使用mysql儲存數據,建議使用mysql5.5以後的版本,因為mysql5.5版本支援json數據類型,這樣的話可以拋棄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模組編寫多線程程序,就是先自己定義一個類,然後這個類要繼承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:
這也是第三方模組,網路上能夠找到相關程式碼,這個模組主要用於向命令列中輸出彩色字串。比如說我們通常爬蟲會出現錯誤,要輸出紅色的字體會比較顯眼,就要用到這個模組。
自動化爬蟲的錯誤處理:
如果大家在網路品質不是很好的環境下使用該爬蟲,會發現有的時候會報如圖所示的異常,這裡沒有寫異常處理。
通常情況下我們如果要編寫高度自動化的爬蟲,那麼就需要預料到我們的爬蟲可能會遇到的所有異常情況,針對這些異常情況做處理。
比如說如圖所示的錯誤,我們就應該把當時正在處理的任務重新塞入任務隊列,否則我們就會出現遺漏資訊的情況。這也是爬蟲寫的一個複雜點。