项目背景
随着短视频平台的快速发展,用户的互动行为(如评论、弹幕)已成为衡量视频内容受欢迎程度和观众情感的重要指标之一。在这些互动中,观众的情感倾向(积极、消极或中立)对于视频创作者和营销人员具有重要的分析价值。特别是Bilibili这一平台,以其独特的弹幕文化,吸引了大量年轻观众参与讨论与互动,弹幕成为了表达观众情感的重要方式。
本项目旨在对Bilibili视频的弹幕数据进行情感分析,探讨观众情感随视频播放进度的变化趋势。我选择了一段关于可爱小猫的视频,并爬取了视频的弹幕数据,包括弹幕的时间戳、内容、情感得分等信息。通过分析这些数据希望能够深入了解视频内容与观众情感之间的关系,并为未来的内容创作和数据分析提供新的见解和方法。
获取数据
利用python编写爬虫爬取数据和解析数据存储到csv文件
1、获取弹幕cid
import requests
import xml.etree.ElementTree as ET
import pandas as pd
## 根据视频bvid去获取cid
def get_cid_from_bvid(bvid):
url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
"Referer": f"https://www.bilibili.com/video/{bvid}"
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
data = response.json()
if data["code"] == 0 and "cid" in data["data"]:
return data["data"]["cid"]
else:
print("API 返回异常:", data)
else:
print("请求失败,状态码:", response.status_code)
return None
2、获取弹幕
def get_bilibili_danmaku(cid):
url = f"https://comment.bilibili.com/{cid}.xml" #f前缀代表格式化字符串,里面可以包含{cid}这种占位符,使其之后能被替换
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
}
response = requests.get(url,headers=headers)
if response.status_code == 200:
return response.content
else:
print(f"Error accessing the API. Status Code: {response.status_code}")
return None
3、解析弹幕
def parse_danmaku(xml_content):
root = ET.fromstring(xml_content) #解析xml格式的字符串
danmaku_list = []
# 遍历每个 <d> 标签
for d in root.findall('d'):
p_attrs = d.attrib['p'].split(',')
time_sec = float(p_attrs[0])
danmaku_type = int(p_attrs[1])
font_size = int(p_attrs[2])
color = f"#{int(p_attrs[3]):06X}"
timestamp = p_attrs[4]
user_id = p_attrs[6]
danmaku_text = d.text
danmaku_list.append({
'时间': time_sec,
'弹幕类型':danmaku_type,
'弹幕': danmaku_text,
'字体':font_size,
'颜色':color,
'时间戳': timestamp,
'用户id':user_id,
})
return danmaku_list
4、存储弹幕数据
def save_danmaku_to_csv(danmaku_list, filename):
df = pd.DataFrame(danmaku_list)
df.to_csv(filename, index=False)
分析数据
使用sonwnlp作为情感分析库,把用户情绪分为积极、消极和中立三种状态,试着查看弹幕情绪是否会随一天中不同时间段情绪发生变化
import pandas as pd
df = pd.read_csv('danmuku.csv')
df.head()
时间 | 弹幕类型 | 弹幕 | 字体 | 颜色 | 时间戳 | 用户id | |
---|---|---|---|---|---|---|---|
0 | 0.641 | 5 | 这猫很贵吗?为什么只买一点点 | 25 | #E70012 | 1643462388 | 8a6973e |
1 | 92.152 | 5 | 说了多少次猪不能染色(生气) | 25 | #E70012 | 1643380116 | aad8c8bd |
2 | 31.016 | 5 | 它好像知道自己很可爱 | 25 | #FFFFFF | 1643893967 | 1ace8411 |
3 | 7.242 | 5 | 那个小黑点是人家的修皮燕儿 | 25 | #E70012 | 1643528090 | b9026e56 |
4 | 20.315 | 5 | 《猫猫虫》 | 25 | #E70012 | 1643462540 | ecd1c9ba |
df['日期']=pd.to_datetime(df['时间戳'],unit='s')
df.head()
时间 | 弹幕类型 | 弹幕 | 字体 | 颜色 | 时间戳 | 用户id | 日期 | |
---|---|---|---|---|---|---|---|---|
0 | 0.641 | 5 | 这猫很贵吗?为什么只买一点点 | 25 | #E70012 | 1643462388 | 8a6973e | 2022-01-29 13:19:48 |
1 | 92.152 | 5 | 说了多少次猪不能染色(生气) | 25 | #E70012 | 1643380116 | aad8c8bd | 2022-01-28 14:28:36 |
2 | 31.016 | 5 | 它好像知道自己很可爱 | 25 | #FFFFFF | 1643893967 | 1ace8411 | 2022-02-03 13:12:47 |
3 | 7.242 | 5 | 那个小黑点是人家的修皮燕儿 | 25 | #E70012 | 1643528090 | b9026e56 | 2022-01-30 07:34:50 |
4 | 20.315 | 5 | 《猫猫虫》 | 25 | #E70012 | 1643462540 | ecd1c9ba | 2022-01-29 13:22:20 |
df = df[['用户id','弹幕','日期','时间']]
df.head()
用户id | 弹幕 | 日期 | 时间 | |
---|---|---|---|---|
0 | 8a6973e | 这猫很贵吗?为什么只买一点点 | 2022-01-29 13:19:48 | 0.641 |
1 | aad8c8bd | 说了多少次猪不能染色(生气) | 2022-01-28 14:28:36 | 92.152 |
2 | 1ace8411 | 它好像知道自己很可爱 | 2022-02-03 13:12:47 | 31.016 |
3 | b9026e56 | 那个小黑点是人家的修皮燕儿 | 2022-01-30 07:34:50 | 7.242 |
4 | ecd1c9ba | 《猫猫虫》 | 2022-01-29 13:22:20 | 20.315 |
import matplotlib.pyplot as plt
from snownlp import SnowNLP
df['情感得分']=df['弹幕'].apply(lambda x: SnowNLP(x).sentiments)
df.head()
用户id | 弹幕 | 日期 | 时间 | 情感得分 | |
---|---|---|---|---|---|
0 | 8a6973e | 这猫很贵吗?为什么只买一点点 | 2022-01-29 13:19:48 | 0.641 | 0.566113 |
1 | aad8c8bd | 说了多少次猪不能染色(生气) | 2022-01-28 14:28:36 | 92.152 | 0.274234 |
2 | 1ace8411 | 它好像知道自己很可爱 | 2022-02-03 13:12:47 | 31.016 | 0.843694 |
3 | b9026e56 | 那个小黑点是人家的修皮燕儿 | 2022-01-30 07:34:50 | 7.242 | 0.508325 |
4 | ecd1c9ba | 《猫猫虫》 | 2022-01-29 13:22:20 | 20.315 | 0.660834 |
#根据得分分类
def classify_sentiment(score):
if score>0.7:
return '积极'
elif score <0.4:
return '消极'
else:
return '中立'
df['情感']=df['情感得分'].apply(lambda x: classify_sentiment(x))
df.head()
用户id | 弹幕 | 日期 | 时间 | 情感得分 | 情感 | |
---|---|---|---|---|---|---|
0 | 8a6973e | 这猫很贵吗?为什么只买一点点 | 2022-01-29 13:19:48 | 0.641 | 0.566113 | 中立 |
1 | aad8c8bd | 说了多少次猪不能染色(生气) | 2022-01-28 14:28:36 | 92.152 | 0.274234 | 消极 |
2 | 1ace8411 | 它好像知道自己很可爱 | 2022-02-03 13:12:47 | 31.016 | 0.843694 | 积极 |
3 | b9026e56 | 那个小黑点是人家的修皮燕儿 | 2022-01-30 07:34:50 | 7.242 | 0.508325 | 中立 |
4 | ecd1c9ba | 《猫猫虫》 | 2022-01-29 13:22:20 | 20.315 | 0.660834 | 中立 |
from matplotlib import rcParams
# 设置中文字体,SimHei 是黑体,适用于大多数 Windows 系统
rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体
rcParams['axes.unicode_minus'] = False # 正常显示负号
# 统计情感分类的数量
sentiment_counts = df['情感'].value_counts()
# 扇形图绘制
labels = sentiment_counts.index
sizes = sentiment_counts.values
colors = ['#66b3ff', '#99ff99', '#ff6666'] # 分别代表积极、中立、消极
explode = (0, 0.1, 0) # 突出显示“积极”部分
plt.pie(sizes, explode=explode, labels=labels, colors=colors,
autopct='%1.1f%%', shadow=True, startangle=140)
plt.axis('equal') # 保证饼图是圆形的
plt.title('用户情感比例')
plt.show()
# 将弹幕按视频时间每30s区分时间段
df['时间段']=df['时间']//30+1
df.head()
用户id | 弹幕 | 日期 | 时间 | 情感得分 | 情感 | 时间段 | |
---|---|---|---|---|---|---|---|
0 | 8a6973e | 这猫很贵吗?为什么只买一点点 | 2022-01-29 13:19:48 | 0.641 | 0.566113 | 中立 | 1.0 |
1 | aad8c8bd | 说了多少次猪不能染色(生气) | 2022-01-28 14:28:36 | 92.152 | 0.274234 | 消极 | 4.0 |
2 | 1ace8411 | 它好像知道自己很可爱 | 2022-02-03 13:12:47 | 31.016 | 0.843694 | 积极 | 2.0 |
3 | b9026e56 | 那个小黑点是人家的修皮燕儿 | 2022-01-30 07:34:50 | 7.242 | 0.508325 | 中立 | 1.0 |
4 | ecd1c9ba | 《猫猫虫》 | 2022-01-29 13:22:20 | 20.315 | 0.660834 | 中立 | 1.0 |
# 计算每个时间段的平均情感得分和数量
sentiment_by_time = df.groupby('时间段')['情感得分'].describe().reset_index()
sentiment_by_time
时间段 | count | mean | std | min | 25% | 50% | 75% | max | |
---|---|---|---|---|---|---|---|---|---|
0 | 1.0 | 433.0 | 0.622972 | 0.230211 | 7.797457e-08 | 0.483517 | 0.660834 | 0.796624 | 0.999970 |
1 | 2.0 | 247.0 | 0.622350 | 0.275436 | 6.285078e-02 | 0.398633 | 0.612223 | 0.884300 | 0.999997 |
2 | 3.0 | 236.0 | 0.583431 | 0.274145 | 2.215580e-03 | 0.410290 | 0.590673 | 0.821006 | 0.999999 |
3 | 4.0 | 178.0 | 0.590221 | 0.269370 | 3.095113e-03 | 0.396908 | 0.569821 | 0.828248 | 1.000000 |
4 | 5.0 | 97.0 | 0.579521 | 0.249213 | 2.450946e-02 | 0.432985 | 0.526233 | 0.843138 | 0.999127 |
5 | 6.0 | 9.0 | 0.534244 | 0.112069 | 4.218746e-01 | 0.500000 | 0.500000 | 0.526233 | 0.770679 |
# 显示情感得分随时间变化的趋势
fig, ax1 = plt.subplots()
# 绘制平均情感得分变化趋势线
ax1.plot(sentiment_by_time['时间段'],sentiment_by_time['mean'],'g-')
ax1.set_xlabel('时间段(秒)')
ax1.set_ylabel('平均情感得分',color='g')
# 创建第二个坐标系
ax2 = ax1.twinx() # 共享x轴
# 绘制第二条线
ax2.plot(sentiment_by_time['时间段'],sentiment_by_time['count'],'b-')
ax2.set_ylabel('弹幕数量',color='b')
plt.title('情感得分和弹幕数量随视频时间变化趋势')
plt.show()
数据分析与推论:
视频内容的节奏变化原因:视频通过不同的情节或者节奏变化,导致观众情感的波动。随着视频进度,从折线图中发现观众情绪和弹幕数量下降,对比视频,发现视频通过开头的可爱小猫吸引观众眼球,观众的情感会较为积极,接着若视频进入较为平淡的部分,情感得分开始下降。
import jieba
from wordcloud import WordCloud
import matplotlib.pyplot as plt
from collections import Counter
# 预处理:将所有弹幕文本分词
df['分词'] = df['弹幕'].apply(lambda x: ' '.join(jieba.cut(x)))
# 分组:按情感分类,合并文本
sentiment_groups = df.groupby('情感')['分词'].apply(' '.join)
# 生成词云的函数
def generate_wordcloud(text, title, save_path=None):
wordcloud = WordCloud(
font_path='C:/Windows/Fonts/simhei.ttf',
width=800,
height=400,
background_color='white',
max_words=100
).generate(text)
plt.figure(figsize=(8, 4))
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis('off')
plt.title(title)
if save_path:
plt.savefig(save_path) # 保存到文件
else:
plt.show() # 显示词云
# 遍历每个情感类别生成词云
for sentiment, sentiment_text in sentiment_groups.items():
generate_wordcloud(sentiment_text, f'{sentiment} 情感下的高频词词云')
数据分析与推论:
在情感分析过程中词云图中出现与情感分类不匹配的词汇(如中立情感中出现“可爱”等积极词,消极情感中出现“卡哇伊”这种积极词)
df['hour'] = df['日期'].dt.hour # 提取小时部分
# 计算每小时各情绪的数量
emotion_counts = df.groupby(['hour', '情感']).size().unstack().fillna(0)
# 绘制折线图
plt.figure(figsize=(10, 6))
for emotion in emotion_counts.columns:
plt.plot(emotion_counts.index, emotion_counts[emotion], label=emotion)
plt.xlabel('时间(小时)')
plt.ylabel('情绪数量')
plt.title('一天中不同时间段的情绪变化')
plt.legend(title='情绪')
plt.grid(True)
plt.show()
数据分析与推论:
中午时段通常是许多人休息的时间,尤其是对于上班族或学生来说,他们可能会在午餐时间观看视频,因此中午时段的观众活跃度较高,导致弹幕数量增多。
结论
- 视频制作者通常通过增强视频开头的吸引力去吸引观众,但为了保持更好的视频质量,同时应该在视频的中后部分设计一些转折点或情节高潮。例如,加入小猫的突发行为、令人捧腹的互动或感人的情节
- 可以尝试更换情感分析模型或尝试通过手动校正某些词的情感类别来提高准确性,后期可以试着从多个角度入手:改进情感分析模型、调整分词和情感分类规则、去除歧义词汇,并结合上下文进行更加精细化的分析。通过这些手段,可以提高情感分析的准确性,从而生成更加符合情感分类的词云图。
- 由于中午时间段是观众互动的高峰期,视频制作者可以考虑在此时间段发布视频或推广已有视频,以便最大化观众参与度。特别是在观众的情感得分较为积极的时段发布视频,可以进一步提升观众的情感共鸣和互动积极性。