python大作业——B站弹幕数据爬取与分析
前段时间要写一个Python大作业,选题为B站弹幕数据分析,由于是Python新手,所以参考了以下的文档,再次感谢分享技术的人
同时也因为本次是只是本人记录一次初学Python期间的一次较有意思的大作业,语法简陋勿喷。参考文档:https://blog.csdn.net/weixin_34161029/article/details/91713988
B站弹幕数据分析
-
第一部分——使用爬虫抓取弹幕数据
-
B站弹幕数据分析,首先我们需要抓取到B站视频的弹幕数据,才能进行数据分析
-
选取分析的对象是B站UP主 观视频工作室 的**《睡前消息》** 系列视频中的最新15期,即 110-124期视频(2020-05-03 ~ 2020-06-05) 的弹幕作为本次分析的弹幕,爬取的日期从第110期发布的日期开始到爬取的时候**(2020-05-03 ~ 2020-06-09)**
-
首先需要从 观视频工作室 的个人主页处找到 睡前消息 频道,爬这里就可以获取到所需要的视频链接,页面是动态页面,所以这里选择用 selenium库 爬取链接,存进Link.txt文件备用
注意这里我用的是比较偷懒的爬取方法,只能爬到最新15期,要爬其他的可以自己改进一下算法
-
在视频页的console处可以输入window.cid来获取cid,但在selenium模拟中往浏览器console处输入的方法,搜索苦久无果,所以想出了如此下策:在视频页打开F12,在elements处搜索cid,发现是有的,这条语句是与网关有关的,通过bs4匹配到该语句,用正则匹配出来即可
正则前缀/ 后缀-1-
-
要爬取B站的弹幕,有两种方法 ( 据我所知 )
5.1 第一种方法 是https://comment.bilibili.com/{cid}.xml,这种方法可以获得最新的maxlimit数量(一般这个数量是3000)的弹幕,但是没办法获得历史弹幕
5.2 第二种方法 是https://api.bilibili.com/x/v2/dm/history?type=1&oid={cid}&date={yyyy-mm-dd},这种方法可以获取从某一天开始的maxlimit数量的弹幕,把想要获得的日期遍历一遍,虽然可能会漏掉一些弹幕(比如某一天弹幕数超过了maxlimit,就获取不到超出的部分),但也算是获取了大部分弹幕,比上一种方法好,采取这种方法
而第二个方法由于是api,所以需要用到cookie,可以用以下的方法来获取cookie
-
接下来就是分析这个xml文件中的弹幕数据,例子为如下:
<d p="369.34300,1,25,16777215,1590515762,0,a6fb6c47,33226011652915207">脱发</d>
这个 p字段 中的参数含义为如下:(百度搜索得到的答案)
第一个参数 369.34300 记作DM_time,是弹幕在视频中出现的时间,以秒数为单位。
第二个参数 1 记作DM_mode,是弹幕的模式1…3 滚动弹幕 4底端弹幕 5顶端弹幕 6.逆向弹幕 7精准定位 8高级弹幕
第三个参数 25 记作DM_font,是字号, 12非常小,16特小,18小,25中,36大,45很大,64特别大
第四个参数 16777215 记作DM_color,是字体的颜色以HTML颜色的十进制为准
第五个参数 1590515762 记作DM_realTime,是发送弹幕的时间戳
第六个参数 0 记作DM_pool,是弹幕池 0普通池 1字幕池 2特殊池(高级弹幕)
第七个参数 a6fb6c47 记作DM_userID,是发送者的ID,用于“屏蔽此弹幕的发送者”功能
第八个参数 33226011652915207 记作DM_id,是弹幕在弹幕数据库中rowID,也就是这条弹幕是历史总弹幕的第几条
p字段以外的 弹幕本体 记作DM_text
-
把爬取到的弹幕分别放到多个csv文件中,每一期一个csv文件,方便后面的数据分析
-
至此爬取数据部分完成
-
-
第二部分——使用Pandas库进行数据分析
-
在爬取并保存了所有弹幕数据之后,先进行去重,可对比还未去重时文件的大小和去重后文件的大小
1.1 未去重:(至于110-112和113-124日期不一样,是因为抓113-124的时候被封了IP,只能等到第二天早上抓110-112)
1.2 去重后:
-
然后进行如下的可视化分析:
2.1 每一期弹幕总数的变化 折线图
结合上图,我们发现每期的弹幕总数统计波动有些大,其中一些弹幕数,比如116期,甚至在这里统计只有不到5000条,但是在B站上可以看到:
这一期其实总弹幕是有1.4W的,统计到的弹幕数仅占1/3左右,出现这种情况是由于 在视频发出的某一天里,大多数用户发过了弹幕,而这部分弹幕超出了B站统计的maxlimit ,导致我们没办法统计到这部分弹幕。
相对而言,弹幕数多的视频就是弹幕不仅仅在某一天大量被发出,比如115期,这里我们统计到了有**9000+**的弹幕数,而B站上的数据:
统计到的弹幕大约占70%,这是由于并没有很多用户在某一个日期大量发出弹幕,导致弹幕库溢出,也从某方面说明了该视频的受众时间较广,是一个较为优质的视频。
另外,加上我去把每期的标题和弹幕数一对应,发现,涉及到 国与国之间的时评 的时候,在播放量相差不多的情况下,视频的受众时间会比较广,相对的,谈论的时事范围或格局越小,受众时间会相对应减小。当然这些都是通过一小部分数据得出的结论,不能代表完全的趋势就是这样。
2.2 统计发弹幕总数TOP10 的用户 横向柱状图
一共15期的节目,根据不完全统计,这位ID为 13631380 的用户一共发了160+条弹幕,平均每一期要发11条弹幕,是真爱粉无误了。
2.3 统计每期弹幕密度变化图 (图太多,只放了总体的缩略图)
首先从个体上观察这些弹幕密度图,发现弹幕都是较为均匀的,但从总体上观察这些弹幕密度图的时候,发现其中112-118期的弹幕密度图极其相似,119-124期的弹幕密度图也是极其相似,总的来说,112-124期都会有一个密度大且会持续一小段时间的“弹幕风云”,更巧的是,这部分都集中在视频时间700秒左右,也就是11分-13分之间会有一波弹幕的小高潮。
为此我特意去再次观看**《睡前消息》,发现在112-118期中,节目大概都在15~20分钟左右,而在11-13分钟的时候,也就是在视频时间过去大概70%**左右的时候,主持人都会作出一些毒辣的点评,从而引起弹幕的激烈讨论。
2.4 绘制出每期视频的弹幕词云 (取其中几个词云图展示)
由于我设置了最小字体为10,所以如果是弹幕种类分布得比较广泛的视频,留白处会比较多,这里我选取了较为饱满的弹幕词云图作为展示
【睡前消息111】深圳房价这样涨下去,会变成第二个香港么?
【睡前消息112】阅文风波的背后,腾讯真的大到只手遮天了吗?
【睡前消息118】《后浪》过后,如何看待B站又要《入海》?
可见弹幕里面的 同意 支持 保护 之类的字眼比较多,**仅观表象的话 **可以说明,虽然该节目主持人虽嘴巴毒辣,在视频弹幕里会造成不少的人激烈讨论,但还是有大部分人同意这些观点的。
项目代码
-
项目目录结构
-
GetUrl.py
from bs4 import BeautifulSoup from selenium import webdriver url = 'https://space.bilibili.com/54992199/channel/detail?cid=82529' print("开始爬取《睡前消息》第110-124期视频地址") # chrome驱动,需要放在Python安装的目录下 driver = webdriver.Chrome(r"E:\Python\chromedriver.exe") driver.get(url) data = driver.page_source soup = BeautifulSoup(data, 'lxml') count=1 res = [] all = soup.find_all('li', attrs={'class': 'small-item fakeDanmu-item'}) for li in all: if count<=15: a=li.find('a',attrs={'class':'cover cover-normal'}) res.append('https:'+a.get("href")) count+=1 else: break with open('Urls/Link.txt', 'w') as f: for link in res: f.write(link+'\n') print("已将全部链接放入到Link.txt文件中")
-
GetCid.py
from bs4 import BeautifulSoup from selenium import webdriver import re import time cids=[] Urls=[] with open('Urls/Link.txt', 'r') as f: for line in f.readlines(): Urls.append(line.strip()) print("开始爬取《睡前消息》第110-124期视频的Cid") # chrome驱动,需要放在Python安装的目录下 driver = webdriver.Chrome(r"E:\Python\chromedriver.exe") for url in Urls: driver.get(url) data = driver.page_source soup = BeautifulSoup(data, 'lxml') all = soup.find_all('script') for a in all: if str(a).startswith("<script>window."): res=a links=re.split(r'\:',str(res)) for url in links: # 这个链接前面是域名,中国的都是以cn开头 if url.startswith("//cn"): link=url break cid=re.findall(".*/(.*)-1-.*", link) # 获取到视频的cid,存进数组然后一起存进Cid.txt文件中 cid=cid[0] # 处理特殊情况下,长度不符合cid,去除尾部部分 if len(cid)>9: length=len(cid) a=length-9 cid=cid[:-a] cids.append(cid) # 每抓完一个网页休眠5秒 time.sleep(5) with open('Urls/Cid.txt', 'w') as f: for id in cids: f.write(id + '\n') print("已将全部视频的Cid放入到Cid.txt文件中")
-
GetBulletChat.py
from bs4 import BeautifulSoup import time import pandas as pd import requests import datetime headers={ "User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36 QIHU 360EE", "Connection": "keep-alive", # 这个cookie的获取方法在文档中已说明 "Cookie":"" } sets=124 # 最新一期的数字 dates=[] # 日期数组,用于填充url # 遍历日期 包括begin和end的日期 生成类似2020-05-03的格式的日期 begin = datetime.date(2020,5,3) end = datetime.date(2020,6,9) d = begin delta = datetime.timedelta(days=1) while d <= end: dates.append(str(d.strftime("%Y-%m-%d"))) d += delta Cids=[] # Cid数组,用于填充url with open('Urls/Cid.txt', 'r') as f: for line in f.readlines(): Cids.append(line.strip()) for cid in Cids: # 每次都要重置这些数据 dm_data = [] # 弹幕数据 dm_text = [] # 弹幕本体 # 弹幕的八个参数和弹幕本体 DM_time = [] DM_mode = [] DM_font = [] DM_color = [] DM_realTime = [] DM_pool = [] DM_userID = [] DM_id = [] DM_text = [] print("正在爬取第" + str(sets) + "期的《睡前消息》弹幕...") for date in dates: url="https://api.bilibili.com/x/v2/dm/history?type=1&oid="+cid+"&date="+date html=requests.get(url=url,headers=headers) #返回文本信息 html.encoding='utf8' soup=BeautifulSoup(html.text,'lxml') #建立soup对象 all=soup.find_all("d") for d in all: # 弹幕数据 dm_data.append(str(d.get("p")).split(",")) # 弹幕本体 dm_text.append(d.get_text()) # 分别把数据存进这几个数组 for i in dm_data: DM_time.append(i[0]) DM_mode.append(i[1]) DM_font.append(i[2]) DM_color.append(i[3]) DM_realTime.append(i[4]) DM_pool.append(i[5]) DM_userID.append(i[6]) DM_id.append(i[7]) for i in dm_text: DM_text.append(i) dt={"DM_time":DM_time,"DM_mode":DM_mode,"DM_font":DM_font,"DM_color":DM_color, "DM_realTime":DM_realTime,"DM_pool":DM_pool,"DM_userID":DM_userID,"DM_id":DM_id,"DM_text":DM_text} d=pd.DataFrame(dt) d.to_csv('./Danmu/Danmu-'+str(sets)+'.csv',encoding='utf-8-sig') #存储弹幕信息 print("已将弹幕放入到Danmu-"+str(sets)+".csv文件中") sets-=1 # 每抓完一个网页休眠7秒 print("缓冲中...") time.sleep(7) print("已将《睡前消息》第110-124期的弹幕爬取完毕")
-
DataAnalysis.py
import matplotlib.pyplot as plt import matplotlib import pandas as pd import os from wordcloud import WordCloud import jieba file_dir="./Danmu/" # 获取文件名 files=[files for root,dirs,files in os.walk(file_dir)] # 去重 def duplicate(files): for file in files: df = pd.read_csv(file_dir + file,encoding="utf-8-sig",index_col=0) data = df.drop_duplicates(subset=['DM_id'], keep='first') data.to_csv(file_dir + file,encoding='utf-8-sig',index=True,index_label="") print("去重完毕") # 每一期弹幕总数的变化折线图 def danmuSumPlot(files): print("弹幕总数变化图绘制中...") list1 = ['110','111','112','113','114','115','116','117','118','119','120','121','122','123','124'] data_sum=[] for file in files: data = pd.read_csv(file_dir + file,encoding="utf-8-sig",index_col=0) data_sum.append(len(data)) matplotlib.rcParams["font.family"] = "SimHei" plt.plot(list1, data_sum, "c") plt.ylabel("弹幕数") plt.xlabel("《睡前消息》期数") plt.title("每一期弹幕总数的变化图") plt.savefig('./Analysis/弹幕总数变化图', dpi=600) plt.show() print("绘制完毕") # 发弹幕总数TOP10的用户柱状图 def danmuUserTopBarh(files): print("弹幕TOP10用户图绘制中...") datas=[] for file in files: datas.append(pd.read_csv(file_dir + file,encoding="utf-8-sig",index_col=0)) # 先合并全部csv文件,再进行统计 data=pd.concat(datas) data = data.groupby('DM_userID').size().reset_index(name="count") data = data.sort_values("count", ascending=False) label = [] # y轴的值 width = [] # 给出具体每个直方图的数值 i = 0 for item in data.values: if i < 10: label.append(item[0]) width.append(item[1]) i += 1 else: break matplotlib.rcParams["font.family"] = "SimHei" y = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] # 给出在y轴上的位置 plt.barh(y=y, width=width, tick_label=label) # 绘制水平直方图 plt.ylabel("用户ID") plt.xlabel("弹幕数") plt.title("发弹幕总数TOP10的用户柱状图") plt.subplots_adjust(left=0.17) # 控制图片左边的间隔 避免显示不全 plt.savefig('./Analysis/TOP10', dpi=600, left=0.17) print("绘制完毕") # 每期弹幕密度变化图 def danmuDensityChange(files): print("弹幕密度变化图绘制中...") sets=110 for file in files: data = pd.read_csv(file_dir + file, encoding="utf-8-sig", index_col=0) data = data.sort_values("DM_time") # 先对弹幕发送时间进行取整 data['DM_time'] = [int(item) for item in data.DM_time] data = data.groupby('DM_time').size().reset_index(name="counted") list2 = [item for item in data.DM_time] data_sum = [item for item in data.counted] matplotlib.rcParams["font.family"] = "SimHei" plt.plot(list2, data_sum, "c") plt.ylabel("弹幕数量") plt.xlabel("视频时间轴/(秒)") plt.title(str(sets)+"期弹幕密度变化图") plt.savefig("./Analysis/弹幕密度变化/"+str(sets)+'期弹幕密度变化图', dpi=600) sets+=1 print("绘制完毕") # 每期的弹幕词云 def danmuWordCloud(files): print("弹幕词云绘制中...") sets = 110 for file in files: data = pd.read_csv(file_dir + file, encoding="utf-8-sig", index_col=0) # 先把全部弹幕信息写成一个字符串,再调用方法 words = '' for item in data.DM_text: words += item words=" ".join(jieba.cut(words)) # 这个scale参数是画布大小参数,也就是调整分辨率的,10代表是原来的10倍大小,越高分辨率越高 wd = WordCloud(font_path='simhei.ttf', max_words=40, background_color='white',min_font_size=10,scale=10).generate(words) plt.imshow(wd) plt.axis("off") wd.to_file("./Analysis/词云/第" + str(sets) + "期词云.jpg") sets+=1 print("绘制完毕") if __name__ == '__main__': # 去重 duplicate(files[0]) # 每一期弹幕总数的变化折线图 danmuSumPlot(files[0]) # 发弹幕总数TOP10的用户柱状图 danmuUserTopBarh(files[0]) # 每期弹幕密度变化图 danmuDensityChange(files[0]) # 每期的弹幕词云 danmuWordCloud(files[0])
-
本文地址:https://blog.csdn.net/lkx_icy/article/details/107178025