最近看到李子柒回归的消息,借此机会练习从B站爬取弹幕并进行数据分析,本篇第一部分介绍数据爬取部分,第二部分介绍使用 python 对爬取的数据进行可视化分析。
一、数据爬取
首先准备好要爬取的视频链接,浏览器打开链接进 F12 观察请求接口

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

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

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

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

对接口有了大致了解,接下来就是编写 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条数据

二、数据分析&可视化
2.1数据预处理
在拿到弹幕数据后,首先导入到 pandas 用 info 看一下这份数据的大致情况,可以看到一共有10项数据,每一项都没有缺失值
1 2 3 4
| import pandas as pd
df = pd.read_csv('danmu.csv') print(df.info())
|

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

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', 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)
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() plt.tight_layout() plt.show()
|
执行以上代码,得到以下统计图:

分析:可以看到发送数量最多的弹幕内容是“哇”,共发送了 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()
def analyze_sentiment(text): """ 使用 SnowNLP 进行情感分析 """ s = SnowNLP(text) score = s.sentiments if score > 0.6: return '积极' elif score < 0.4: return '消极' else: return '中立'
sentiment_results = [analyze_sentiment(danmaku) for danmaku in danmaku_list]
df['Sentiment'] = sentiment_results
sentiment_counts = df['Sentiment'].value_counts()
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('') 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')
user_danmaku_count = df['发送者mid的HASH'].value_counts()
top_users = user_danmaku_count.head(10)
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) 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 分析图:

分析:可以看到发送弹幕数量最多的用户发送了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: 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=['弹幕发送时间'])
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