當前位置:
首頁 > 知識 > 教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

打開今日頭條,查看更多圖片

作者 | 周志鵬

責編 | 郭芮

本文以騰訊視頻《都挺好》為例,解析彈幕爬取的細節和難點,對思路感興趣的旁友們可以跟著文章邏輯走一遍,對於想直接上手爬的同學,文末已給出完整代碼。

相對於一般電影OR電視劇評論,彈幕能夠貼合劇情,進行更多有意思的腦洞分析。《Python 爬取 394452 條《都挺好》彈幕數據,發現彈幕比劇還精彩?》彈幕分析文章所有數據均基於本文代碼爬取。

前言

每次寫爬蟲,耳畔都會迴響起那句經典的freestyle:「你看這個碗,它又大它又圓,你看這個面,它又長它又寬」!

短短四句,揭示了兩種本質——碗是大和圓的,面是長亦寬的。一秒就看清事物本質的人和一輩子才看透事物本質的人自然過著不同的人生。

所以,寫爬蟲也是一樣的,理清目標數據和網址的變化規律,也就是先看到碗的大和圓,面的長和寬,隨後再去解決細節的數據定位和抓取(欣賞碗的花紋細節,面的Q彈),往往事半功倍。

——這就是我寫爬蟲所信奉的大碗寬面邏輯。

子彈(彈幕)軌跡規律探究

1、數據定位:

打開騰訊視頻的電視劇(這裡以《都挺好》為例),F12審查元素,默默地等待目標獵物出現,因為彈幕是播放時不斷滾動出現,所以我們先假設它在JS下。

正片開始後,一群以「danmu"為開頭的請求不斷載入打破了短暫的平靜,我們把這個疑似目標預覽一下:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

果然,彈幕內容赫然在列,對於我們分析有用的欄位還有彈幕的ID,upcount(點贊數),opername(用戶名)和uservip_degree(會員等級)。

到這一步,我們先不糾結於這個JSON文件要如何偽裝訪問,如何解析,不妨跟隨那句「大碗寬面」的旋律,跳出碗來,看看這個碗是大還是圓(找規律)。

2、彈道(彈幕網址)規律分析:

在找網址規律的時候,有一個小技巧,就是嘗試暴力刪掉目標網址中不影響最終結果的部分參數,再從最精簡的網址中尋找規律。

拿我們第一個彈幕網址來說,原網址是這樣的:

https://mfm.video.qq.com/danmu?otype=json&callback=jQuery19109123255549841207_1553922882824&timestamp=45&target_id=3753912718%26vid%3Dt00306i1e62&count=80&second_count=5&session_key=558401%2C8142%2C1553922887&_=1553922882831

在瀏覽器中打開是這樣的:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

網址最後一串數據好像是時間戳,我們刪掉試試,果然,返回的內容沒變。那個sessiong_key到底影不影響呢?刪了試試,返回內容還是沒變!

刪到最後,我們把原網址精簡成了下面的網址:

https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912718%26vid%3Dt00306i1e62&count=80

我們把第二頁網址也精簡一下:

https://mfm.video.qq.com/danmu?otype=json&timestamp=45&target_id=3753912718%26vid%3Dt00306i1e62&count=80

對比很容易找到規律,從第一頁到第二頁,timestamp值從15變到了45,其他部分沒有任何變化,我有一個大膽的猜測,這個timestamp值是控制頁數的變數,並且是30秒更新一次彈幕。

那一級有多少頁呢?我們把進度條拉到影片結束的邊緣,發現最後一頁的網址的timestamp的值變成了2565。

整個過程,我們只需要構造步長為30的循環變數來替換timestamp參數就可以實現批量訪問了。

到這裡,單集中彈幕動態更新的規律我們已經探究清楚,下面來對單個頁面進行解析。

PS:其實大碗寬面的邏輯下,我們這個時候應該再繼續對比不同集數之間網址變化規律,並找到規律本身,但考慮到內容實操性與可讀性,我們不妨把這一塊往後稍稍。

解析單頁彈幕內容

