爬取B站视频弹幕并分析

最近看到李子柒回归的消息,借此机会练习从B站爬取弹幕并进行数据分析,本篇第一部分介绍数据爬取部分,第二部分介绍使用 python 对爬取的数据进行可视化分析。

一、数据爬取

首先准备好要爬取的视频链接,浏览器打开链接进 F12 观察请求接口

https://cloudflare-imgbed-ebz.pages.dev/file/1731934091240_image.png

并没有发现有明文返回弹幕信息的请求接口,继续点开视频右上角的弹幕列表,观察发送的请求接口

https://cloudflare-imgbed-ebz.pages.dev/file/1731936184646_image.png

这里发现一共发送了三次请求,观察到三次请求的 &segment_index 参数分别为 1,2,3,&w_rid 也全部都不相同

https://cloudflare-imgbed-ebz.pages.dev/file/1731936990359_image.png

将这三个接口分别复制到 Apifox,分别测试三个接口都返回了弹幕文件,但是返回文件显示格式似乎出现错误,同时在对比观察时发现第一个接口比后两个接口多了三个参数

https://cloudflare-imgbed-ebz.pages.dev/file/1731938063068_image.png

尝试在第二个接口中加上这三个参数并再次发送请求发现返回 403 状态码,应该是对接口做了反爬处理

https://cloudflare-imgbed-ebz.pages.dev/file/1731938284094_image.png

对接口有了大致了解,接下来就是编写 python 代码的时间,因为三个接口的参数有区别,这里直接把三次请求的参数放入列表中进行循环请求,同时打印响应内容发现还是乱码,尝试把响应数据写入文件并设置为 utf-8 编码格式,用 VSCode 打开发现依旧是乱码,发送请求的部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
cookies = {...}
headers = {...}
params = [{...},{...},{...}]

for i in range(0, len(params)):
resp = requests.get('https://api.bilibili.com/x/v2/dm/wbi/web/seg.so',
params=params[i],
cookies=cookies,
headers=headers)
print(f"HTTP 状态码: {resp.status_code}")
print(f"响应内容: {resp.text}")

带着疑问我在 GitHub 找到了 哔哩哔哩-API收集整理 这个项目,了解到原来返回的文件乱码是因为B站弹幕接口数据传输格式改为了 protobuf ,这个格式为二进制编码传输,需要额外的编译器进行解析,相关教程在这里可以找到 链接1链接2 ,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
my_seg = dm_pb2.DmSegMobileReply()
my_seg.ParseFromString(resp.content)
try:
with open('danmu.csv', 'a', newline='', encoding='utf-8-sig', ) as f:
w = csv.writer(f)
w.writerow(
['弹幕id', '视频内弹幕出现时间', '弹幕类型', '弹幕字号', '弹幕颜色',
'发送者mid的HASH', '弹幕内容', '弹幕发送时间', '弹幕dmid', '弹幕属性位'])
for elem in my_seg.elems:
danmu_id = elem.id
progress = elem.progress
mode = elem.mode
fontsize = elem.fontsize
color = elem.color
mid_hash = elem.midHash
content = elem.content
ctime = elem.ctime
id_str = elem.idStr
attr = elem.attr

w.writerow(
[f'"{danmu_id}"', progress, mode, fontsize, color, mid_hash,
content, ctime, f'"{id_str}"',attr])
print(f"第{i + 1}份--弹幕数据已写入到 danmu.csv")

# 休眠
time.sleep(3)
except Exception as e:
print('Exception-' + str(e))

ps:这里有个小坑,在写入 csv 文件时把编码格式设置为 utf-8 用 Excel 打开会乱码,需要设置为 utf-8-sig

返回数据字段信息如下所示

名称 类型 含义 备注
id int64 弹幕 dmid 唯一 可用于操作参数
progress int32 视频内弹幕出现时间 毫秒
mode int32 弹幕类型 123:普通弹幕
4:底部弹幕
5:顶部弹幕
6:逆向弹幕
7:高级弹幕
8:代码弹幕
9:BAS 弹幕(仅限于特殊弹幕专包)
fontsize int32 弹幕字号 18:小
25:标准
36:大
color uint32 弹幕颜色 十进制 RGB888 值
midHash string 发送者 mid 的 HASH 用于屏蔽用户和查看用户发送的所有弹幕,也可反查用户id
content string 弹幕内容 utf-8编码
ctime int64 弹幕发送时间 时间戳
weight int32 权重 用于智能屏蔽,根据弹幕语义及长度通过AI识别得出范围:[0-10]值越大权重越高
action string 动作?
pool int32 弹幕池 0:普通池
1:字幕池
2:特殊池(代码/BAS弹幕)
idStr string 弹幕 dmid 字串形式唯一 可用于操作参数
attr int32 弹幕属性位 bit0:保护
bit1:直播
bit2:高赞
animation string 动画?

