弹幕id格式错误_让弹幕飞一会儿——腾讯视频弹幕(39W+)爬取实战

7c3f7c7938a1768923f53e53c31542f7.png
本文以腾讯视频(都挺好)为例,解析弹幕爬取的细节和难点,对思路感兴趣的旁友们可以跟着文章逻辑走一遍,对于想直接上手爬的同学,文末已给出完整代码。
相对于一般电影OR电视剧评论,弹幕能够贴合剧情,进行更多有意思的脑洞分析。

注:上一篇《都挺好》弹幕分析文章所有数据(39W+)均基于本文代码爬取。

每次写爬虫,耳畔都会回响起那句经典的freestyle:

你看这个碗,它又大它又圆,你看这个面,它又长它又宽

短短四句,揭示了两种本质——碗是大和圆的,面是长亦宽的。一秒就看清事物本质的人和一辈子才看透事物本质的人自然过着不同的人生。

93410899d7215579e82549e89c2fd7b1.gif

所以,写爬虫也是一样的,理清目标数据和网址的变化规律,也就是先看到碗的大和圆,面的长和宽,随后再去解决细节的数据定位和抓取(欣赏碗的花纹细节,面的Q弹),往往事半功倍。

#这就是我写爬虫所信奉的大碗宽面逻辑。

01 子弹(弹幕)轨迹规律探究

1、数据定位:

打开腾讯视频的电视剧(这里以《都挺好》为例),F12审查元素,默默的等待目标猎物出现,因为弹幕是播放时不断滚动出现,所以我们先假设它在JS下。

正片开始后,一群以“danmu"为开头的请求不断加载打破了短暂的平静,我们把这个疑似目标预览一下:

620be9acc492a2c0b71fbb6a24845176.png

果然,弹幕内容赫然在列,对于我们分析有用的字段还有弹幕的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

在浏览器中打开是这样的:

cf047a122f70566b4b3fe170fa8287eb.png

网址最后一串数据好像是时间戳,我们删掉试试,果然,返回的内容没变。那个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:其实大碗宽面的逻辑下,我们这个时候应该再继续对比不同集数之间网址变化规律,并找到规律本身,但考虑到内容实操性与可读性,我们不妨把这一块往后稍稍)

02 解析单页弹幕内容

以第一集第一页的弹幕为例,我们只进行简单的headers伪装,进行访问尝试:

88dc1a8012eecc4f5505282b2b5fbc46.png

异常顺利,成功返回目标结果,而且是友好的JSON格式,我们用JSON来解析一下:

7bbe09a36e99f0d9f58f38c27d4cf082.png

纳尼?结果疯狂报错:

b66e213593cd20ad0c9e0197fdd70395.png

告诉我们在35444的位置有字符问题,经过排查,发现错误的原因是解析的部分内容因为格式问题没有通过JSON语法检查,解决方法很简单,我们json.loads中strict参数变成Fasle即可:

009fd107fdd655c32e7dac82e8a3e255.png

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])

0f491ae5769ee3690d71b26b95211c00.png

大写的EASY!要进行多页爬取,只需要在外层构造一个循环,以30为步长改变timestamp的变量即可。

03 不同集之间网址规律探究

单页、单集的规律都搞清楚了,那不同集之间的网址有什么规律呢?

第一集是这样的:

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

这个时候,需要一些常识来开路了。我们发现播放视频的时候,在播放屏右边总会显示全部集数:

0870949c21e10cd6e5eb0ab17453e509.png

点击对应的集数就会进行相应的换集跳转,所以我们有理由相信ID相关的锅藏在其中。重新刷新网页,很容易找到了他们的踪迹:

22f3bcaaa74e622e66f8692e074b147f.png

可以看到,上面截图中第一集的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])

5d239fc67deb20211c1856e18c24c5dc.png


OK,非常顺利。

目前来说我们拿到了所有的后缀ID,但还是缺少target_id,无法构造完整的网页进行自动循环爬取。而我们在这两个网页中找不到任何和target_id有关的信息,真让人头大!

2、死磕target_id

每当没有头绪的时候,我总是想起莎翁的那句:

“一切过往,皆为序章”

反之,一切序章,皆有过往,正在发生或者已经发生的万事万物一定有迹可循

我们心心念念的target_id一定在某个动态网页中记录着。

这个时候就需要耐心的筛选了,最后,我们发现,单集的target_id,隐藏在XHR下的一个"regist"开头的动态网址中:

ece53770d4a408c9b1dc3f79505cfe00.png

仔细观察,他是一个POST请求

c2a5667fcc9a9e11abbb7b7d1773c2ac.png

传递的参数如下:

5c1b499602790db4015c701ac814600d.png

翻了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())

当当当当~结果如下:(截取了部分)

3272f4dce2bb9da147bad230f74485f4.png

我们终于集齐了构成单页弹幕网址所需的target_id,后缀ID,只需要构造两个循环就可以实现完整的弹幕爬取(第一个循环构造每一集的基础网页,第二个循环构造单集内的弹幕页数)。

目前来说,对于弹幕爬取(腾讯视频),单纯的headers伪装就能够畅通无阻,但也建议大家文明爬取,理性分析 :)

至此,我们锅、碗和面都已经准备到位了,再把刚才各模块写的精简一些,然后就可以酣畅淋漓的吃大碗宽面了。

Skrrrrrrrrrrr~

完整代码已整理在公众号“数据不吹牛”。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值