以第一集第一頁的彈幕為例,我們只進行簡單的headers偽裝,進行訪問嘗試:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

異常順利,成功返回目標結果,而且是友好的JSON格式,我們用JSON來解析一下:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

納尼?結果瘋狂報錯:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

告訴我們在35444的位置有字元問題,經過排查,發現錯誤的原因是解析的部分內容因為格式問題沒有通過JSON語法檢查,解決方法很簡單,我們json.loads中strict參數變成Fasle即可:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

OK,接下來遍歷提取我們需要的關鍵數據:

#存儲數據
df = pd.DataFrame()
#遍歷獲取目標欄位
for i in bs["comments"]:
content = i["content"] #彈幕內容
name = i["opername"] #用戶名
upcount = i["upcount"] #點贊數
user_degree =i["uservip_degree"] #會員等級
timepoint = i["timepoint"] #發布時間
comment_id = i["commentid"] #彈幕ID
cache = pd.DataFrame({"用戶名":[name],"內容":[content],"會員等級":[user_degree],
"評論時間點":[timepoint],"評論點贊":[upcount],"評論id":[comment_id]})
df = pd.concat([df,cache])

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

大寫的EASY!要進行多頁爬取,只需要在外層構造一個循環,以30為步長改變timestamp的變數即可。

不同集之間網址規律探究

單頁、單集的規律都搞清楚了,那不同集之間的網址有什麼規律呢?

第一集是這樣的:

https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912718%26vid%3Dt00306i1e62&count=80

我們把第二集的彈幕網址也暴力精簡:

https://mfm.video.qq.com/danmu?otype=json&timestamp=15&target_id=3753912717%26vid%3Dx003061htl5&count=80

發現是target_id值和%3D後面一串ID(第一集是t00306i1e62,第二集是x003061htl5)的變化決定了不同的集數。為了區分,我們把後面那一串ID叫做後綴ID。而難點就在於他們之間沒有像timestamp那樣明顯的規律可循,彈幕內容所在的網址本身又沒有任何關於兩個ID的信息。

所以,我們必須跳出碗來找線索,看看有沒有又大又黑的鍋裝這些碗(目的在於找到存儲target_id和後面不規則ID的那口大鍋)。

1、找到後綴ID

這個時候,需要一些常識來開路了。我們發現播放視頻的時候,在播放屏右邊總會顯示全部集數:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

點擊對應的集數就會進行相應的換集跳轉,所以我們有理由相信ID相關的鍋藏在其中。重新刷新網頁,很容易找到了他們的蹤跡:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

可以看到,上面截圖中第一集的ID「t00306i1e62」對應著我們前面找到的規律(後綴ID)。打開任意一集,發現1-30集和31-46集相關的後綴ID都分別存儲在兩個相鄰的網頁。

所以,我們先嘗試拿下所有的後綴ID、對應劇集名稱、播放量和集數:

#打開任意一集,1-30和31-46存儲在兩個網頁
part1_url = "https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=b0030velala,t00306i1e62,x003061htl5,b0030velala,w0030ilim7z,i0030r7v63u,z003044noq2,m0030sfinyr,c0030u884k7,k0030m5zbr7,l0030e5nglm,h0030b060vn,j003090ci7w,n0030falyoi,s00308u9kwx,p0030fohijf,g00303ob0cx,v0030960y6n,x0030bl84xw,v0030keuav1,t0030kups1i,n0030y2o52i,x0030s52mev,d0030xuekgw,o0030md1a2a,x0030peo3sk,d00303l5j4k,t0030aexmnt,a0030ybi45z,y0030wpe2wu&callback=jQuery19101240739643414368_1553238198070&_=1553238198071"
part2_url = "https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=t0030epjqsi,g003035mi84,n00301fxqbh,h0030zivlrq,d0030qc1yu2,m0030q9ywxj,h0030j0eq19,j0030jks835,t0030owh5uu,e0030xbj246,a00308xw434,l0030tb319m,a0030mhntt6,t0030wnr3t9,l0030t7o64e,b0030i9bi3o,m0030yklk6j,z0030tgz3pp,r00307wgnly,o00306b4zax,k00309i6ul6,j00304eu73n,v08521l667a,u0851gzzoqi,a0852328197,k0852mb3ymt,v00308p65xf,z08527pia6g,z08520difig,z0852ybpxn0&callback=jQuery19101240739643414368_1553238198072&_=1553238198073"
base_info = pd.DataFrame()
for url in [part1_url,part2_url]:
html = requests.get(url,headers = headers)
bs = json.loads(html.text[html.text.find("{"):-1])