最终成功将爬取到的数据写入 csv 文件,共计27602条数据

https://cloudflare-imgbed-ebz.pages.dev/file/1731944589874_image.png

二、数据分析&可视化

2.1数据预处理

在拿到弹幕数据后,首先导入到 pandas 用 info 看一下这份数据的大致情况,可以看到一共有10项数据,每一项都没有缺失值

1
2
3
4
import pandas as pd

df = pd.read_csv('danmu.csv')
print(df.info())

https://cloudflare-imgbed-ebz.pages.dev/file/1731957934097_image.png

再用 describe 看一下弹幕内容的大概情况,可以看到一共有 27601 项不为空的弹幕内容,其中有 20076 项不重复的弹幕内容,“哇”这个词出现了 646 次,是频率最高的弹幕内容

1
print(df['弹幕内容'].describe())

https://cloudflare-imgbed-ebz.pages.dev/file/1731958053331_image.png

2.2 生成弹幕词云图

生成词云图除了要用到 pandas 还有以下几个必要的库:

  • wordcloud:用于生成词云图
  • matplotlib:用于展示词云图
  • jieba:中文分词库,用于处理中文文本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_wordcloud():
# 提取弹幕内容
text_data = df['弹幕内容'].dropna() # 去除缺失值

# 对弹幕内容进行分词
text = ' '.join(text_data.apply(lambda x: ' '.join(jieba.cut(x))))

# 创建词云
wordcloud = WordCloud(
font_path='msyh.ttc', # 指定字体路径,确保能显示中文,msyh.ttc 是微软雅黑字体的路径
width=800, height=600, # 设置词云图的大小
background_color='white', # 设置背景色
max_words=1000, # 最大显示的词数
max_font_size=100, # 最大字体大小
).generate(text)

# 显示词云图
plt.figure(figsize=(10, 8))
plt.imshow(wordcloud)
plt.show()

执行上述代码,得到以下词云图:

弹幕词云图

分析:从该词云图中可以看出大量弹幕出现了“欢迎回来”,“生日快乐”,“回来了”,“太美了”等词眼,说明许多发送弹幕的观众都表达了对李子柒回归的欢迎和祝福,“马面裙”,“蜀锦”等词眼可能是在讨论视频中出现的传统文化相关的内容。

2.3 分析弹幕 TOP10 内容

在看到词云图后,我开始对发送数量前10的弹幕是什么感到好奇,于是准备开始通过代码生成弹幕数量排行前10的统计图。

首先统计弹幕出现次数需要用到 collections 中的 Counter,将弹幕内容转换为列表再传进Counter()就得到了所以弹幕出现次数的统计数据,截取前10的数据并作图就得到了弹幕数量排行前10的统计图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from collections import Counter
def get_top_dm():
df = pd.read_csv('danmu.csv')

danmaku_list = df['弹幕内容'].dropna().tolist() # 去除空值并转换为列表

# 统计弹幕出现次数
counter = Counter(danmaku_list)

# 获取出现次数最多的前 10 条弹幕
top_10 = counter.most_common(10)

# 绘制图表
labels, values = zip(*top_10) # 解压为两个列表
plt.figure(figsize=(10, 6))
bars = plt.barh(labels, values, color='skyblue')
# 在每个柱状条上标注具体数据值
for bar, value in zip(bars, values):
plt.text(value + 1, bar.get_y() + bar.get_height() / 2, str(value), va='center', fontsize=10)

plt.xlabel('出现次数')
plt.ylabel('弹幕内容')
plt.title('弹幕数量排行前10')
plt.gca().invert_yaxis() # 翻转 y 轴,最多的弹幕排在最上面
plt.tight_layout()
plt.show()

执行以上代码,得到以下统计图:

弹幕数量 TOP10

分析:可以看到发送数量最多的弹幕内容是“哇”,共发送了 646 次,”欢迎回来”和“欢迎回来!”共发送了 805 次,“生日快乐”、“生日快乐!!!”、“生日快乐!”共发送了 604 次,剩下的热度比较高的词也是以夸赞性的词为主,和词云图看到的信息大致一致,弹幕主要表达了观众对李子柒回归的欢迎和生日的祝福。

