爬取B站电视剧《风犬少年的天空》弹幕并分析
写在前面
在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/history?type=1&oid=251215409&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/history/index?type=1&oid=253042215&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每四分半就会发射一条弹幕。这波啊,这波是经典双线程操作,看剧弹幕两不误。
接下来,我们看看这位疯狂发射弹幕的大佬都发了些啥。去掉无意义弹幕和重复弹幕之后,大佬的弹幕内容如下:
可以看出,还是有很多剧情相关的弹幕。大佬不愧是大佬,质量和数量,两手抓两手都要硬啊~而且似乎还是彭昱畅的粉丝?
弹幕情感倾向分析初步
最后一个部分,轮到弹幕的情感分析。最开始了解到这方面还是csdn上某个大佬的一篇分析提到的,既然数据都拿到了,索性也来试一试。
首先,情感倾向分析这个东西我个人是无法完成的,毕竟这是一门完全陌生的学科,而且数据量很大,逐个分析不现实。(情感倾向分析属于NLP (Natural Language Processing)即自然语言处理下的一个分支,而NLP也是AI的一个子领域)
此时可以借助AI来进行分析,本次测试使用的是某度的AI平台,其中就有情感倾向分析所需要的功能。(主要是注册有赠送使用次数)。
具体过程略去不提,网上教程一抓一大把。应用创建好之后如图:
可以看出我已经处理了24242条弹幕数据。相比将近27万条的总数据,只处理这一小部分的原因有几点:
-
并不是所有弹幕都与角色有关,精准去除所有与角色无关的弹幕对于我个人来说是不可能的。只能选取所有提及角色名字的弹幕进行分析。当然,这种办法也不准确,这些弹幕可能是某些调皮的观众的角色扮演,还可能同一条弹幕提及多个角色,使主体不明确。不过这也是没有办法的办法了。
-
总共就免费50万次啊!试一次没了!!!
-
免费的产品QPS上限很低,全部处理需要的时间太长了,就这些还弄了几个小时。。。
下一步就是提取出与角色相关的弹幕了:
import os
import pandas as pd
def char_danmu():
# 准备工作路径和文件
file_list = []
work_path = os.getcwd()
path = r'D:\bilibili\drop' # 待处理文件目录
files = os.listdir(path)
for file in files:
if '.csv' in file:
file_list.append(file)
# 筛选角色相关弹幕
key_words1 = ['涂俊', '俊俊', '狗哥', '老狗']
key_words2 = ['安然']
key_words3 = ['娇娇', '大力娇', '娇姐']
key_words4 = ['马田', '马甜']
key_words5 = ['刘文钦', '刘闻钦', '钦哥']
def char_keywords():
for file_name in file_list:
res = []
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 key_words5:
for j in data_dm_list:
if i in j:
res.append(j)
res_frame = pd.DataFrame(res, columns=['弹幕内容'])
drop_name = '(keywords)' + file_name
res_frame.to_csv(os.path.join(work_path, drop_name), encoding='utf-8-sig')
print('【%s】关键词提取完毕!' % file_name)
print('全部处理完成!')
if __name__ == '__main__':
char_danmu()
得到内容如下:
这其中还含有大量的无分析价值弹幕,需要去掉。但时间精力有限,只能全部拿来分析了,这样得出的结果将会有很大的偏差。
接着,调用某度AI平台的API接口,进行情感倾向分析:
import pandas as pd
from aip import AipNlp
import time
import csv
import os
def class_sentiment():
#配置api接口
app_id = 'aid'
api_key = 'ak'
secret_key = 'sk'
client = AipNlp(app_id, api_key, secret_key)
# 准备工作路径和文件
file_list = []
work_path = os.getcwd()
path = r'D:\py+\Bilibili\Danmu\keywords\kw1' # 待处理文件目录
files = os.listdir(path)
for file in files:
if '.csv' in file:
file_list.append(file)
for file_name in file_list:
data = pd.read_csv(os.path.join(path,file_name),)
data_text = data['弹幕内容'].tolist()
csv_header = ['内容', '积极概率', '消极概率', '置信度', '情感极性']
res_list = [] # api处理后内容,每条为一个dict
count = 0
csv_list = [] # writerows所用的list,包含每一个处理好的dict
for i in data_text:
try:
sen_res = client.sentimentClassify(i)
time.sleep(0.6)
res_list.append(sen_res)
count += 1
print('第%d条【%s】处理完毕,延时0.6s!' % (count, i))
except:
count -= 1
continue
print('全部处理完成!')
for j in res_list:
new_list = [j['text'], j['items'][0]['positive_prob'], j['items'][0]['negative_prob'],
j['items'][0]['confidence'],
j['items'][0]['sentiment']] # 提取每个参数为一个新list
new_dict = dict(zip(csv_header, new_list)) # 生成将要写入csv的dict
csv_list.append(new_dict)
save_name = '(sentiment)' + file_name
with open(save_name, 'w', encoding='utf-8-sig', newline='')as f:
f_csv = csv.DictWriter(f, csv_header)
f_csv.writeheader()
f_csv.writerows(csv_list)
print('已保存为【%s】' %save_name)
print('全部保存完毕!')
if __name__ == '__main__':
class_sentiment()
值得注意的是,一开始并不知道QPS是啥,写的代码访问次数过快导致后面的请求全都被服务器拒绝了,白白浪费好多次机会。QPS(Queries-per-second)即每秒请求次数,顾名思义,懂得都懂。后面加上了time.sleep()
函数降低了请求速度,才能正常运行,这也使得数据的处理速度变慢了很多。当然肯定有其他的方法能够突破这个限制,关键我不会啊!
正常运行后,处理过程如图(pycharm中运行):
处理后的得到的结果如下:
其实可以发现,这个AI还是挺不靠谱的,识别能力令人捉急!不过都免费了,还要啥自行车啊,用来学习是足够了。
上面表中的内容,每一项含义为(摘自官方说明文档):
-
情感极性:表示情感极性分类结果,0:负向,1:中性,2:正向
-
置信度:表示分类的置信度,取值范围[0,1],越大结果越可信
-
积极概率:表示属于积极类别的概率 ,取值范围[0,1] 越高几率越大
-
消极概率:表示属于消极类别的概率,取值范围[0,1] 越高几率越大
由于没有现成的模型可供参考,也没有相关方向建模的经验,只能使用如下模型计算感情倾向分值:
感情倾向分值 = 基础分值(每条弹幕为1) x 置信度 x 情感极性概率(积极为正,消极为负)
将每集中上面的分值求和取平均数,即为该集该角色观众所发弹幕的情感倾向分值。
需要注意的是,该分值代表的是观众此刻对于角色所表露出的感情,而不是角色此刻的感情。而且,由于分类只有正向和负向,没有更细的划分,负向不等于角色不好,正向也不等于角色好,他们表现的都是观众的惊喜,失落,难过等情绪。
最终得出的可视化结果为:
认真来说的话,这个变化趋势参考价值不高,每一集之间的变化不是很准确,不过总体来看还是有点可取之处因为:
- 样本容量较少
- 无价值样本没有剔除
- AI识别能力不足
- 建模不准确
这些因素最终必将造成极大的误差。而且我个人一路看下来,也发现图中的几个有趣之处:
-
就比如说狗哥,观众的情绪分值基本都在0分以下,但这不代表这个人物不好,他如何如何坏,如何如何作死。纵观全剧,从开始的工具人,后来的死老汉,最后的告白,狗哥这个角色身上一直环绕着一个字,那就是“惨”。喜欢的女孩子喜欢自己大哥,自己却要拱手让人;父母离婚,千里迢迢看望母亲,母亲有了新的家庭,结果无功而返;父子关系好不容易更进一步,结果飞来横祸老汉去世;最后面对喜欢的人的告白,却无法回应。。。在整理数据的时候,我注意到提到狗哥的弹幕多半都离不开“心疼”,“工具人”,“好惨”。面对狗哥,观众们的感情多半是可怜,心痛,惋惜,你能说它们不是负向的吗?
-
对比来看安然,安然的情感分值显然比狗哥高了太多 。作为“人上人”,家境优渥,成绩优异,外表靓丽,这个人的角色过于完美,当然,这种完美人设也仅仅存在于剧中。对于安然,观众更多表达的是一种钦慕,羡慕,向往,谁不想要这样的伴侣陪在身旁呢?弹幕多见于“好美”,“好甜”,“太会了”。而在剧情上,安然也少有坎坷,一路顺风顺水,没有过多沉重的虐心情节。尽管也有灰暗时刻,但跟狗哥的遭遇比起来却也算不得什么。总而言之,演员出众的个人形象+完美讨喜的人设+相比之下更为轻松欢快的剧情,观众在观看时的情绪自然是正向的,积极的,放松的。也难怪安然的得分如此之高。安然这个角色设计的有多完美呢?16集中没有任何一集的感情分数在0以下。换言之,无论是从人物设定的各个方面,还是演员的演绎,到剧情的安排上,这个角色就好像是“美好的化身”,几乎没有缺点。如果现实中真的有狗哥安然这两个人,他们之间或许就像这两条折线一样,永远没有交点吧。
-
娇娇的分值也很高。娇娇的成长过程中,始终充满了爱。从小到大,有大兴村的三个好兄弟(
好姐妹)一直包容她,爱护她(爱她,就送她刮胡刀);家境虽然不富裕,但父母恩爱,感情和谐,一家人和和睦睦,爸妈对待娇娇也是关爱有加,娇娇高三了还准备参加超级女声,爸妈也是大力支持;大兴村的叔叔嬢嬢都很喜欢娇娇,比赛过后一起为娇娇庆祝。而到了学校,“情敌”安然变成了好闺蜜,多次鼓励,帮助娇娇;马田更不用说,一见钟情之后从头宠到尾。甚至马田的妈妈最后也慢慢地喜欢上了娇娇。虽然娇娇的人设没有安然完美,但从另一个方面来说,娇娇更接地气。正是这样,每当进行到娇娇的剧情,都会流露出一种市井之中普通家庭的温馨,好朋友好闺蜜之间的纯真情谊还有年轻男女之间懵懂的甜蜜暧昧。再加上娇娇的鬼马精灵,企业级理解,灵魂歌王等属性贡献出的笑点,观众在观看时自然而然地处于一种轻松愉快的氛围中,感情自然是正向的不能再正向了。 -
马田和安然很像。同样的高起点,几乎完美的人设,
还有超出同龄人的做作,出类拔萃的身高。与安然不同,他更高傲,他也有缺点,他更像一个人。面对“恶霸”狗哥,他仗义执言;当狗哥得寸进尺,他大打出手,重拳出击;混混追击,他临危救人;看好朋友越陷越深,尽力挽救。这些事情,安然处理的太过完美,而马田的反应更像十七八岁的少年,做了就是做了,何必考虑那么多!同时他又具有同龄人所没有高情商,在学校生活里更是如鱼得水。想必观众也无法拒绝这样一个“少年白马王子”吧。 -
刘闻钦这个角色,从最开始的“白月光”,到最后的“意难平”,令人唏嘘。和狗哥一样,刘闻钦也是一个苦命之人,不同的是他更苦一点。父亲体弱多病,家徒四壁,甚至学业都无法继续完成,可以说是地狱难度开局。从球场上的追风少年,到街头的烧烤摊老板,个中滋味也许只有他自己能体会吧。与娇娇相反,伴随着刘闻钦的成长的,没有父母给的温馨环境,好兄弟也只有狗哥一人。而谈到爱情,来自安然的爱意却成了他生命中不可承受之重,他更无法接受。纵然不太认同他对待安然的方式,但同为男人,在那个年纪遇到这样一份感情,我能做的好像也只有放弃。一首一生所爱,一个慢慢离去的背影,一段感情就此终结。抛开最后的骚操作不谈,观众们也许没有刘闻钦和狗哥那么惨,但多多少少都在年少气盛之时立下过“凌云志”,现在回首,又有多少人成就了“第一流”呢?也许是角色无奈的遭遇+曾经的追风少年的亮眼表现,在一生所爱的加持下,刘闻钦的情感分值得到了中和,不高也不低。
后记
到这里,全部内容都已经完成。从开始学习Pyhotn到今天,差不多两个月整,而这个项目的完成,也接近半个月的时间。实践和兴趣都是最好的老师,在这个过程中我又学到了很多。风犬是部好剧,翻来覆去地看了很多遍,意犹未尽,这种感觉多年未有。这次练习,也算是为爱发电,因为我不想错过这部作品中的每一个细节,想尽可能多的从不同的方面去了解它。有人写文章,有人剪视频,而我也打算从我喜欢的领域来解读这部作品。现在,我终于实现了这个目标。学习的过程多少有些枯燥无味,真正着手之时,反而没有无趣的感觉。遇到问题难免会心急,但解决问题那一刻却是无比开心。在不断的学习中,我接触到了越来越多的知识,这一次终于明白了什么叫学无止境,三百六十行,行行皆如是。这一次,好多知识都只学了个皮毛,仅仅知其然;希望下一次,能够彻底地掌握,做到知其所以然。
新接触到的知识:pandas,matplotlib.pyplot,csv文件,BOM与编码相关,浏览器抓包,html语言,NLP,AI,数据可视化,CRC32
总共代码675行,文件如下:
完结撒花~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~