爬蟲圖解:輪子哥關注了哪些人

這不是 UC 式的男默女淚震驚文。

——作者


全文已在個人網站發布:爬蟲圖解:輪子哥關注了哪些人


前言:

本文以知乎大V輪子哥關注的用戶列表作為爬蟲對象,爬取每個用戶的url_token、昵稱、性別、該用戶關注其他用戶的數量、回答數、文章數、一句話介紹和頭像的鏈接。並將這些數據持久化存儲在資料庫中,將用戶頭像下載到本地。最後簡單使用 Python 中的 matplotlib 庫將爬取的部分信息可視化。


爬蟲思路:

打開輪子哥知乎上關注的 用戶列表 ,調出Chrome的開發者工具(Developer Tools),重新刷新頁面,選擇『XHR』,第一個就是我們需要的請求信息:

我們觀察 請求頭 Headers,得知實際該頁面的數據請求URL為:

分析這個URL的欄位,其中excited-vczh為輪子哥的url_token,是知乎識別唯一用戶的欄位,followees代表是輪子哥的關注列表,如果是followers,代表輪子哥的粉絲列表,不難發現URL中間部分包含data、answer_count、articles_count、gender、follower_count等欄位信息,而這些信息正是我們想要的。再看最後的兩個欄位是offset,代表用戶列表頁起始數量,limit代表每頁返回用戶信息的數量。

點擊『Preview』,可以看到格式化後響應的Json數據,源數據可以在『Response』查看。

查看 Json 數據結構:

根據欄位見名知意,其中 avatar_url 就是用戶頭像鏈接,avatar_url_template 是頭像的路由模板,在該鏈接中有個 {size} 參數,當我們點擊查看網頁源碼發現,size 為 xll 表示頭像大圖,所以在程序中,我們每爬到這個鏈接都需要把 {size} 參數替換成 xll,這樣就可以爬取用戶較為清楚的頭像,這個 Json 中還有個 badge 欄位,表示該用戶所獲得的徽章,也就是在知乎的成就,比如各個領域的高質量答主,但不是本次爬蟲所關心的。每一次請求,都是20條用戶數據:

那麼問題來了,這時我們確實能夠爬蟲這20個被輪子哥關注的用戶的一些數據,但是輪子哥關注的用戶肯定不止20個,那麼其他用戶的數據該怎麼爬?繼續觀察 Json 數據:

有個『paging』欄位,該欄位下封裝了『is_end』『totals』『previous』『is_start』『next』五個參數,分別代表:

  • 是否是輪子哥關注用戶列表的最後一頁URL
  • 輪子哥一共關注了多少人
  • 當前關注用戶列表頁的前一頁的URL
  • 是否是輪子哥關注用戶列表的第一頁URL
  • 當前關注用戶列表頁的下一頁的URL

我們找到輪子哥關注列表的第一頁開始爬蟲,只需要每頁爬蟲20個用戶數據,就獲得『next』的值,即下一頁URL,可以繼續爬蟲,直至所有輪子哥關注的用戶全部爬取完成。

實現方法:

在網頁瀏覽時,我們可以不用登錄知乎帳號就可以查看知乎某個用戶關注了哪些人,當我們在沒有登錄知乎的時候,把上述真正的用戶列表URL在地址欄訪問時,其實是查看不了的:

會顯示 error,錯誤原因是無效授權請求,即對真正的 URL 訪問是需要登錄的,這樣做是避免爬蟲,增加爬蟲的小障礙,所以程序中要模擬登錄,需要模擬成瀏覽器,避免別服務端發現我們是爬蟲把我們拒之門外。驗證一下,當我們在網頁中登錄進知乎,再次輸入這個真正的 URL 鏈接:

說明登錄進知乎,我們得到我們想要的數據,這些數據和之前在 Chrome 開發者模式『Preview』 中的格式化的數據是一模一樣的。

