|
马上注册,结交更多好友,享用更多功能^_^
您需要 登录 才可以下载或查看,没有账号?立即注册
x
本帖最后由 skyrimz 于 2020-11-23 14:32 编辑
这篇文章是我先发在csdn博客上的,不知道论坛可不可以发外站连接,就直接搬过来了。希望各位高手能多多指导
写在前面
在b站跟着小甲鱼学习Pyhton一个半月了,基本知识部分已经学习完毕,想着做点什么练习一下所学的知识。正好碰上一部很喜欢的电视剧,于是打算就着这部喜欢的剧练习一下学习的知识。
想到哪写到哪,慢慢更新慢慢完善,一步一步记录自己学习的过程~
如果有一起学习的新手,大家一起进步,共勉!
在敲代码以及数据分析方面,作者完全是业余爱好,大家笑一笑就好哈。
PS:感谢CSDN,学习过程中95%的问题都在各位大佬的博客中得到了解决
开始!
简单分析一下弹幕数据
《风犬少年的天空》,2020年9月24日上线第一集,10月22日迎来大结局(大会员),非会员则在11月5日能够观看大结局。同时播放完之后似乎有一段时间的限免,可以随意观看,且会员比非会员每一集早一周能够观看。
根据B站官方数据,截至2020-11-16日,总共弹幕量达到399.2万,其中是否包括花絮,周边,MV等暂时未知。
根据B站每一集显示的弹幕量,计算出的总弹幕量为:
11615+15966+11981+11971+11974+12000+12000+11993+11994+11973+11946+15926+11955+11975+11979+15944=191192
(更新:这个数据没有价值,不值得参考)
也就是大概20万条弹幕,为什么只有官方的将近400万条弹幕的5%呢?由于本人对这方面的知识一窍不通,只能在此做出几点猜测,希望有了解的大神指点一二:
分集弹幕已经经过b站本身某种算法的去重,也许剔除了大量的重复,无意义弹幕
也许有些弹幕违规被清理了?但不应该有如此庞大的数量
弹幕量过于巨大,我所用的b站接口并没有全部收录(经过实验,该接口对于弹幕量较少的视频是可以精准爬取每一条弹幕的)
似乎分集弹幕有一定的上限,普通视频1000,电视剧6000(存疑,因为有几集达到了12000,甚至16000的,怀疑这两个也是一种上限)
(更新:找到了b站官方对于弹幕数量问题的解答:确实会按照时间替换早期弹幕,也就是说我爬到的27万条弹幕大概率就是能拿到的所有弹幕了,全部弹幕估计只能b站内部才能看到了。)
而根据我所写的代码,经过去重后,最终得到的弹幕数量为:273778 。似乎比分集弹幕的总和多了8万条,但还远远达不到400万条。(好像相对于400万来说,这点误差也不大?hhh )
能力有限,暂时能拿到的数据只有这些,就当是一切正常,用这个数据样本进行接下来的分析吧。
蠢并痛苦着的学习过程。。。
作为一名机械狗,学习编程完全是导师建议+自己有那么一点兴趣,最后学的乱七八糟,所以后面所有写的代码看起来都可能蠢的不行+bug频出,希望各路大神不吝赐教(希望我能看懂吧)。。。
起初,是打算爬了弹幕然后做个词云图,分析下词频拉倒,没想到这竟然是折磨的开始…
且不说用pycharm安装各种库时候的一次次的失败(大多数情况下pycharm本身安装还是很方便的,除了个别的库对py版本,网络有要求),期间尝试过很多办法,手动pip安装等,好在最后该用的模块和运行库都装上了,一切正常。
除此之外,零基础的我还恶补了好多知识,包括但不限于BeautifulSoup的用法(处理xml格式的数据很好用),正则表达式(至今仍然一头雾水),浏览器审查元素的用法(好多功能在爬弹幕的时候经常用),还有Scrapy(甚至都没用上)。
需要学习的内容:html知识,pandas,numpy,nlp。。。
干(烂)货环节-------弹幕的获取与整理
最最最开始,只会用urllib.request这个功能,配合网上查到的一个弹幕接口:http://comment.bilibili.com/cid.xml,写了个简单的代码,通过urllib.request+BeautifulSoup确实能够实现弹幕的爬取。
进行到这里,发现了几个问题:
cid的获取,相较于BV,AV号,这个值并不能直接获得,但却与这两个编号存在联系。
可以看到,其中maxlimit为500,即最大弹幕量,也就是说通过这种方式获得的弹幕在弹幕数量很大时是不完整的。因此如果想要获取大量弹幕,我们需要更换一个接口。
关于cid的获取
根据我的理解,cid与av,bv号一样,都是每一个视频独一无二的“代号”(分p,电视剧分集也都是唯一的),只不过这个代号似乎并不对普通用户开放,无法直接获得。于是我进行了几次尝试:
首先通过浏览器的“审查元素”功能(通常来说按F12或者右键空白处)中的Elements项查找cid,不知道是不是b站的代码改变了,没能在Elements下找到cid相关值。
转到Network项,搜索cid,可以看到在General下的Request URL中有一条包括cid的链接,cid=后面的那串数字就是我们要找的cid值。把这个值带入http://comment.bilibili.com/cid.xml中替换cid,即可得到存储弹幕的xml文件。
到这里,如果是简单爬取少量弹幕,就大功告成了!(代码很简单就不献丑了,相信大家都比我聪明,随便写写就出来了~)
上面说过,AV,BV号均与cid有关,且能够相互转化。利用审查元素手动获取cid的方式过于繁琐,后面会用到一种较为简单且可以通过Python自动获取cid的方法。
关于弹幕上限
对于发布时间长,弹幕数量多的视频来说,第一种方法将不再适用。此时考虑通过历史弹幕来获取尽可能多的弹幕(甚至全部弹幕)。
查询历史弹幕的接口为:https://api.bilibili.com/x/v2/dm ... amp;date=2020-10-07
其中:oid即为之前提过的cid,date则是所要获取的历史弹幕的日期。通过修改这两处参数,即可获取不同视频,不同时间的历史弹幕。
然而直接使用该链接,得到的结果是这样的:
可以发现,该链接要求用户保持登陆状态才可以查看,也就是说我们要模拟已经登陆b站的情况下再进行查看。此时我们可以在请求中加入headers,填上cookie一起发送,从而使服务器认为我们是在登陆情况下进行的访问。
cookie的获取(很多办法,简单提一种):到Network项下面的Request Headers中查找。
headers_needed = {"cookie": "xxxxxxx", "user-agent": "xxxxxxx"}
req = requests.get(url, headers=headers_needed)
通过requests库的get方法访问,配合BS即可爬取历史弹幕。
新的风暴已经出现。。。
虽然进行到这步之后,之前的cid和弹幕上限问题已经得到了解决。但是! 《风犬》作为一部播出将近1个月,片长16集的电视剧,这么一集一集一天一天扒弹幕怕不是要累死呀!于是乎打算搞一个批量获取某一集历史弹幕的功能~(精力有限,以后说不定会拓展到全集)
简单的思路
在探索(受苦)的过程中,我发现了这么一个链接:
https://api.bilibili.com/x/v2/dm ... 5&month=2020-10
打开之后是这样的:
data所对应的list中,保存了当月内每一天(如果当天有弹幕)的日期,修改oid和month这两个参数,即可得到任意视频任意月份内的历史弹幕日期。再通过上面爬取单日弹幕的代码,即可实现批量爬取。
(其实自己写一个list也可以,不过考虑到每一集发布时间不同,总体跨度大,用现成的更好一点,省事)
批量处理中的cid获取方法
之前提到过,手动获取cid实在是不怎么效率。如今要通过程序批量爬取弹幕,就更不可能每一集都手动去获取一遍。于是乎找了一个解决办法:(链接均来自于互联网各位大佬分享)
https://api.bilibili.com/x/player/pagelist?bvid=
在bvid后填入视频BV号,即可得到包含cid在内的一组数据
https://www.bilibili.com/widget/getPageList?aid=
同理,aid后填入视频AV号(基本没有用到av的时候了吧,以防万一还是带上吧)
“ tips:和 Elements 与 Network 相同,有一个 Console 项,输入 bvid 或 aid 即可获取视频BV/AV号,适用于无法直接从视频链接获取BV/AV号的情况。”
之后写段代码处理下数据就能得到cid啦~(BS和正则表达式就学了一点点皮毛中的皮毛,想了半天也没写出来处理办法,只能用最笨的办法实现了,见笑)
def bv2cid():
id_bv = input('输入BV号:')
convert_url = 'https://api.bilibili.com/x/player/pagelist?bvid=' + id_bv
response = urllib.request.urlopen(convert_url)
response_readable = response.read().decode('utf-8')
begin = response_readable.find('"cid"') + 6
over = response_readable.find('"page"') - 1
cid = response_readable[begin:over]
return cid,id_bv
aid2cid中用到了BeautifulSoup 记得import
def aid2cid():
id_aid = input('输入aid号:')
convert_url = 'https://www.bilibili.com/widget/getPageList?aid=' + id_aid
req = requests.get(convert_url)
req.encoding = 'utf-8'
soup = BeautifulSoup(req.text, 'lxml')
soup_find = soup.find(string=re.compile("cid"))
soup_find_str = str(soup_find)
begin = soup_find_str.find('"cid"') + 6
over = soup_find_str.find('}]')
cid = soup_find_str[begin:over]
return cid,id_aid
通过写的批量爬取程序,可以达到如下效果:
弹幕处理与保存
如果只需要制作词云的话,提取弹幕中的文字部分即可,不过我对前面的一组数据也很好奇。根据网上的分析,几组数据分别代表:
’弹幕出现时间’, ‘弹幕模式’, ‘字号’, ‘字体颜色’, ‘实际发布时间’, ‘弹幕池’, ‘用户ID’, 'rowID’
我们逐一处理其中的每一项:
用户ID这一项经过加密计算,上网查过之后发现反推是可以的,但似乎属于暴力破解?暂时不考虑转换为真实ID,不会对统计造成影响。
(2020.11.17更新:属于crc32b算法,纠结了半天最终在网上找到了反求的python程序,感谢GitHub上的Aruelius.L大佬的程序,非常好用!代码来源:https://github.com/Aruelius/crc32-crack)
弹幕出现时间以及实际发布时间均使用秒计数(如:2583.65000),实际发布时间使用UNIX时间戳的形式(如:1601559058),这个数据以后分析会用到。不过暂时为了好看,我们把它转换成更符合阅读习惯的形式,即:年月日时分秒的形式。这里很简单,使用datetime.timedelta以及datetime.timedelta.fromtimestamp函数转换即可。
根据观察,个人猜测 rowID 这一项为每一条弹幕的“代号”,且唯一。(我的去重程序就是根据这个猜测写的,坑爹的是不一定对,网上找了找没找到相关信息,只能硬着头皮上了,姑且算是正确猜测吧。)
暂时处理这几项***************************************************************************************
到这一步基本的处理就完成了,现在考虑数据的存储方式。 由于经常要用到pandas,遂决定以CSV(Comma-Separated Values)格式(跟excel差不多哈)保存数据。
在Python中,可以通过list生成csv文件,简单方便。
对于xml文件中的每一条数据,即(<d p="207.87000,1,25,16777215,1601558965,0,ed7bd0bf,40628661897920515">哈哈哈哈</d>),分两大部分处理,前几项数据与 ‘弹幕出现时间’, ‘弹幕模式’, ‘字号’, ‘字体颜色’, ‘实际发布时间’, ‘弹幕池’, ‘用户ID’, ‘rowID’ 组合为一个dict,实际弹幕内容与 弹幕
内容 组合为一个dict,最后把两个dict拼接成一个大的dict(利用dict(dict1,**dict2))。
# 数据部分
comments_data = str_each.split('p="')[1].split('">')[0].split(',')
# 弹幕内容
comments_text = str_each.split('p="')[1].split('">')[1].split('</d>')
非常无脑的分法,为了分出一一对应的格式。。。
接下来把每一条数据对应的dict存入一个list,用这个list生成csv文件。
关于csv文件,有两点需要注意:
rowID 这一项的内容,数值长度过长,在生成csv文件的时候系统会自作聪明的转化为科学计数法(坑爹啊!),解决办法是:数据末尾+\t即可,不让系统判定为数据而判定为“字符串”就好了。
生成的csv文件可能为乱码。我的环境为win10,经测试会出现此问题。这个问题涉及到编码,即使用gbk或utf-8都无法正常生成csv文件,打开会出现乱码。问题出现的原因是由于写入csv文件时,文件头部会出现一个BOM(Byte Order Mark)用来声明文件编码信息,然而却没有被正常识别,导致编码错乱。解决办法也很简单:使用 utf-8-sig 格式保存即可,能够正确识别且无乱码。
csv文件合并与去重
通过 csv , pandas, glob 等模块即可完成。
合并csv文件虽然很简单,过程却苦不堪言。。。自己写+网上找了好多代码,不是合并之后数据错位就是索引出问题,困扰了好久,最终通过一段代码搞定了csv文件合并以及索引问题。
关于去重,使用了pandas中的.drop_duplicates()函数,去重依据就是上文提到的rowID,即通过去除所有重复rowID来达到剔除重复弹幕的目的(并不代表重复内容一并去除)。
爬取的历史弹幕,每天之间并不会有太大出入,也就是说可能两天之间的弹幕只有几十条不同,剩下几千条都是一模一样的,这些都需要合并后统一去除掉。
贴一段去重+合并文件的代码:
import pandas as pd
import glob
import csv
def csv_drop_duplicates():
combine_name = input('输入合并后文件名:')
drop_duplicates_name = input('输入去重后文件名:')
csv_list = glob.glob('*.csv')
for i in csv_list:
fr = open(i, 'r', encoding='utf-8-sig').read()
with open(combine_name, 'a', encoding='utf-8-sig') as f:
f.write(fr)
print('合并完毕!')
temp_list = []
with open(combine_name, 'r', encoding='utf-8-sig') as f1:
reader = csv.reader(f1)
for row in reader:
temp_list.append(row)
df = pd.DataFrame(temp_list)
df.drop_duplicates(subset=7, inplace=True, keep='first')
df.to_csv(drop_duplicates_name, encoding='utf-8-sig')
print('去重完毕!')
if __name__ == '__main__':
csv_drop_duplicates()
通过更改drop_duplicates()中的subset参数,即可对用户ID,弹幕内容等每一项进行去重。如果对用户ID项去重,得到的就是每集发布弹幕的用户数。
以第五集为例:
爬取9月25到11月15共52天的弹幕,每天的csv大小约为612KB,弹幕条数6000条左右。
经过合并后的csv大小约为30.3MB,弹幕条数310711条(其中绝大多数都是重复了N遍的无用数据,这个csv文件主要是用来去重,没有参考意义。)
经过去重之后的csv大小约为2.18MB,弹幕条数20937条(与官方数目有出入,不过我检查过确实没有重复弹幕,弹幕数量姑且算是合理吧。)
简单分析
弹幕不同于文章,有一定的特殊性。弹幕长度一般不会太长,大多数以一句话为基本单位,如果再进行分词,就破坏了“弹幕”这种形式所传递的信息,所以不再进行分词。下面进行简单的分析:
思路及代码实现
每集弹幕的数量我们已经得到(去重后),现在来获取每集出现最多的弹幕,统计弹幕内容列中的每一个值并进行计数和排序就是此处我们的需求。
需要注意的是:尽管已经进行初步去重,但仍有很大一部分弹幕,比如:来了,卧槽,666,哈哈哈等毫无意义的重复弹幕,这里也要一并去除。
此处使用value_counts()函数来进行统计,它是pandas中一个较为常用的函数。具体代码如下,思路也都写在了里面。
import os
import pandas as pd
import csv
#准备工作路径和文件
file_list = []
work_path = os.getcwd()
path = r'D:\bilibili' # 待处理文件目录
files = os.listdir(path)
for file in files:
if '.csv' in file:
file_list.append(file)
# 统计高频弹幕,并简单清洗 (如果统计其他项,不需要去重可直接删掉这一块)
drop_words = ['哈哈','来了','来啦','啊啊','hhh'] # 过滤词
def freq_auto():
for file_name in file_list:
init_name = file_name
init_data = pd.read_csv(os.path.join(path,init_name))
data_dm = init_data['弹幕内容'] # 需要统计的表头,按需更换
data_dm_list = data_dm.tolist()
for i in tuple(data_dm_list): # 去掉无意义弹幕
if len(i) == 1: # 单字
data_dm_list.remove(i)
continue
elif i.isdigit() is True: # 纯数字
data_dm_list.remove(i)
continue
elif i.encode('utf-8').isalpha() is True: # 纯字母(汉字Unicode识别为字母,需要转换)
data_dm_list.remove(i)
continue
else:
for j in drop_words:
if j in i:
data_dm_list.remove(i)
break
else:
pass
frame = pd.DataFrame(data_dm_list, columns=['弹幕内容'])
drop_name = '(已清洗)' + file_name
frame.to_csv(os.path.join(work_path,drop_name), encoding='utf-8-sig')
print('【%s】清洗完毕!'%file_name)
# 统计清洗后弹幕频率(注意,转成的DataFrame索引是需要被统计的词,列是词出现的次数)
data = pd.read_csv(os.path.join(work_path,drop_name))
data_counts = data['弹幕内容'].value_counts() # 通过value_counts计算词频
df_data_counts = pd.DataFrame(data_counts) # 将词频结果转成DataFrame格式。
comment_header = ['弹幕内容']
count_header = ['出现次数']
comment = df_data_counts.index.values.tolist() # 把词转成列表
count = df_data_counts['弹幕内容'].tolist() # 把词出现的次数转成列表
dictlist = []
for k, v in zip(comment, count): # 准备好字典,写入csv
k_str = k.split('为了换成列表') # str转换为list,下同
v_str = str(v).split('为了换成列表')
comment_dict = dict(zip(comment_header, k_str))
count_dict = dict(zip(count_header, v_str))
full_dict = dict(comment_dict, **count_dict)
dictlist.append(full_dict)
freq_name = '(已统计)' + file_name
csv_header = ['弹幕内容', '出现次数']
with open(os.path.join(work_path,freq_name), 'w', newline='', encoding='utf-8-sig') as f:
f_csv = csv.DictWriter(f, csv_header)
f_csv.writeheader()
f_csv.writerows(dictlist)
os.remove(os.path.join(work_path,drop_name)) # 删掉无用文件
print('【%s】处理完毕!'%freq_name)
print('全部处理完成!')
if __name__ == '__main__':
freq_auto()
处理后内容如下(以第十集为例):
可以看出,尽管需求已经实现,但仍有无统计价值的弹幕出现,而且数量上无法忽略。比如“爷青结”,“泪目”这种,每集的文件处理后都存在这种现象,由于各集内容都不同,暂时只能手动去除这部分数据。
上面的代码也可以用来统计单一用户每集发布的弹幕数量,原理是一样的,具体代码大同小异。数据如下:
这里的用户ID经过CRC32处理过,对制作图表没有影响,后面再进行还原。
数据可视化及分析
拿到数据之后,就可以着手进行可视化的工作了。这部分要用到matplotlib.pyplot模块,功能十分强大,进行简单的图表制作还是不在话下~
matplotlib.pyplot模块绘制饼图,柱状图,折线图等都很方便,代码也很简单。相关的知识还没有深入学习,用到的代码都是很基础的,就不放上来了。
根据得到的弹幕数量和观众数量,可以得出每一集的弹幕/观众数量变化趋势:
简单分析如下:
大多数情况下弹幕数量和观众数保持同样的变化趋势,而1-5集的变化趋势却毫无章法。做点推测:剧播完后,1-3集为免费观看,4集之后为会员内容,因此第4集观众数有明显下跌。同时,第4集用户平均弹幕发送量远高于第3集,是否说明会员用户比起非会员用户更喜欢发布弹幕?
第七集和十三集在剧情上均有重大转折。刘闻钦下线,马田被陷害出走等情节都有剧烈的感情变化,激起了观众的讨论。因此这两集无论是弹幕数量还是发布弹幕的观众数都有所增加。需要注意的是:第七集弹幕数如此之多,是因为存在大量的“一生所爱xxx”的表白弹幕,属于与剧情无关的刷屏行为。(按照b站的弹幕获取限制,不知道为什么能得到8W条这个夸张的数字,检查几遍之后也没什么头绪。)
每一集的弹幕情况如下(以第八集为例):
分析如下:
绝大多数观众还是很少发弹幕的,属于安静看剧党。
也有狂热粉丝,一集发布了34条弹幕,表达欲望很强烈~
在其他集数,甚至有的用户不足一分钟便会发布一条弹幕,真爱粉无疑了。
风犬这部剧,为我们塑造了7个个性鲜明的少年。那么观众对于每一位角色的关注度又是怎么样的呢?通过弹幕,我们可以略知一二。
对角色相关弹幕进行过滤和统计,最终结果为:
分析环节:
狗哥作为男主角,关注度讨论度自然最高,远远甩开其他人,独占第一档。不得不说彭昱畅演绎的老狗很棒!
娇姐通过讨喜的人设以及演员精湛的表演,获得了第二名的成绩,不愧是大力娇!甜椒组合紧随狗哥其后~
作为女主,安然的戏份跟弹幕一样的少。这个角色甚至可以说是全剧第一大工具人,哪里剧情需要哪里搬,就是没有属于自己的剧情。当然演员还是很漂亮的~
刘闻钦=嘴哥+咪哥。白月光+意难平的buff属实强力。
上面提到过,有的观众甚至不到一分钟就会发布一条弹幕。那么,纵观全集,谁又是行走的弹幕发射机呢?
之前我们已经实现了高频弹幕的统计,同样的原理,这里依旧使用value_counts()函数来统计观众发送的弹幕量。
上面提到过,用户ID并不是真正的uid,还需要进行还原。这里使用GitHub上的Aruelius.L大佬的程序来进行还原,d1aed527为需要还原的内容,364436978就是真实的用户uid。
结果如下:
分析:
“翻斗fa园扛把子”同学凭借230条的弹幕量稳坐第一把交椅,不愧是扛把子呀~该大佬每集平均输出14条弹幕,按照风犬每集60分钟计算,dalao每四分半就会发射一条弹幕。这波啊,这波是经典双线程操作,看剧弹幕两不误。
接下来,我们看看这位疯狂发射弹幕的大佬都发了些啥。去掉无意义弹幕和重复弹幕之后,大佬的弹幕内容如下:
可以看出,还是有很多剧情相关的弹幕。大佬不愧是大佬,质量和数量,两手抓两手都要硬啊~而且似乎还是彭昱畅的粉丝?
目前就进行到这里,更多内容还在学习中。。。 |
评分
-
参与人数 1 | 荣誉 +5 |
鱼币 +5 |
贡献 +3 |
收起
理由
|
昨非
| + 5 |
+ 5 |
+ 3 |
建议楼主申请精化 |
查看全部评分
|