for i in bs["results"]:
#後綴ID
v_id = i["id"]
#這一集的名字,比如「都挺好_01」
title = i["fields"]["title"]
#播放量
view_count = i["fields"]["view_all_count"]
#整型存儲的集數,片花則為0
episode = int(i["fields"]["episode"])
#去掉片花,只留下正片
if episode == 0:
pass
else:
cache = pd.DataFrame({"id":[v_id],"title":[title],"播放量":[view_count],"第幾集":[episode]})
base_info = pd.concat([base_info,cache])

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

OK,非常順利。

目前來說我們拿到了所有的後綴ID,但還是缺少target_id,無法構造完整的網頁進行自動循環爬取。而我們在這兩個網頁中找不到任何和target_id有關的信息,真讓人頭大!

2、死磕target_id

每當沒有頭緒的時候,我總是想起莎翁的那句:

「一切過往,皆為序章。」反之,一切序章,皆有過往,正在發生或者已經發生的萬事萬物一定有跡可循。我們心心念念的target_id一定在某個動態網頁中記錄著。

這個時候就需要耐心的篩選了,最後,我們發現,單集的target_id,隱藏在XHR下的一個"regist"開頭的動態網址中:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

仔細觀察,他是一個POST請求:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

傳遞的參數如下:

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

翻了N集來對比,我們發現不同集數之間網址變化的只有傳入的這個「vecIdList」,裡面的參數正是我們上一步獲取的那些後綴ID。

真相漸漸浮出水面。

3、思路梳理:

  • 第一步,我們搞清楚了單集內部彈幕網址的動態變化,只需要改變timestamp的值即可循環爬取單集所有內容。
  • 第二步,發現要自動爬取每一集,必須先找到構造網址的target_id和後綴的ID
  • 第三步,任意一集網頁中都能直接找到所有劇集的後綴ID(我們已經拿下了所有的後綴ID),但是卻只能在一集中找到單集的一個target_id。
  • 第四步,也就是接下來的一步,我們可以基於已經爬到的後綴ID,去循環訪問每一集,拿到單集對應的target_id,這樣就能構造出完整的彈幕網頁所需的ID們了。

說干就干,循環爬取target_id:

#定義爬取單集target_id的函數
#只需要向函數傳入v_id(後綴ID)和headers
def get_episode_danmu(v_id,headers):
#target_id所在基礎網址
base_url = "https://access.video.qq.com/danmu_manage/regist?vappid=97767206&vsecret=c0bdcbae120669fff425d0ef853674614aa659c605a613a4&raw=1"
#傳遞參數,只需要改變後綴ID
pay = {"wRegistType":2,"vecIdList":[v_id],
"wSpeSource":0,"bIsGetUserCfg":1,
"mapExtData":{v_id:{"strCid":"wu1e7mrffzvibjy","strLid":""}}}

html = requests.post(base_url,data = json.dumps(pay),headers = headers)
bs = json.loads(html.text)
#定位元素
danmu_key = bs["data"]["stMap"][v_id]["strDanMuKey"]
#解析出target_id
target_id = danmu_key[danmu_key.find("targetid") + 9 : danmu_key.find("vid") - 1]
return [v_id,target_id]
info_lst = []
#循環獲取後綴ID並傳遞
for i in base_info["id"]:
#得到每一集的後綴ID和target_id
info = get_episode_danmu(i,headers)
print(info)
info_lst.append(info)
time.sleep(3 + random.random())

噹噹噹噹~結果如下:(截取了部分)

教你如何將《都挺好》中 394452 條彈幕數據爬取分析!| 技術頭條

