分散式爬蟲很難嗎?用Python寫一個小白也能聽懂的分散式知乎爬蟲

雁橫發表於2018-05-04

oLT1QEwrG3qKhB674rStBORSD2Cb07GuPUzZMKCP

前言

很早就有采集知乎使用者資料的想法,要實現這個想法,需要寫一個網路爬蟲(Web Spider)。因為在學習 python,正好 python 寫爬蟲也是極好的選擇,於是就寫了一個基於 python 的網路爬蟲。

幾個月前寫了爬蟲的初版,後來因為一些原因,暫時擱置了下來,最近重新拾起這個想法。首先優化了程式碼的結構,然後在學弟的提醒下,從多執行緒改成了多程式,一臺機器上執行一個爬蟲程式,會啟動幾百個子程式加速抓取。

但是一臺機器的效能是有極限的,所以後來我使用 MongoDB 和 Redis 搭建了一個主從結構的分散式爬取系統,來進一步加快抓取的速度。

然後我就去好幾個伺服器廠商申請免費的試用,比如百度雲、騰訊雲、Ucloud…… 加上自己的筆記本,斷斷續續抓取了一個多周,才採集到300萬知乎使用者資料。中間還跑壞了執行網站的雲主機,還好 自動備份 起作用,資料沒有丟失,但那又是另外一個故事了……

廢話不多說,下面我介紹一下如何寫一個簡單的分散式知乎爬蟲。

抓取知乎使用者的個人資訊

給大家推薦一個學習交流的地方,想要學習Python的小夥伴可以一起來學習,719+139+688,入坑需謹慎,對Python沒啥興趣的就不要來湊熱鬧啦。我們要抓取知乎使用者資料,首先要知道在哪個頁面可以抓取到使用者的資料。知乎使用者的個人資訊在哪裡呢,當然是在使用者的主頁啦,我們以輪子哥為例 ~

22xYWGpZ17SDG9KWH84QY0nTkU6i=fQqhJITQh71

紅框裡的便我們要抓取的使用者關鍵資訊(的一部分)。

最上面是我們的目標URL:https://www.zhihu.com/people/excited-vczh/answers

觀察一下這個URL的組成:

http://www.zhihu.com + /people + /excited-vczh + /answer

可以發現只有 excited-vczh 這部分是會變化的,它代表著知乎使用者的唯一ID,在知乎的資料格式中,它的鍵名叫做 urlToken

所以我們可以用拼接字串的形式,得到我們待抓取頁面的URL:

url = `%s/people/%s/answers`%(host,urlToken)

頁面URL有了,而且從上圖我們可以發現 不登入 也可以訪問使用者主頁,這說明我們可以不用考慮模擬登陸的問題,可以自由的獲取使用者主頁面原始碼。

那麼我們如何從使用者主頁的原始碼中獲取使用者的資料呢?一開始我以為需要挨個匹配頁面中對應的部分,但我檢視原始碼的時候發現知乎把使用者資料集集中放到了原始碼的一個地方,那就是 id=”data” 的 div 的 data-state 屬性的值中,看下圖:

XhNrjl14CEJlU9fkdpYGjSXUOgKmfIxrf=oh1Eb6

從上圖我們可以發現,date-state 的屬性值中藏有使用者的資訊,比如我們可以依次找到使用者的教育經歷(educations)、簡介(headline)、參與的 Live 數量(participatedLiveCount)、關注的收藏夾數量(followingFavlistsCount)、被收藏的次數(favoritedCount)、關注他的使用者數(followerCount)、關注的話題數量(followingTopicCount)、使用者描述(description)等資訊。通過觀察我們也可以發現,資料應該是以 JSON 格式儲存。

知道了使用者資料都藏在 date-state 中,我們 用 BeautifulSoup 把該屬性的值取出來,然後作為 JSON 格式讀取,再把資料集中儲存使用者資料的部分提取出來即可,看程式碼:

# 解析htmls = BS(html,`html.parser`)# 獲得該使用者藏在主頁面中的json格式資料集data = s.find(`div`,attrs={`id`:`data`})[`data-state`]

data = json.loads(data)

data = data[`entities`][`users`][urlToken]

如此,我們便得到了某一個使用者的個人資訊。

抓取知乎使用者的關注者列表

剛剛我們討論到可以通過抓取使用者主頁面原始碼來獲取個人資訊,而使用者主頁面可以通過拼接字串的形式得到 URL,其中拼接的關鍵是 如何獲取使用者唯一ID —— urlToken

我採用的方法是 抓取使用者的關注者列表

每個使用者都會有關注者列表,比如輪子哥的:

mvTlXLCKd=Y94FZKLq1J2gNOUr0Efkx7zHs=7Tzy

請點選此處輸入圖片描述

OQ1O3SkxVEsnUouiAKowXpwzXNz1sspfgzcUFRSx

和獲取個人資訊同樣的方法,我們可以在該頁面原始碼的 date-state 屬性值中找到關注他的使用者(一部分):

DmTfXe1MweXyeoUZ9wNnLn91YvQuMvQPSocAU1Uq

請點選此處輸入圖片描述

名為 ids 的鍵值中儲存有當前列表頁的所有使用者的 urlToken,預設列表的每一頁顯示20個使用者,所以我們寫一個迴圈便可以獲取當前頁該使用者的所有關注者的 urlToken

# 解析當前頁的 html   url = `%s/people/%s/followers?page=%d`%(host,urlToken,page)

html = c.get_html(url)

s = BS(html,`html.parser`)# 獲得當前頁的所有關注使用者data = s.find(`div`,attrs={`id`:`data`})[`data-state`]

data = json.loads(data)

