前言
很早就有采集知乎用戶數(shù)據(jù)的想法揽乱,要實現(xiàn)這個想法名眉,需要寫一個網(wǎng)絡爬蟲(Web Spider)。因為在學習 python锤窑,正好 python 寫爬蟲也是極好的選擇璧针,于是就寫了一個基于 python 的網(wǎng)絡爬蟲。
幾個月前寫了爬蟲的初版渊啰,后來因為一些原因探橱,暫時擱置了下來,最近重新拾起這個想法绘证。首先優(yōu)化了代碼的結(jié)構(gòu)隧膏,然后在學弟的提醒下,從多線程改成了多進程嚷那,一臺機器上運行一個爬蟲程序胞枕,會啟動幾百個子進程加速抓取。
但是一臺機器的性能是有極限的魏宽,所以后來我使用 MongoDB 和 Redis 搭建了一個主從結(jié)構(gòu)的分布式爬取系統(tǒng)腐泻,來進一步加快抓取的速度。
然后我就去好幾個服務器廠商申請免費的試用队询,比如百度云派桩、騰訊云、Ucloud…… 加上自己的筆記本蚌斩,斷斷續(xù)續(xù)抓取了一個多周铆惑,才采集到300萬知乎用戶數(shù)據(jù)。中間還跑壞了運行網(wǎng)站的云主機送膳,還好 自動備份 起作用员魏,數(shù)據(jù)沒有丟失,但那又是另外一個故事了……
完整版的爬蟲鏈接在這兒:windcode/zhihu-crawler-people叠聋,走過路過別忘了點個 star ~
廢話不多說撕阎,下面我介紹一下如何寫一個簡單的分布式知乎爬蟲。
抓取知乎用戶的個人信息
我們要抓取知乎用戶數(shù)據(jù)碌补,首先要知道在哪個頁面可以抓取到用戶的數(shù)據(jù)闻书。知乎用戶的個人信息在哪里呢名斟,當然是在用戶的主頁啦,我們以輪子哥為例 ~
紅框里的便我們要抓取的用戶關(guān)鍵信息(的一部分)。
最上面是我們的目標URL:https://www.zhihu.com/people/excited-vczh/answers
钉稍。
觀察一下這個URL的組成:
http://www.zhihu.com + /people + /excited-vczh + /answer
可以發(fā)現(xiàn)只有 excited-vczh 這部分是會變化的粱檀,它代表著知乎用戶的唯一ID,在知乎的數(shù)據(jù)格式中律胀,它的鍵名叫做 urlToken。
所以我們可以用拼接字符串的形式,得到我們待抓取頁面的URL:
url = '%s/people/%s/answers'%(host,urlToken)
頁面URL有了晃择,而且從上圖我們可以發(fā)現(xiàn) 不登錄 也可以訪問用戶主頁,這說明我們可以不用考慮模擬登陸的問題也物,可以自由的獲取用戶主頁面源碼宫屠。
那么我們?nèi)绾螐挠脩糁黜摰脑创a中獲取用戶的數(shù)據(jù)呢?一開始我以為需要挨個匹配頁面中對應的部分滑蚯,但我查看源碼的時候發(fā)現(xiàn)知乎把用戶數(shù)據(jù)集集中放到了源碼的一個地方浪蹂,那就是 id="data" 的 div 的 data-state 屬性的值中,看下圖:
從上圖我們可以發(fā)現(xiàn)告材,date-state 的屬性值中藏有用戶的信息坤次,比如我們可以依次找到用戶的教育經(jīng)歷(educations)、簡介(headline)斥赋、參與的 Live 數(shù)量(participatedLiveCount)缰猴、關(guān)注的收藏夾數(shù)量(followingFavlistsCount)、被收藏的次數(shù)(favoritedCount)疤剑、關(guān)注他的用戶數(shù)(followerCount)滑绒、關(guān)注的話題數(shù)量(followingTopicCount)、用戶描述(description)等信息隘膘。通過觀察我們也可以發(fā)現(xiàn)疑故,數(shù)據(jù)應該是以 JSON 格式存儲。
知道了用戶數(shù)據(jù)都藏在 date-state 中棘幸,我們 用 BeautifulSoup 把該屬性的值取出來焰扳,然后作為 JSON 格式讀取,再把數(shù)據(jù)集中存儲用戶數(shù)據(jù)的部分提取出來即可误续,看代碼:
# 解析html
s = BS(html,'html.parser')
# 獲得該用戶藏在主頁面中的json格式數(shù)據(jù)集
data = s.find('div',attrs={'id':'data'})['data-state']
data = json.loads(data)
data = data['entities']['users'][urlToken]
如此吨悍,我們便得到了某一個用戶的個人信息。
抓取知乎用戶的關(guān)注者列表
剛剛我們討論到可以通過抓取用戶主頁面源碼來獲取個人信息蹋嵌,而用戶主頁面可以通過拼接字符串的形式得到 URL育瓜,其中拼接的關(guān)鍵是 如何獲取用戶唯一ID —— urlToken?
我采用的方法是 抓取用戶的關(guān)注者列表栽烂。
每個用戶都會有關(guān)注者列表躏仇,比如輪子哥的:
和獲取個人信息同樣的方法恋脚,我們可以在該頁面源碼的 date-state 屬性值中找到關(guān)注他的用戶(一部分):
名為 ids 的鍵值中存儲有當前列表頁的所有用戶的 urlToken,默認列表的每一頁顯示20個用戶焰手,所以我們寫一個循環(huán)便可以獲取當前頁該用戶的所有關(guān)注者的 urlToken糟描。
# 解析當前頁的 html
url = '%s/people/%s/followers?page=%d'%(host,urlToken,page)
html = c.get_html(url)
s = BS(html,'html.parser')
# 獲得當前頁的所有關(guān)注用戶
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)
再寫一個循環(huán)遍歷關(guān)注者列表的所有頁,便可以獲取用戶的所有關(guān)注者的 urlToken书妻。
有了每個用戶在知乎的唯一ID船响,我們便可以通過拼接這個ID得到每個用戶的主頁面URL,進一步獲取到每個用戶的個人信息躲履。
我選擇抓取的是用戶的關(guān)注者列表见间,即關(guān)注這個用戶的所有用戶(follower)的列表,其實你也可以選擇抓取用戶的關(guān)注列表(following)工猜。我希望抓取更多知乎非典型用戶(潛水用戶)米诉,于是選擇了抓取關(guān)注者列表。當時抓取的時候有這樣的擔心篷帅,萬一這樣抓不到主流用戶怎么辦史侣?畢竟很多知乎大V雖然關(guān)注者很多,但是主動關(guān)注的人相對都很少犹褒,而且關(guān)注的很可能也是大V抵窒。但事實證明,主流用戶基本都抓取到了叠骑,看來基數(shù)提上來后李皇,總有縫隙出現(xiàn)。
反爬蟲機制
頻繁抓取會被知乎封IP宙枷,也就是常說的反爬蟲手段之一掉房,不過俗話說“道高一尺,魔高一丈”慰丛,既然有反爬蟲手段卓囚,那么就一定有反反爬蟲手段,咳诅病,我自己起的名……
言歸正傳哪亿,如果知乎封了你的IP,那么怎么辦呢贤笆?很簡單蝇棉,換一個IP。這樣的思想催生了 代理IP池 的誕生芥永。所謂代理IP池篡殷,是一個代理IP的集合,使用代理IP可以偽裝你的訪問請求埋涧,讓服務器以為你來自不同的機器板辽。
于是我的 應對知乎反爬蟲機制的策略 就很簡單了:全力抓取知乎頁面 --> 被知乎封IP --> 換代理IP --> 繼續(xù)抓 --> 知乎繼續(xù)封 --> 繼續(xù)換 IP..... (手動斜眼)
使用 代理IP池奇瘦,你可以選擇用付費的服務,也可以選擇自己寫一個劲弦,或者選擇用現(xiàn)成的輪子耳标。我選擇用七夜寫的 qiyeboy/IPProxyPool 搭建代理池服務,部署好之后邑跪,修改了一下代碼讓它只保存https協(xié)議的代理IP麻捻,因為 使用http協(xié)議的IP訪問知乎會被拒絕。
搭建好代理池服務后呀袱,我們便可以隨時在代碼中獲取以及使用代理 IP 來偽裝我們的訪問請求啦!
(其實反爬手段有很多郑叠,代理池只是其中一種)
簡單的分布式架構(gòu)
多線程/多進程只是最大限度的利用了單臺機器的性能夜赵,如果要利用多臺機器的性能,便需要分布式的支持乡革。
如何搭建一個簡單的分布式爬蟲寇僧?
我采用了 主從結(jié)構(gòu),即一臺主機負責調(diào)度沸版、管理待抓取節(jié)點嘁傀,多臺從機負責具體的抓取工作。
具體到這個知乎爬蟲來說视粮,主機上搭建了兩個數(shù)據(jù)庫:MongoDB 和 Redis细办。MongoDB 負責存儲抓取到的知乎用戶數(shù)據(jù),Redis 負責維護待抓取節(jié)點集合蕾殴。從機上可以運行兩個不同的爬蟲程序笑撞,一個是抓取用戶關(guān)注者列表的爬蟲(list_crawler),一個是抓取用戶個人資料的爬蟲(info_crawler)钓觉,他們可以配合使用茴肥,但是互不影響。
我們重點講講主機上維護的集合荡灾,主機的 Redis 數(shù)據(jù)庫中一共維護了5個集合:
- waiting:待抓取節(jié)點集合
- info_success:個人信息抓取成功節(jié)點集合
- info_failed:個人信息抓取失敗節(jié)點集合
- list_success:關(guān)注列表抓取成功節(jié)點集合
- list_failed:關(guān)注列表抓取失敗節(jié)點集合
這里插一句瓤狐,之所以采用集合(set),而不采用隊列(queue)批幌,是因為集合天然的帶有唯一性础锐,也就是說可以加入集合的節(jié)點一定是集合中沒有出現(xiàn)過的節(jié)點,這里在5個集合中流通的節(jié)點其實是 urlToken逼裆。
(其實集合可以縮減為3個郁稍,省去失敗集合,失敗則重新投入原來的集合胜宇,但我為了測速所以保留了5個集合的結(jié)構(gòu))
他們的關(guān)系是:
舉個具體的栗子:從一個 urlToken 在 waiting 集合中出現(xiàn)開始耀怜,經(jīng)過一段時間恢着,它被 info_crawler 爬蟲程序從 waiting 集合中隨機獲取到,然后在 info_crawler 爬蟲程序中抓取個人信息财破,如果抓取成功將個人信息存儲到主機的 MongoDB 中掰派,將該 urlToken 放到 info_success 集合中;如果抓取失敗則將該 urlToken 放置到 info_failed 集合中左痢。下一個階段靡羡,經(jīng)過一段時間后,list_crawler 爬蟲程序?qū)?info_success 集合中隨機獲取到該 urlToken俊性,然后嘗試抓取該 urlToken 代表用戶的關(guān)注者列表略步,如果關(guān)注者列表抓取成功,則將抓取到的所有關(guān)注者放入到 waiting 集合中定页,將該 urlToken 放到 list_success 集合中趟薄;如果抓取失敗,將該 urlToken 放置到 list_failed 集合中典徊。
如此杭煎,主機維護的數(shù)據(jù)庫,配合從機的 info_crawler 和 list_crawler 爬蟲程序卒落,便可以循環(huán)起來:info_crawler 不斷從 waiting 集合中獲取節(jié)點羡铲,抓取個人信息,存入數(shù)據(jù)庫儡毕;list_crawler 不斷的補充 waiting 集合也切。
主機和從機的關(guān)系如下圖:
主機是一臺外網(wǎng)/局域網(wǎng)可以訪問的“服務器”,從機可以是PC/筆記本/Mac/服務器妥曲,這個架構(gòu)可以部署在外網(wǎng)也可以部署在內(nèi)網(wǎng)贾费。
后記
本文分享的是如何寫一個簡單的分布式知乎爬蟲,但愿能帶給你啟發(fā)檐盟。
采用這個分布式爬蟲的思路抓取數(shù)據(jù)然后分析的文章在這里:
大數(shù)據(jù)報告:知乎百萬用戶分析
原創(chuàng)聲明
作者:囈語
微信公眾號:囈語的黑板報
轉(zhuǎn)載請注明:囈語 ? 如何寫一個簡單的分布式知乎爬蟲褂萧?