在现代短视频与直播平台中,弹幕不仅是用户互动的工具,更是分析观众兴趣、情绪和行为模式的重要数据源。本文将以 B 站(Bilibili)为例,使用 Python 完整实现从弹幕抓取、数据处理到可视化分析的全流程。
1. 项目概述
本项目目标是抓取指定 B 站视频的所有分 P 弹幕,并进行文本分析与可视化,具体功能包括:
- 获取视频的分 P 信息(cid)。
- 抓取每个分 P 的弹幕 XML 数据。
- 将弹幕保存为 CSV 文件。
- 进行文本分析(词频统计、关键词统计)。
- 生成可视化图表(时间分布图、词云、弹幕长度分布图等)。
2. 弹幕抓取实现
2.1 配置与请求头
在抓取 B 站弹幕时,需要设置请求头以模拟浏览器访问:
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
同时配置视频 BV 号和输出文件名:
BVID = "BV19R4y1K7yc"
OUTPUT_CSV = f"公孙田浩弹幕_{BVID}_danmu.csv"
2.2 获取所有分 P 的 cid
B 站视频可能存在多 P,每个分 P 对应一个 cid,通过官方 API 可以获取:
def fetch_cid_list(bvid):
cid_api = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp"
response = requests.get(cid_api, headers=headers, timeout=10)
response.raise_for_status()
return response.json()['data']
企业级提示:异常处理必不可少,网络请求失败或 JSON 解析异常都需要捕获,以保证程序健壮性。
2.3 获取弹幕 XML 数据
通过 cid 可以访问弹幕 XML 文件,并解析每条弹幕属性:
from lxml import etree
def fetch_danmu_for_cid(cid, page_index):
danmu_url = f"https://comment.bilibili.com/{cid}.xml"
xml_response = requests.get(danmu_url, headers=headers, timeout=10)
xml_response.encoding = 'utf-8'
root = etree.fromstring(xml_response.text.encode('utf-8'))
danmus = []
for d in root.xpath("//d"):
p_attr = d.attrib.get("p", "").split(",")
if len(p_attr) < 8: continue
danmus.append({
"page": page_index,
"time_in_video": float(p_attr[0]),
"mode": int(p_attr[1]),
"font_size": int(p_attr[2]),
"color": int(p_attr[3]),
"send_time": p_attr[4],
"danmu_pool": p_attr[5],
"user_hash": p_attr[6],
"danmu_id": p_attr[7],
"content": d.text
})
return danmus
注意:XML 解析错误和非法内容需捕获并记录,保证数据完整性。
2.4 主流程与数据保存
将抓取的弹幕统一保存到 CSV,方便后续分析:
import pandas as pd
pages = fetch_cid_list(BVID)
danmus = []
for page in pages:
danmus.extend(fetch_danmu_for_cid(page['cid'], page.get('page', 1)))
df = pd.DataFrame(danmus)
df.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig")
print(f"弹幕已保存到 {OUTPUT_CSV}")
3. 弹幕数据分析
3.1 数据清洗与基本信息
使用 pandas 对 CSV 数据进行处理:
df['send_time'] = pd.to_datetime(df['send_time'], unit='s')
df['content'] = df['content'].astype(str)
print(f"总弹幕数量: {len(df)}")
3.2 高频词分析与词云
结合 jieba 分词库,过滤停用词后统计词频,并生成词云:
import jieba
from collections import Counter
from wordcloud import WordCloud
import matplotlib.pyplot as plt
stopwords = {'的','了','在','是','我','有','和','就','不','人','都','一','一个','上','也','很','到','说','要','去','你','会','着','没有','看','好','自己','这'}
all_text = ' '.join(df['content'].tolist())
words = [w for w in jieba.cut(all_text) if len(w) > 1 and w not in stopwords]
word_freq = Counter(words)
wordcloud = WordCloud(width=800, height=600, background_color='white', font_path='simhei.ttf').generate_from_frequencies(word_freq)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.show()
企业级建议:词云可视化有助于快速捕捉弹幕热点主题和关键词。
3.3 弹幕时间分布
分析弹幕发送时间,绘制柱状图:
df['hour'] = df['send_time'].dt.hour
hourly_counts = df['hour'].value_counts().sort_index()
plt.bar(hourly_counts.index, hourly_counts.values, color='skyblue')
plt.xlabel('小时')
plt.ylabel('弹幕数量')
plt.title('弹幕时间分布(按小时)')
plt.show()
3.4 关键词统计与长度分布
针对核心关键词进行统计,并分析弹幕长度分布:
keywords = ['开盒','个人信息','电报','缓刑','犯罪','法律','处罚','网络','平台','主播']
keyword_counts = {kw: df['content'].str.contains(kw, na=False).sum() for kw in keywords}
plt.bar(keywords, list(keyword_counts.values()), color='lightcoral')
plt.show()
df['content_length'] = df['content'].apply(len)
plt.hist(df['content_length'], bins=30, color='lightgreen')
plt.show()
此方法可帮助分析用户关注的热点话题及内容互动特点。
4. 总结与企业级应用价值
通过以上流程,我们实现了从 B 站视频弹幕抓取到数据分析的闭环。该方法的应用场景包括:
- 内容运营分析:了解用户最关注的内容点和互动时段。
- 舆情监测:对关键词进行统计,快速捕捉负面信息或话题热点。
- 推荐算法优化:分析弹幕情绪、长度、时间分布,为视频推荐提供数据支撑。
结语:弹幕中的社会脉搏
通过Python的强大生态,我们不仅可以抓取弹幕数据,更能从中解读出丰富的文化和社会信息。每一次弹幕的发送都是用户情感的即时表达,而将这些碎片化的表达汇聚起来,就能看到一幅生动的网络社会图景。
技术不是目的,而是理解世界的工具。希望本文为你打开了弹幕分析的大门,期待看到你更有创意的应用!
完整代码:
import requests
from lxml import etree
import pandas as pd
# ---------- 配置 ----------
# BVID: B站视频的BV号,用于获取该视频的所有分P信息
BVID = "BV19R4y1K7yc"
# headers: 请求头配置,模拟浏览器访问
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
# OUTPUT_CSV: 输出的CSV文件名,格式为“视频标题_弹幕.csv”
OUTPUT_CSV = f"公孙田浩弹幕_{BVID}_danmu.csv"
# ---------- 1. 获取所有 P 的 cid ----------
def fetch_cid_list(bvid):
"""
根据视频的BV号获取所有分P的cid列表。
参数:
bvid (str): 视频的BV号
返回:
list: 包含每个分P信息的字典列表,每个字典包含 'cid', 'page' 等字段
异常:
requests.exceptions.RequestException: 网络请求失败时抛出
requests.exceptions.JSONDecodeError: 响应内容无法解析为JSON时抛出
"""
cid_api = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp"
try:
response = requests.get(cid_api, headers=headers, timeout=10)
response.raise_for_status()
cid_data = response.json()
return cid_data['data']
except requests.exceptions.RequestException as e:
print("请求 cid 列表失败:", str(e))
raise
except requests.exceptions.JSONDecodeError:
print("无法解析JSON响应,响应内容为:", response.text)
print("状态码:", response.status_code)
raise
# ---------- 2. 获取弹幕数据 ----------
def fetch_danmu_for_cid(cid, page_index):
"""
根据cid获取指定分P的弹幕数据。
参数:
cid (int): 分P对应的cid
page_index (int): 当前分P的索引编号(从1开始)
返回:
list: 弹幕信息列表,每个元素是一个包含弹幕属性的字典
"""
danmu_url = f"https://comment.bilibili.com/{cid}.xml"
try:
xml_response = requests.get(danmu_url, headers=headers, timeout=10)
xml_response.raise_for_status()
xml_response.encoding = 'utf-8'
xml_text = xml_response.text.strip()
# 检查是否是合法的XML内容
if not (xml_text.startswith('<?xml') or xml_text.startswith('<')):
print(f"第 {page_index} P 弹幕获取异常,内容预览:")
print(xml_text[:500])
return []
root = etree.fromstring(xml_text.encode('utf-8'))
danmus = []
for d in root.xpath("//d"):
p_attr = d.attrib.get("p", "").split(",")
if len(p_attr) < 8:
continue
danmus.append({
"page": page_index,
"time_in_video": float(p_attr[0]), # 弹幕在视频中的出现时间(秒)
"mode": int(p_attr[1]), # 弹幕类型(1: 滚动, 4: 底部, 5: 顶部)
"font_size": int(p_attr[2]), # 字体大小
"color": int(p_attr[3]), # 颜色RGB值
"send_time": p_attr[4], # 发送时间戳
"danmu_pool": p_attr[5], # 弹幕池分类
"user_hash": p_attr[6], # 用户ID哈希值
"danmu_id": p_attr[7], # 弹幕唯一ID
"content": d.text # 弹幕文本内容
})
return danmus
except requests.exceptions.RequestException as e:
print(f"第 {page_index} P 请求失败:", str(e))
return []
except etree.XMLSyntaxError as e:
print(f"第 {page_index} P XML解析错误:", str(e))
print(xml_text[:1000])
return []
# ---------- 主流程 ----------
# 获取所有分P的cid信息
pages = fetch_cid_list(BVID)
danmus = []
# 遍历所有分P并抓取弹幕
for page in pages:
cid = page['cid']
page_index = page.get('page', 1)
print(f"正在抓取第 {page_index} P 弹幕,cid={cid} ...")
danmus.extend(fetch_danmu_for_cid(cid, page_index))
print(f"总共抓取弹幕数量: {len(danmus)}")
# ---------- 3. 保存到 CSV ----------
# 将弹幕数据转换为DataFrame并保存为CSV文件
df = pd.DataFrame(danmus)
df.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig")
print(f"弹幕已保存到 {OUTPUT_CSV}")
# ---------- 4. 输出全部弹幕 ----------
# 打印所有弹幕内容,处理可能存在的编码问题
print(f"弹幕内容预览 (共 {len(danmus)} 条):")
for d in danmus:
safe_output = []
for item in d.values():
if isinstance(item, str):
try:
item.encode('gbk')
safe_output.append(item)
except UnicodeEncodeError:
safe_output.append(item.encode('gbk', errors='replace').decode('gbk'))
else:
safe_output.append(item)
print(tuple(safe_output))
import logging
from collections import Counter
import jieba
import matplotlib.pyplot as plt
import pandas as pd
from wordcloud import WordCloud
# 减少jieba日志输出
jieba.setLogLevel(logging.INFO)
# 解决中文显示问题
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# 读取CSV文件中的弹幕数据
file_path = "公孙田浩弹幕_BV19R4y1K7yc_danmu.csv"
df = pd.read_csv(file_path, encoding="utf-8-sig")
# 数据清洗和预处理
# 将 send_time 列转换为 datetime 类型,content 列确保为字符串类型
df['send_time'] = pd.to_datetime(df['send_time'], unit='s')
df['content'] = df['content'].astype(str)
# 显示所有弹幕数据的基本信息
print("弹幕数据基本信息:")
print(f"总弹幕数量: {len(df)}")
print(f"数据列数: {len(df.columns)}")
print(f"列名: {list(df.columns)}")
# 提取所有弹幕内容并拼接成一个字符串
contents = df['content'].tolist()
all_text = ' '.join(contents)
# 使用jieba进行中文分词
words = list(jieba.cut(all_text))
# 过滤掉单字符和无意义的词 (使用和之前一致的停用词)
stopwords = {'的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这'}
filtered_words = [word for word in words if len(word) > 1 and word not in stopwords]
# 统计词频
word_freq = Counter(filtered_words)
# 显示高频词 (恢复到Top 30)
print("\n高频词汇 Top 30:")
for word, freq in word_freq.most_common(30):
print(f"{word}: {freq}")
# 1. 时间分布图
# 绘制弹幕按小时的时间分布柱状图
plt.figure(figsize=(10, 6))
df['hour'] = df['send_time'].dt.hour
hourly_counts = df['hour'].value_counts().sort_index()
plt.bar(hourly_counts.index, hourly_counts.values, color='skyblue')
plt.xlabel('小时')
plt.ylabel('弹幕数量')
plt.title('弹幕时间分布(按小时)')
plt.xticks(range(0, 24))
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('danmu_time_distribution.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n时间分布图已保存为 danmu_time_distribution.png")
# 2. 词云图 (使用相同的词频数据)
# 根据词频生成词云图,并尝试使用指定字体
plt.figure(figsize=(10, 8))
try:
wordcloud = WordCloud(
width=800,
height=600,
background_color='white',
font_path='simhei.ttf', # Windows系统黑体
max_words=100,
relative_scaling=0.5,
random_state=42
).generate_from_frequencies(word_freq)
except OSError:
# 如果找不到指定字体,使用默认设置
wordcloud = WordCloud(
width=800,
height=600,
background_color='white',
max_words=100,
relative_scaling=0.5,
random_state=42
).generate_from_frequencies(word_freq)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.title('弹幕词云图')
plt.tight_layout()
plt.savefig('danmu_wordcloud.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n词云图已保存为 danmu_wordcloud.png")
# 3. 关键词统计图
# 对特定关键词在弹幕中出现的次数进行统计并绘制柱状图
plt.figure(figsize=(10, 6))
# 定义关键词
keywords = ['开盒', '个人信息', '电报', '缓刑', '犯罪', '法律', '处罚', '网络', '平台', '主播']
keyword_counts = {}
for keyword in keywords:
keyword_counts[keyword] = df['content'].str.contains(keyword, na=False).sum()
# 绘制关键词统计图
bars = plt.bar(keywords, list(keyword_counts.values()), color='lightcoral')
plt.xlabel('关键词')
plt.ylabel('出现次数')
plt.title('关键词统计')
plt.xticks(rotation=45)
plt.grid(axis='y', alpha=0.3)
# 在柱状图上添加数值标签
for bar in bars:
height = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2., height,
f'{int(height)}',
ha='center', va='bottom')
plt.tight_layout()
plt.savefig('danmu_keyword_statistics.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n关键词统计图已保存为 danmu_keyword_statistics.png")
# 4. 弹幕长度分布
# 统计每条弹幕的字符长度并绘制直方图
plt.figure(figsize=(10, 6))
df['content_length'] = df['content'].apply(len)
plt.hist(df['content_length'], bins=30, color='lightgreen', edgecolor='black')
plt.xlabel('弹幕长度(字符数)')
plt.ylabel('弹幕数量')
plt.title('弹幕长度分布')
plt.grid(axis='y', alpha=0.3)
plt.tight_layout()
plt.savefig('danmu_length_distribution.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n弹幕长度分布图已保存为 danmu_length_distribution.png")
# 输出统计信息
print(f"\n弹幕统计信息:")
print(f"平均每条弹幕长度: {df['content_length'].mean():.2f} 字符")
print(f"最长弹幕: {df['content_length'].max()} 字符")
print(f"最短弹幕: {df['content_length'].min()} 字符")
# 输出时间分布信息
print(f"\n时间分布信息:")
print(f"最早弹幕: {df['send_time'].min()}")
print(f"最晚弹幕: {df['send_time'].max()}")
数据来源:B站公开弹幕数据
工具栈:Python, requests, pandas, jieba, matplotlib, wordcloud
🙋♂️ 如果你觉得这篇文章有帮助:
- 点赞 👍
- 收藏 ⭐
- 评论 💬
- 关注我 👇 获取后续实战内容