2.4 弹幕内容情感分析

在看到发送的弹幕数量前10都是对李子柒这期视频的正面评价,我有了一个新的问题,绝大部分弹幕都是对李子柒的夸赞吗?为此,我决定对弹幕内容进行情感分析。

首先选择 Snownlp 这个库进行情感分析,因为它专门针对中文文本优化,尤其在处理分词和情感分析时表现较好,所以更适合用来进行中文情感分析。使用 SnowNLP 的 sentiment 方法对文本进行情感分析,该方法返回一个 0-1 之间的情感极性值,值越接近 1 表示情感越积极,值越接近 0 表示情感越消极。这里我自定义了阈值大于 0.6 为正面情绪,小于 0.4 为负面情绪,介于之间的为中性情绪。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
from snownlp import SnowNLP

def sentiment_analysis():
df = pd.read_csv('danmu.csv')

danmaku_list = df['弹幕内容'].dropna().tolist() # 去除空值并转换为列表

# 1. 定义情感分析函数
def analyze_sentiment(text):
"""
使用 SnowNLP 进行情感分析
"""
s = SnowNLP(text)
score = s.sentiments # 得分范围 0 ~ 1,接近 1 为正面,接近 0 为负面
if score > 0.6: # 自定义正面情感阈值
return '积极'
elif score < 0.4: # 自定义负面情感阈值
return '消极'
else:
return '中立'

# 2. 对弹幕内容进行情感分类
sentiment_results = [analyze_sentiment(danmaku) for danmaku in danmaku_list]

# 将结果添加到原始数据
df['Sentiment'] = sentiment_results

# 3. 统计情感类别的比例
sentiment_counts = df['Sentiment'].value_counts()

# 4. 绘制饼图展示情感分布
plt.figure(figsize=(8, 6))
colors = ['#57A0D3', '#A0D357', '#D35757'] # 为正面、中性、负面情感设置颜色
sentiment_counts.plot.pie(autopct='%1.1f%%', colors=colors, startangle=140, textprops={'fontsize': 12})
plt.title('弹幕情感分析结果')
plt.ylabel('') # 隐藏 y 轴标签
plt.tight_layout()
plt.show()

执行以上代码,得到以下情感分析图:

弹幕情感分析图

分析:可以看到发送的弹幕中最多的是正面情绪的弹幕,占比达到了 55.9%,中立弹幕占到了 26.9%,最少的是负面情绪的弹幕,占到了17.2%,由此可见大部分弹幕都是积极的情绪,只有较少占比的弹幕是比较消极的。

2.5 分析发送弹幕数量最多的用户

在做完以上分析,我突发奇想,观看量这么高的视频应该有不少人发了不止一条弹幕,那么在这个视频里发送弹幕最多的用户到底发了多少条弹幕呢?好在从B站弹幕接口里拿到的数据中的“发送者mid的HASH”是对用户id进行了hash计算后的结果,保证了用户信息的安全,所以在这里可以直接用“发送者mid的HASH”这个字段进行统计展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def send_dm_user_top():
df = pd.read_csv('danmu.csv')

# 1. 统计每个用户发送弹幕的数量
user_danmaku_count = df['发送者mid的HASH'].value_counts()

# 2. 获取弹幕数量排名前 10 的用户
top_users = user_danmaku_count.head(10)

# 3. 绘制柱状图展示用户弹幕数量排行
plt.figure(figsize=(10, 6))
bars = top_users.plot(kind='bar', color='#57A0D3', edgecolor='black')
plt.title('弹幕数量最多的用户排行榜', fontsize=16)
plt.xlabel('用户 ID(已加密)', fontsize=14)
plt.ylabel('弹幕数量', fontsize=14)
plt.xticks(rotation=45, fontsize=12)
# 4. 在每个柱状图上显示具体的弹幕数量
for bar in bars.patches: # 遍历所有柱子
height = bar.get_height() # 获取柱子的高度(弹幕数量)
plt.text(bar.get_x() + bar.get_width() / 2, height, # 设置文本位置
f'{int(height)}', # 显示数据
ha='center', va='bottom', fontsize=12) # 字体样式
plt.tight_layout()
plt.show()

执行以上代码,得到以下 TOP10 分析图:

发送弹幕最多的用户TOP10