items = data[`people`][`followersByUser`][urlToken][`ids`]for item in items:    if item!=None and item!=False and item!=True and item!=`知乎使用者`.decode(`utf8`):

       node = item.encode(`utf8`)

       follower_list.append(node)

再寫一個迴圈遍歷關注者列表的所有頁,便可以獲取使用者的所有關注者的 urlToken。

有了每個使用者在知乎的唯一ID,我們便可以通過拼接這個ID得到每個使用者的主頁面URL,進一步獲取到每個使用者的個人資訊。

我選擇抓取的是使用者的關注者列表,即關注這個使用者的所有使用者(follower)的列表,其實你也可以選擇抓取使用者的關注列表(following)。我希望抓取更多知乎非典型使用者(潛水使用者),於是選擇了抓取關注者列表。當時抓取的時候有這樣的擔心,萬一這樣抓不到主流使用者怎麼辦?畢竟很多知乎大V雖然關注者很多,但是主動關注的人相對都很少,而且關注的很可能也是大V。但事實證明,主流使用者基本都抓取到了,看來基數提上來後,總有縫隙出現。

反爬蟲機制

頻繁抓取會被知乎封IP,也就是常說的反爬蟲手段之一,不過俗話說“道高一尺,魔高一丈”,既然有反爬蟲手段,那麼就一定有反反爬蟲手段,咳,我自己起的名……

言歸正傳,如果知乎封了你的IP,那麼怎麼辦呢?很簡單,換一個IP。這樣的思想催生了 代理IP池 的誕生。所謂代理IP池,是一個代理IP的集合,使用代理IP可以偽裝你的訪問請求,讓伺服器以為你來自不同的機器。

於是我的 應對知乎反爬蟲機制的策略 就很簡單了:全力抓取知乎頁面 –> 被知乎封IP –> 換代理IP –> 繼續抓 –> 知乎繼續封 –> 繼續換 IP….. (手動斜眼)

使用 代理IP池,你可以選擇用付費的服務,也可以選擇自己寫一個,或者選擇用現成的輪子。我選擇用七夜寫的 qiyeboy/IPProxyPool 搭建代理池服務,部署好之後,修改了一下程式碼讓它只儲存https協議的代理IP,因為 使用http協議的IP訪問知乎會被拒絕

搭建好代理池服務後,我們便可以隨時在程式碼中獲取以及使用代理 IP 來偽裝我們的訪問請求啦!

(其實反爬手段有很多,代理池只是其中一種)

簡單的分散式架構

多執行緒/多程式只是最大限度的利用了單臺機器的效能,如果要利用多臺機器的效能,便需要分散式的支援。

如何搭建一個簡單的分散式爬蟲?

我採用了 主從結構,即一臺主機負責排程、管理待抓取節點,多臺從機負責具體的抓取工作。

具體到這個知乎爬蟲來說,主機上搭建了兩個資料庫MongoDB 和 Redis。MongoDB 負責儲存抓取到的知乎使用者資料,Redis 負責維護待抓取節點集合。從機上可以執行兩個不同的爬蟲程式,一個是抓取使用者關注者列表的爬蟲(list_crawler),一個是抓取使用者個人資料的爬蟲(info_crawler),他們可以配合使用,但是互不影響。

我們重點講講主機上維護的集合,主機的 Redis 資料庫中一共維護了5個集合:

  • waiting:待抓取節點集合

  • info_success:個人資訊抓取成功節點集合

  • info_failed:個人資訊抓取失敗節點集合

  • list_success:關注列表抓取成功節點集合

  • list_failed:關注列表抓取失敗節點集合

這裡插一句,之所以採用集合(set),而不採用佇列(queue),是因為集合天然的帶有唯一性,也就是說可以加入集合的節點一定是集合中沒有出現過的節點,這裡在5個集合中流通的節點其實是 urlToken

(其實集合可以縮減為3個,省去失敗集合,失敗則重新投入原來的集合,但我為了測速所以保留了5個集合的結構)

他們的關係是:

Pr=mjKBUQToU5jllm=LcpMiPIQzWVpSs5lEU1I8E

請點選此處輸入圖片描述

舉個具體的栗子:從一個 urlToken 在 waiting 集合中出現開始,經過一段時間,它被 info_crawler 爬蟲程式從 waiting 集合中隨機獲取到,然後在 info_crawler 爬蟲程式中抓取個人資訊,如果抓取成功將個人資訊儲存到主機的 MongoDB 中,將該 urlToken 放到 info_success 集合中;如果抓取失敗則將該 urlToken 放置到 info_failed 集合中。下一個階段,經過一段時間後,list_crawler 爬蟲程式將從 info_success 集合中隨機獲取到該 urlToken,然後嘗試抓取該 urlToken 代表使用者的關注者列表,如果關注者列表抓取成功,則將抓取到的所有關注者放入到 waiting 集合中,將該 urlToken 放到 list_success 集合中;如果抓取失敗,將該 urlToken 放置到 list_failed 集合中。

如此,主機維護的資料庫,配合從機的 info_crawler 和 list_crawler 爬蟲程式,便可以迴圈起來:info_crawler 不斷從 waiting 集合中獲取節點,抓取個人資訊,存入資料庫;list_crawler 不斷的補充 waiting 集合

主機和從機的關係如下圖:

BuvrpbKaX6jZ=9JM0lG7TGh7o1EUInNIq=0YLlki

主機是一臺外網/區域網可以訪問的“伺服器”,從機可以是PC/筆記本/Mac/伺服器,這個架構可以部署在外網也可以部署在內網。

X7oKk70LD4bKEmXN4cX1BD583JXhSP2M4AQunNBC


相關文章