我們終於集齊了構成單頁彈幕網址所需的target_id,後綴ID,只需要構造兩個循環就可以實現完整的彈幕爬取(第一個循環構造每一集的基礎網頁,第二個循環構造單集內的彈幕頁數)。

目前來說,對於彈幕爬取(騰訊視頻),單純的headers偽裝就能夠暢通無阻,但也建議大家文明爬取,理性分析 :)

至此,我們鍋、碗和面都已經準備到位了,再把剛才各模塊寫的精簡一些,然後就可以酣暢淋漓的吃大碗寬面了。

Skrrrrrrrrrrr~

最後附上完整代碼:

import requests
import json
import pandas as pd
import os
import time
import random
#頁面基本信息解析,獲取構成彈幕網址所需的後綴ID、播放量、集數等信息。
def parse_base_info(url,headers):
df = pd.DataFrame()

html = requests.get(url,headers = headers)
bs = json.loads(html.text[html.text.find("{"):-1])

for i in bs["results"]:
v_id = i["id"]
title = i["fields"]["title"]
view_count = i["fields"]["view_all_count"]
episode = int(i["fields"]["episode"])
if episode == 0:
pass
else:
cache = pd.DataFrame({"id":[v_id],"title":[title],"播放量":[view_count],"第幾集":[episode]})
df = pd.concat([df,cache])
return df
#傳入後綴ID,獲取該集的target_id並返回
def get_episode_danmu(v_id,headers):
base_url = "https://access.video.qq.com/danmu_manage/regist?vappid=97767206&vsecret=c0bdcbae120669fff425d0ef853674614aa659c605a613a4&raw=1"
pay = {"wRegistType":2,"vecIdList":[v_id],
"wSpeSource":0,"bIsGetUserCfg":1,
"mapExtData":{v_id:{"strCid":"wu1e7mrffzvibjy","strLid":""}}}
html = requests.post(base_url,data = json.dumps(pay),headers = headers)
bs = json.loads(html.text)
danmu_key = bs["data"]["stMap"][v_id]["strDanMuKey"]
target_id = danmu_key[danmu_key.find("targetid") + 9 : danmu_key.find("vid") - 1]
return [v_id,target_id]
#解析單個彈幕頁面,需傳入target_id,v_id(後綴ID)和集數(方便匹配),返回具體的彈幕信息
def parse_danmu(url,target_id,v_id,headers,period):
html = requests.get(url,headers = headers)
bs = json.loads(html.text,strict = False)
df = pd.DataFrame()
for i in bs["comments"]:
content = i["content"]
name = i["opername"]
upcount = i["upcount"]
user_degree =i["uservip_degree"]
timepoint = i["timepoint"]
comment_id = i["commentid"]
cache = pd.DataFrame({"用戶名":[name],"內容":[content],"會員等級":[user_degree],
"彈幕時間點":[timepoint],"彈幕點贊":[upcount],"彈幕id":[comment_id],"集數":[period]})
df = pd.concat([df,cache])
return df
#構造單集彈幕的循環網頁,傳入target_id和後綴ID(v_id),通過設置爬取頁數來改變timestamp的值完成翻頁操作
def format_url(target_id,v_id,end = 85):
urls = []
base_url = "https://mfm.video.qq.com/danmu?otype=json&timestamp={}&target_id={}%26vid%3D{}&count=80&second_count=5"

