醒來的時候登QQ發現有人找我要一份貼吧爬蟲的源代碼,想起之前練手的時候寫過一個抓取百度貼吧發帖記錄中的郵箱與手機號的爬蟲,於是開源分享給大家學習與參考。 需求分析: 本爬蟲主要是對百度貼吧中各種帖子的內容進行抓取,並且分析帖子內容將其中的手機號和郵箱地址抓取出來。主要流程在代碼註釋中有詳細解釋。 測 ...
醒來的時候登QQ發現有人找我要一份貼吧爬蟲的源代碼,想起之前練手的時候寫過一個抓取百度貼吧發帖記錄中的郵箱與手機號的爬蟲,於是開源分享給大家學習與參考。
需求分析:
本爬蟲主要是對百度貼吧中各種帖子的內容進行抓取,並且分析帖子內容將其中的手機號和郵箱地址抓取出來。主要流程在代碼註釋中有詳細解釋。
測試環境:
代碼在Windows7 64bit,python 2.7 64bit(安裝mysqldb擴展)以及centos 6.5,python 2.7(帶mysqldb擴展)環境下測試通過
github:https://github.com/cw1997/get-email-by-tieba/blob/master/get-email-by-tieba-multithreading.py
環境準備:
工欲善其事必先利其器,大家可以從截圖看出我的環境是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:
這也是一個第三方模塊,網上能夠找到相關代碼,這個模塊主要用於向命令行中輸出彩色字元串。比如說我們通常爬蟲出現錯誤,要輸出紅色的字體會比較顯眼,就要使用到這個模塊。
自動化爬蟲的錯誤處理:
如果大家在網路質量不是很好的環境下使用該爬蟲,會發現有的時候會報如圖所示的異常,這是我為了偷懶並沒有寫各種異常處理的邏輯。
通常情況下我們如果要編寫高度自動化的爬蟲,那麼就需要預料到我們的爬蟲可能會遇到的所有異常情況,針對這些異常情況做處理。
比如說如圖所示的錯誤,我們就應該把當時正在處理的任務重新塞入任務隊列,否則我們就會出現遺漏信息的情況。這也是爬蟲編寫的一個複雜點。
總結:
其實多線程爬蟲的編寫也不複雜,多看示例代碼,多自己動手嘗試,多去社區,論壇交流,很多經典的書上對多線程編程也有非常詳細的解釋。這篇文章本質上主要還是一篇科普文章,內容講解的都不是很深入,大家還需要課外自己多結合網上各種資料自己學習。如果對代碼中的邏輯有所不明白可以在評論區下提問,有空我都會耐心解答。
python學習交流群:125240963
轉載至:https://zhuanlan.zhihu.com/p/25039408