分析:可以看到发送弹幕数量最多的用户发送了27条弹幕,第二名紧跟其后发了26条弹幕,第8、9、10名都发送了13条弹幕,看来还是有一部分观众对这个视频发言很积极的,同一个视频发送了10条以上的弹幕。

2.6 分析用户弹幕数量分布情况

在看完了发送弹幕数量最多的前10名用户,那么整体用户发送的弹幕数量情况是怎样的,大部分用户都发送了几条弹幕呢?为了得到答案,我决定用直方图来分析用户发弹幕的数量分布情况。这里使用“发送者mid的HASH”来统计每个用户发送了多少条弹幕,将取到的值传入到plt.hist()生成直方图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def dm_distribution():
df = pd.read_csv('danmu.csv')

# 获取用户的弹幕数量统计
user_danmaku_count = df['发送者mid的HASH'].value_counts()

# 绘制直方图
plt.figure(figsize=(12, 6))
counts, bins, patches = plt.hist(user_danmaku_count, bins=30, color='#57A0D3', edgecolor='black')

# 添加标题和标签
plt.title('用户发送弹幕数量分布情况', fontsize=16)
plt.xlabel('每个用户的弹幕数量', fontsize=14)
plt.ylabel('用户数量', fontsize=14)

# 在每个柱子上显示数据
for i in range(len(patches)):
height = counts[i] # 每个柱子的高度
if height > 0: # 仅显示大于0的数据
plt.text(patches[i].get_x() + patches[i].get_width() / 2, height, int(height),
ha='center', va='bottom', fontsize=10, color='black')
plt.tight_layout()
plt.show()

执行以上代码,得到以下分析图:

用户发送弹幕数量分布图

分析:可以看到绝大多数的观众在观看此视频时都只发了1条弹幕,小部分观众发了2条以上,其中有极少一部分观众发送了超过5条弹幕,发送弹幕最多的观众一共发了27条。这份分布图还是比较符合我的预期的,果然能坚持发多条弹幕的真爱粉还是少数。

2.7 分析弹幕数量随日期变化情况

“弹幕发送时间”这个字段是字符串类型,需要先转换为 int 类型,再转换为 datetime 类型。

数据格式准备好后,接下来就是画图的部分,这里需要用到 matplotlib 这个库,统计每天有多少条弹幕需要用到 pandas 库中的 resample(),这里df.resample('1d', on='弹幕发送时间').size()将 DataFrame 按'1d')重采样,on='弹幕发送时间'指定了用’弹幕发送时间’这一列进行时间的分组,size()会返回每个分组的大小,即每一天的弹幕数量。因此,这行代码会返回一个按天索引的 Series,其中每个索引(日期)对应的值就是那一天的弹幕数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def get_dm_num_by_date_time():
df = pd.read_csv('danmu.csv')

# 清理列名和数据
df.columns = df.columns.str.strip() # 清理列名中的多余空格
df['弹幕发送时间'] = df['弹幕发送时间'].str.strip('"') # 移除时间字段中的引号
df['弹幕发送时间'] = pd.to_numeric(df['弹幕发送时间'], errors='coerce') # 转换为数值
df = df.dropna(subset=['弹幕发送时间']) # 删除无效时间值的行

# 转换时间字段为datetime格式
df['弹幕发送时间'] = pd.to_datetime(df['弹幕发送时间'], unit='s')

# 按天分组统计弹幕数量
danmu_per_minute = df.resample('1d', on='弹幕发送时间').size()

# 绘制折线图
plt.figure(figsize=(12, 6))
plt.plot(danmu_per_minute.index, danmu_per_minute.values, marker='o', linestyle='-', alpha=0.7)
plt.title('弹幕数量随日期变化折线图')
plt.xlabel('时间')
plt.ylabel('弹幕数量')
plt.grid(True)
plt.tight_layout()
plt.show()

执行代码,得到以下折线图:

弹幕数量随日期变化图

分析:从图中可以看出发送弹幕数量最多的一天是2024年11月13日,也就是视频发布当天,弹幕数量最高超过 14000 条,随后每天弹幕数量都在持续下降,到2024年11月18日这天发送弹幕数量已经在 1000 条左右。看来观众对李子柒回归后这期视频的围观和讨论主要集中在发布视频当天,随后开始持续下降。

参考链接:

GitHub - SocialSisterYi/bilibili-API-collect: 哔哩哔哩-API收集整理【不断更新中….】

b站 实时弹幕和历史弹幕 Protobuf 格式解析-CSDN博客

Protocol Buffers