for num in range(15,end * 30 + 15,30):
url = base_url.format(num,target_id,v_id)
urls.append(url)
return urls
def get_all_ids(part1_url,part2_url,headers):
#分別獲取1-30,31-46的所有後綴ID(v_id)
part_1 = parse_base_info(part1_url,headers)
part_2 = parse_base_info(part2_url,headers)
df = pd.concat([part_1,part_2])
df.sort_values("第幾集",ascending = True,inplace = True)
count = 1
#創建一個列表存儲target_id
info_lst = []
for i in df["id"]:
info = get_episode_danmu(i,headers)
info_lst.append(info)
print("正在努力爬取第 %d 集的target_id" % count)
count += 1
time.sleep(2 + random.random())
print("是不是發現多了一集?別擔心,會去重的")
#根據後綴ID,將target_id和後綴ID所在的表合併
info_lst = pd.DataFrame(info_lst)
info_lst.columns = ["v_id","target_id"]
combine = pd.merge(df,info_lst,left_on = "id",right_on = "v_id",how = "inner")
#去重複值
combine = combine.loc[combine.duplicated("id") == False,:]
return combine
#輸入包含v_id,target_id的表,並傳入想要爬取多少集
def crawl_all(combine,num,page,headers):
c = 1
final_result = pd.DataFrame()
#print("Bro,馬上要開始循環爬取每一集的彈幕了")
for v_id,target_id in zip(combine["v_id"][:num],combine["target_id"][:num]):
count = 1
urls = format_url(target_id,v_id,page)
for url in urls:
result = parse_danmu(url,target_id,v_id,headers,c)
final_result = pd.concat([final_result,result])
time.sleep(2+ random.random())
print("這是 %d 集的第 %d 頁爬取.." % (c,count))
count += 1
print("-------------------------------------")
c += 1
return final_result
if __name__ == "__main__":

#《都挺好》1-30集的網址,31-46集的網址
#如果要爬取其他電視劇,只需要根據文章的提示,找到存儲後綴ID的原網址即可
part1_url = "https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=x003061htl5,t00306i1e62,x003061htl5,b0030velala,w0030ilim7z,i0030r7v63u,z003044noq2,m0030sfinyr,c0030u884k7,k0030m5zbr7,l0030e5nglm,h0030b060vn,j003090ci7w,n0030falyoi,s00308u9kwx,p0030fohijf,g00303ob0cx,v0030960y6n,x0030bl84xw,v0030keuav1,t0030kups1i,n0030y2o52i,x0030s52mev,d0030xuekgw,o0030md1a2a,x0030peo3sk,d00303l5j4k,t0030aexmnt,a0030ybi45z,y0030wpe2wu&callback=jQuery191020844423583354543_1554200358596&_=1554200358597"
part2_url = "https://union.video.qq.com/fcgi-bin/data?otype=json&tid=682&appid=20001238&appkey=6c03bbe9658448a4&idlist=t0030epjqsi,g003035mi84,n00301fxqbh,h0030zivlrq,d0030qc1yu2,m0030q9ywxj,h0030j0eq19,j0030jks835,a00308xw434,l0030tb319m,x0030xogl32,g0030fju3w3,a0030vrcww0,l0030jzi1mi,c0030mq8yjr,u00302fdo8v,a0030w9g57k,n0030wnj6i8,j0030h91ouj,j00304eu73n,t00305kc1f5,i0030x490o2,u0030jtmlj2,d003031ey5h,w0850w594k6,l0854pfn9lg,f08546r7l7a,d0854s0oq1z,m08546pcd9k,p0854r1nygj&callback=jQuery191020844423583354543_1554200358598&_=1554200358599"
headers = {"User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36"}

#得到所有的後綴ID,基於後綴ID爬取target_id
combine = get_all_ids(part1_url,part2_url,headers)

#設置要爬取多少集(num參數),每一集爬取多少頁彈幕(1-85頁,page參數),這裡默認是爬取第一集的5頁彈幕
#比如想要爬取30集,每一集85頁,num = 30,page = 85
final_result = crawl_all(combine,num = 1,page = 5,headers = headers)
#final_result.to_excel("xxx.xlsx") 可以輸出成EXCEL格式的文件

作者:周志鵬,2年數據分析,深切感受到數據分析的有趣和學習過程中缺少案例的無奈,遂新開公眾號「數據不吹牛」,定期更新數據分析相關技巧和有趣案例(含實戰數據集),歡迎大家關注交流。聲明:本文為作者投稿,版權歸其所有。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 CSDN 的精彩文章:

我是技術男,也曾創業過,也拿過風投......
救救中國996程序員!GitHub近230000 Star、Python之父伸張正義!

TAG:CSDN |