這是非常小規模的爬蟲,純粹使用 urllib 庫足以完成需求,對於模擬登錄,可以查找登錄的實際請求 URL ,根據規則用鍵值對的形式封裝帳號、密碼形成 post 請求的數據。另一種是直接是在瀏覽器中登錄後,把整個 cookie 放進程序的請求頭中,當然,為了更加逼真體現我們是合法的請求,把『user-agent』『accept』『accept-language』等信息也放進請求中。在爬蟲的同時,也將輪子哥關注的用戶的信息進行 Mysql 資料庫本地持久化,以便後續的數據可視化,同時也將輪子哥關注的用戶的頭像存在本地。


結果:

  • 數據持久化


  • 頭像爬取

  • 數據可視化

輪子哥帶逛聞名遐邇,今日一見,名不虛傳。輪子哥關注的用戶中接近三分之二是女性用戶,接近三分之一是男性用戶。

被輪子哥關注的用戶中,這些用戶的粉絲數量分布還是很均勻的,他們的粉絲數在 0-1k 範圍內,佔到輪子哥關注的用戶的一半比例,輪子哥關注的大於十萬粉的用戶也有72位,其中輪子哥關注的用戶中粉絲數最多是張佳瑋老師,接近160w粉。

輪子哥關注的用戶的回答數分布如上圖,其中最高的三個柱子是回答了:1~5、51~100、11~20,其中1~5個回答數人數最多,有410個用戶。其中,回答數11~50和回答數大於51的人數是差不多的, 這個是很有意思的情況。

在輪子哥關注的用戶中,有82.49%的用戶個人信息中含有『一句話介紹』,17.51%的用戶是沒有『一句話介紹』。

輪子哥關注的用戶中,有接近80%的人是有文章輸出的,而這個比例,接近之前含有『一句話介紹』的用戶數量。


代碼:

# -*- coding: utf-8 -*-import urllib.requestimport urllib.errorimport http.cookiejarimport jsonimport timeimport pymysqlimport matplotlib.pyplot as plt# 創建連接conn = pymysql.connect(host=127.0.0.1, port=3306, user=root, passwd=root, db=spider, use_unicode=True, charset="utf8")cursor = conn.cursor()cjar = http.cookiejar.CookieJar()headers = { accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8, # 注意瀏覽器里的encoding 可能是gzip,deflate 而這裡需要utf-8 避免不能解碼 accept-encoding: utf-8, accept-language: zh-CN,zh;q=0.9, referer: https://www.zhihu.com/, user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36, Cookie: 這裡填寫你自己的登錄知乎的cookie }headers_all = []opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cjar))for k, v in headers.items(): item = (k, v) headers_all.append(item)opener.addheaders = headers_allurllib.request.install_opener(opener)def followers(url): content = urllib.request.urlopen(url).read().decode(utf-8) print(content) j = json.loads(content) # 如果data數據為空說明已爬取最後一頁,結束爬蟲 # 雖然響應數據中有is_end 判斷,但是在倒數第二頁有is_end為true,所以不好直接用is_end欄位判斷是否爬取完成,否則會造成最後一頁爬取不到 data = j[data] if not data: return images = {} for d in data: token = d[url_token] name = d[name] if d[gender] == 1: gender = elif d[gender] == -1: gender = 性別未知 else: gender = follower_count = d[follower_count] avatar_url = d[avatar_url_template] answer_count = d[answer_count] articles_count = d[articles_count] headline = d[headline] # 將頭像url模板中的{size}設置成xll avatar_url = avatar_url.replace({size}, xll) # 數據存儲 images[name] = avatar_url image_name = /Users/rundouble/Desktop/excited-vczh_followers/ + name + .jpg urllib.request.urlretrieve(avatar_url, image_name) print(name+ +gender+ +str(follower_count)) kv = [follower_count, name] info.append(kv) cursor.execute("insert into zhihu_info(token, name, gender, follower_count, answer_count, articles_count, headline, avatar_url) values(%s, %s, %s ,%s, %s ,%s, %s, %s)", (token, name, gender, follower_count, answer_count, articles_count, headline, avatar_url)) conn.commit() url = j[paging][next] if url: time.sleep(2) followers(url)def visualize(): # 性別餅狀圖 sizes = [] cursor.execute(select count(*) from zhihu_info where gender="男";) sizes.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where gender="女";) sizes.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where gender="性別未知";) sizes.append(cursor.fetchone()[0]) labels = Male:+str(sizes[0]), Female:+str(sizes[1]), Unknown:+str(sizes[2]) explode = [0, 0.1, 0] plt.title(Sex Distribution of Followers) plt.pie(sizes, explode=explode, labels=labels, autopct=%1.2f%%, startangle=90) plt.show() # 是否含有簡介的餅狀圖 sizes = [] cursor.execute(select count(*) from zhihu_info where headline!="") sizes.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where headline="") sizes.append(cursor.fetchone()[0]) labels = Contains introduction:+str(sizes[0]), Does not include introduction:+str(sizes[1]) explode = [0.1, 0] plt.title(Introduction Distribution of Followers) plt.pie(sizes, labels=labels, explode=explode, autopct=%1.2f%%, startangle=90) plt.show() # 關注用戶的粉絲數量分布計算 data = [] cursor.execute(select COUNT(*) from zhihu_info where follower_count <=100) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>100 and follower_count <=500) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>500 and follower_count <=1000) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>1000 and follower_count<=2000) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>2000 and follower_count<=5000) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>5000 and follower_count<=10000) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count>10000 and follower_count<=100000) data.append(cursor.fetchone()[0]) cursor.execute(select COUNT(*) from zhihu_info where follower_count >100000) data.append(cursor.fetchone()[0]) print(data) labels = [0-100, 10-500, 500-1k, 1k-2k, 2k-5k, 5k-1w, 1w-10w, >10w] plt.bar(range(len(data)), data, tick_label=labels, color=rgb) plt.title(Followers Fan Distribution) plt.show() # 關注用戶的回答數分布圖 data = [] cursor.execute(select count(*) from zhihu_info where answer_count=0) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>0 and answer_count<=5) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>5 and answer_count<=10) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>10 and answer_count<=20) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>20 and answer_count<=30) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>30 and answer_count<=40) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>40 and answer_count<=50) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>50 and answer_count<=100) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>100 and answer_count<=200) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>200 and answer_count<=500) data.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where answer_count>500) data.append(cursor.fetchone()[0]) print(data) labels = [0, 1-5, 6-10, 11-20, 21-30, 31-40, 41-50, 51-100, 101-200, 201-500, >500] plt.title(Number of Answers Distribution) plt.bar(range(len(data)), data, width=0.5, tick_label=labels, color=rgb) plt.show() # 用戶是否寫文章的餅狀圖 sizes = [] cursor.execute(select count(*) from zhihu_info where articles_count=0) sizes.append(cursor.fetchone()[0]) cursor.execute(select count(*) from zhihu_info where articles_count!=0) sizes.append(cursor.fetchone()[0]) labels = No Article user: + str(sizes[0]), Article user: + str(sizes[1]) explode = [0, 0.1] plt.title(Articles Distribution of Followers) print(sizes) plt.pie(sizes, labels=labels, explode=explode, autopct=%1.2f%%, startangle=90) plt.show()url = https://www.zhihu.com/api/v4/members/excited-vczh/followees?include=data%5B*%5D.answer_count%2Carticles_count%2Cgender%2Cfollower_count%2Cis_followed%2Cis_following%2Cbadge%5B%3F(type%3Dbest_answerer)%5D.topics&offset=20&limit=20followers(url)visualize()


注意:

  • 代碼中的 Mysql 的連接需要換成你自己的資料庫、表、用戶名和密碼
  • cookie 使用你登錄知乎的cookie
  • 頭像本地存儲需要對/Users/rundouble/Desktop/excited-vczh_followers/ + name + .jpg修改成你的路徑
  • 對URL中的 url_token 可以換成其他用戶的url_token 也可以正常使用
  • 代碼中一共兩個方法,一個是 followers(url) 進行數據的爬蟲,另一個是 visualize() 對爬取到數據進行簡單的可視化
  • 本次爬蟲時間截止於:2018.3.28

本文完。


推薦閱讀:

Python在Finance上的應用:獲取股票價格
Pandas筆記|【機器學習集訓營學員】(下)
xpath 使用教程
Python學習基礎知識小結
PY交易(二)使用Pygame寫一個小遊戲——Pie

TAG:編程 | 互聯網 | Python |