简介:一套面向毕业设计、课程设计与项目演示的B站弹幕数据分析完整项目,涵盖爬虫采集、词云与词频统计、情感分析和衍生指标构建,涉及Selenium与Python技术栈,既能为计算机专业学生提供可复用的高分参考,也能帮助初学者快速上手数据处理流程。压缩包共1442个文件,大小28.29MB,以1152个CSV词频统计表和16个Python脚本为主体,配合22张PNG可视化图、6个HTML报告及xlsx、md等说明文件,目录分层清晰,从爬虫数据到结果图表形成完整链路,便于按模块取用。项目系个人高分源码,已通过导师认可并取得答辩评审95分,代码经过测试运行,功能稳定,可直接演示或作为二次开发基础。除完整源码和详细文档外,资源还提供按科学科普、社科人文、校园学习、财经、野生技术协会等知识区拆分的词频数据,包括对应的可视化结果,以及情感分析与衍生指标构建的具体思路,方便读者结合自身课题灵活调整。已有59人学习下载,适合需要快速搭建弹幕分析任务或系统掌握相关技术链条的学生、教师和企业员工。
1. 从一条弹幕到一张可视化大屏,B站弹幕分析到底在分析什么
B站弹幕和普通评论最大的区别是带时间轴:用户在视频的某一秒说出当下感受,情绪和画面绑定,密度和节奏也是内容的一部分。所以基于B站弹幕的数据分析,不只是把弹幕抓下来画个词云,而是要把「视频内容 -> 用户即时反应 -> 整体情感曲线 -> 可量化指标」这条链路打通。整个过程会用到爬虫、词云、词频、情感分析,再往上叠加弹幕密度、时间分布、情感波动等衍生指标,最后落到可视化大屏或报告页。适合谁看:想用Python做爬虫练手但不想只抓静态页面的,做内容运营想量化视频表现的,以及准备用数据分析项目作为简历作品集的。这篇直接讲清楚每一步怎么做、参数怎么调、坑在哪里,核心代码可以用,不会有「只讲概念不给代码」的部分。
2. B站弹幕爬虫:从接口协议到并发设计,先拿到干净数据
2.1 弹幕接口不是页面里的,是CID换来的
B站弹幕不走搜索页或视频页的HTML解析,而是通过视频的cid去请求专门的弹幕XML接口。常见做法是先拿BV号,再通过B站公开的view接口把cid取出来,然后用cid拼出弹幕文件地址。接口地址一般是https://api.bilibili.com/x/web-interface/view和https://comment.bilibili.com/{cid}.xml,前者返回视频元数据,后者直接给XML文本。
这里要先说明:B站风控在弹幕接口上比搜索接口宽松,但并不意味着可以无脑并发。弹幕分历史弹幕和当前弹幕,当前弹幕接口只返回最近一部分,想要全量数据必须去https://api.bilibili.com/x/v1/dm/list.so?oid={cid}这类历史弹幕接口,需要带cookie或wbi签名。新人最容易犯的错是只在XML接口上拿数据,结果弹幕数量少得可怜,还以为是代码问题。
import requests import re def get_cid(bvid): url = "https://api.bilibili.com/x/web-interface/view" params = {"bvid": bvid} resp = requests.get(url, params=params, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.bilibili.com/" }) data = resp.json() return data["data"]["cid"] def fetch_danmaku(bvid): cid = get_cid(bvid) xml_url = f"https://comment.bilibili.com/{cid}.xml" resp = requests.get(xml_url, headers={"Referer": "https://www.bilibili.com/"}) # 弹幕内容提取 danmaku_list = re.findall(r'<d p="(.*?)">(.*?)</d>', resp.text) return danmaku_list if __name__ == "__main__": bv = "BV1xx411c7mD" result = fetch_danmaku(bv) print(result[:3])这段代码里,get_cid负责从BV号拿cid,fetch_danmaku再组合弹幕接口。注意<d p="...">里用引号包裹的才是弹幕参数,后面的文本才是弹幕内容。这里刻意没有直接解析p参数,因为参数段里包含时间戳、模式、字号、颜色、发送时间等一串数据,留到下一步处理更清晰。
2.2 弹幕p参数解析:时间、用户、模式都在这一行里
弹幕XML里每条弹幕的p属性都是逗号分隔的,常见顺序是:出现时间(秒)、弹幕模式、字号、颜色、发送时间戳、用户hash、弹幕ID。其中发送时间戳是Unix秒级,用户hash是B站脱敏后的用户标识,不能反查真实UID,但可以用它做去重和参与用户量估算。
解析时用split(",")就够了,不要想复杂。模式字段值得解释一下:1代表滚动弹幕,4代表底部,5代表顶部,7是高级弹幕,9是BAS弹幕。做词频或情感分析时,建议过滤掉模式小于1和大于6的弹幕,因为滚动和固定弹幕才是正常用户输入,高级弹幕很多是代码生成的。
import time def parse_danmaku(raw_list): parsed = [] for attr, content in raw_list: attr_parts = attr.split(",") if len(attr_parts) < 8: continue mode = int(attr_parts[1]) if mode < 1 or mode > 6: continue parsed.append({ "time": float(attr_parts[0]), "mode": mode, "font_size": int(attr_parts[2]), "color": int(attr_parts[3]), "send_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(int(attr_parts[4]))), "user_hash": attr_parts[5], "content": content }) return parsed这个解析函数把每条弹幕变成字典,后续做数据分析时直接操作content和send_time字段。注意这里跳过了弹幕ID,因为它通常只用于去重接口,普通分析用不上。time.localtime把发送时间转成可读字符串,方便后面按小时或按天聚合。
2.3 requests爬虫的并发设计,到底哪个好
很多人遇到弹幕量大的视频第一反应是上多线程,但B站弹幕接口单次返回就有几百条,普通视频根本不需要并发。真正需要并发的是「批量抓多个视频」的场景,比如做UP主作品全集分析。这时并发设计的选择直接影响效率,常见的做法是concurrent.futures.ThreadPoolExecutor,而不是asyncio。
原因很直接:弹幕接口是纯IO等待,线程池的GIL切换开销远小于网络延迟。asyncio要额外引入httpx或aiohttp,代码复杂度上升,收益不大。更推荐用requests加线程池,配合信号量控制并发数,防止被封IP。
from concurrent.futures import ThreadPoolExecutor, as_completed import threading def fetch_with_semaphore(bvid): with sem: return bvid, fetch_danmaku(bvid) bvid_list = ["BV1xx411c7mD", "BV1Qy4y1n7Jt", "BV1L4411o7Vb"] sem = threading.Semaphore(5) with ThreadPoolExecutor(max_workers=8) as executor: futures = [executor.submit(fetch_with_semaphore, bv) for bv in bvid_list] for future in as_completed(futures): bv, data = future.result() print(f"{bv} 抓到 {len(data)} 条弹幕")注意这里的Semaphore(5)是全局并发上限,max_workers=8是线程池大小。实际运行中线程池开到8,但同一时刻只有5个请求真正打到B站,剩余3个在等信号量。这样设计的好处是线程可以快速抢占任务,又不会因为并发太高触发B站风控。如果发现频繁返回-412,把信号量降到3即可。
提示:B站对未登录接口有频控,批量抓取时建议加
time.sleep(0.5)或使用随机间隔,不要追求极限速度。
3. 弹幕文本清洗与词频统计:先把「哈哈哈哈」变成能分析的词
3.1 去除噪声词和短文本,保留有效弹幕
弹幕文本质量比评论差很多,大量弹幕是「哈哈哈哈」「???」「awsl」「草」这类无意义内容,还有「前方高能」「空降成功」这种特殊文化词。做词频和情感分析之前,必须做文本清洗,否则高频词永远是语气词和梗。
清洗规则一般分几步:去掉html实体、全角转半角、去除emoji和特殊符号、过滤纯数字和纯标点、过滤长度小于2的弹幕。再配合停用词表,把「的地得」「了」「啊」这类助词去掉。这里不要把「哈哈哈哈」全删掉,因为它本身能反映弹幕密度和情绪强度,可以在衍生指标里保留。
import re import jieba from collections import Counter stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: stopwords.add(line.strip()) def clean_text(text): text = re.sub(r'&#\d+;', '', text) text = re.sub(r'[a-zA-Z0-9]', '', text) text = re.sub(r'[^\u4e00-\u9fa5]', '', text) text = text.replace("哈哈哈", "").replace("hhhh", "") return text.strip() def word_freq(danmaku_list, top_n=50): word_counter = Counter() for item in danmaku_list: content = clean_text(item["content"]) if len(content) < 2: continue words = jieba.lcut(content) for w in words: if w not in stopwords and len(w) > 1: word_counter[w] += 1 return word_counter.most_common(top_n)这段代码里,clean_text直接删掉了英文和数字,适合纯中文弹幕场景。如果视频里有大量英文弹幕(比如外语视频),就不能用这个正则,得改成保留英文单词的处理方式。replace("哈哈哈", "")这个操作看起来很粗暴,但对词频统计很有效,因为「哈哈」和「哈哈哈」会被jieba分成不同词,统一去掉后不会污染高频词列表。
3.2 jieba分词参数:用户词典和停用词不能少
弹幕里大量出现B站专属词,比如「爷青回」「双厨狂喜」「梦幻联动」,这些词默认词典里没有,jieba会硬切成「爷青」「回」「双厨」「狂喜」。应对办法是加载自定义词典,把B站高频弹幕词加进去,让jieba按整体词切分。
自定义词典格式很简单:每行一个词,可以带词频和词性,也可以不带。比如爷青回 10 n表示词频设置为10,词性为名词。词频数字越大,该词被整体切出的概率越高。建议把从弹幕统计出来的高频未登录词手动加入词典,效果立竿见影。
jieba.load_userdict("bilibili_dict.txt") with open("bilibili_dict.txt", "w", encoding="utf-8") as f: f.write("爷青回 10 n\n") f.write("梦幻联动 5 n\n") f.write("空降成功 5 n\n") # 验证分词效果 print(jieba.lcut("爷青回,梦幻联动!"))load_userdict在程序启动时调用一次即可。这里有个细节:自定义词典中的词不一定非要符合中文语法,B站弹幕本身就有大量缩写和变体。比如「awsl」虽然不在中文词表里,但如果保留英文,就需要在自定义词典里加awsl 10 n。实际项目中我会把清洗规则设计成可配置的,英文场景切换正则即可。
3.3 词云生成:字体、遮罩和背景色的坑
词云是弹幕分析最常见的可视化图。WordCloud的中文显示问题是最大一个坑:默认字体不支持中文,出来全是方框。解决方法是加载中文字体路径,Windows下可以用C:\Windows\Fonts\msyh.ttc,Linux下用/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc。
词云的数据输入有两种方式:一种是传generate_from_frequencies直接吃词频字典,一种是传文本让WordCloud内部自己分词。后者方便但没法用自定义停用词,推荐前者。
from wordcloud import WordCloud import matplotlib.pyplot as plt word_freq_dict = dict(word_freq(danmaku_list, top_n=200)) wc = WordCloud( font_path="C:/Windows/Fonts/msyh.ttc", width=1600, height=900, max_words=200, background_color="white", colormap="viridis", max_font_size=120 ).generate_from_frequencies(word_freq_dict) plt.figure(figsize=(12, 7)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.show()max_words控制显示词数,max_font_size控制最大字号,colormap影响颜色。如果词云太稀疏,把max_words调小或调整min_font_size;如果词云糊成一片,降低max_font_size。不要用background_color="black"配深色词,视觉上会很难看。
注意:
generate_from_frequencies要求字典的key是词、value是词频。如果value是0,WordCloud会直接抛错,所以清洗词频时要把词频为0的词过滤掉。
4. 弹幕情感分析:模型选型与B站语境适配
4.1 情感分析不是只有「正负」,多模态和中文语境都要考虑
通用情感分析模型在B站弹幕上表现不佳,因为弹幕大量使用反讽、玩梗、网络黑话。比如「就这?」表面是疑问,实际是负面评价;「泪目」在B站语境里是感动,但在普通情感词典里可能被归为悲伤。常见做法是选择针对中文社交媒体语料训练的模型,比如SnowNLP、BosonNLP,或使用预训练的BERT情感分类模型。
SnowNLP是轻量级方案,适合入门,但它的训练语料是电商评论,对B站弹幕的准确率一般。如果要更准,可以用基于预训练模型的情感分析接口,比如transformers库加载uer/roberta-base-finetuned-jd-binary-chinese,这个模型虽然也是电商语料,但泛化能力比SnowNLP好。多模态情感分析这个方向强调的是除了文本,还结合音频和视觉,弹幕分析里基本用不上,至少在第一版不用考虑。
from snownlp import SnowNLP def sentiment_analyze(danmaku_list): pos, neg, neutral = 0, 0, 0 scores = [] for item in danmaku_list: content = item["content"] if len(content) < 2: continue s = SnowNLP(content) score = s.sentiments scores.append(score) if score > 0.6: pos += 1 elif score < 0.4: neg += 1 else: neutral += 1 total = pos + neg + neutral return { "pos_ratio": pos / total, "neg_ratio": neg / total, "neutral_ratio": neutral / total, "avg_score": sum(scores) / len(scores) }SnowNLP.sentiments返回0到1之间的分值,越接近1越正面。注意阈值0.6和0.4是按经验定的,不同视频可以调整。如果整个视频的弹幕情感平均分接近0.5,说明弹幕大多是中性或混杂的。
4.2 SnowNLP结果太粗糙时,用预训练BERT做替代
当视频弹幕数量超过10万条,SnowNLP的速度会成为瓶颈,同时精度也撑不住。这时可以用transformers加载BERT情感分类模型,但是要注意显存和推理速度。纯CPU跑BERT每千条弹幕需要几十秒,批量抓取场景不现实,建议先跑SnowNLP做粗筛,再对情感分模糊的样本(0.4到0.6之间)用BERT精判。
from transformers import pipeline classifier = pipeline("sentiment-analysis", model="uer/roberta-base-finetuned-jd-binary-chinese") def bert_sentiment(text): result = classifier(text, truncation=True, max_length=128) label = result[0]["label"] score = result[0]["score"] if label == "negative": return 1 - score return scorepipeline会自动做分词和编码,truncation=True确保长弹幕被截断到128个token。这里返回的score是模型置信度,注意BERT模型的输出标签是positive和negative,和SnowNLP的数值口径不同,计算正负比例时要统一映射。实际项目中我会把两种模型都跑一遍,对比差异,然后把差异大的弹幕单独抽出来人工检查。
4.3 情感分析的衍生维度:分P情感对比和弹幕时间情感曲线
单个视频的总体情感比例不够直观,更有价值的是情感随播放时间的变化。比如一段游戏视频的P1部分情感分高,P2突然降低,说明P2内容出了问题。具体做法是把弹幕按item["time"]分桶,每10秒一个桶,然后对每个桶内的弹幕情感分求均值。
def sentiment_by_time(danmaku_list, bucket=10): max_time = max(item["time"] for item in danmaku_list) buckets = {} for item in danmaku_list: idx = int(item["time"] // bucket) * bucket if idx not in buckets: buckets[idx] = [] buckets[idx].append(SnowNLP(item["content"]).sentiments) result = [] for t in sorted(buckets.keys()): scores = buckets[t] result.append({ "time": f"{t}s-{t+bucket}s", "avg_sentiment": sum(scores) / len(scores), "count": len(scores) }) return result这里item["time"] // bucket把弹幕时间映射到对应的10秒区间。返回结果里既有情感均值又有弹幕数量,可以直接画成双轴折线图。注意每个区间弹幕数量差距很大,弹幕少的区间情感均值波动也大,画图时要加数量作为参考。
5. 衍生指标构建:弹幕密度、参与用户量、高能时刻与可视化
5.1 弹幕密度曲线和高能时刻识别
弹幕密度是最基础的衍生指标,做法是按时间窗口统计弹幕数量,然后用滑动平均平滑。高能时刻是弹幕密度突然飙升的时间点,通常对应视频里「名场面」或「反转」。识别高能时刻的常见做法是把弹幕密度排序列个阈值,超过平均值加两倍标准差的点标记为高能点。
import numpy as np def danmaku_density(danmaku_list, window=20): max_time = int(max(item["time"] for item in danmaku_list)) density = [] for start in range(0, max_time, window): end = start + window count = sum(1 for item in danmaku_list if start <= item["time"] < end) density.append({"window_start": start, "count": count}) return density def find_high_energy_points(density_list): counts = [d["count"] for d in density_list] mean = np.mean(counts) std = np.std(counts) threshold = mean + 2 * std return [d for d in density_list if d["count"] > threshold]这个实现有个性能问题:sum内部遍历了所有弹幕,时间复杂度是O(n*m)。弹幕量大时建议先用numpy的histogram函数统计,代码会快很多。find_high_energy_points里的2 * std是可调参数,视频节奏紧凑时(比如鬼畜视频)阈值要放大到2.5,否则每个高潮段都是高能点。
5.2 参与用户量、弹幕增速与内容留存率
参与用户量用user_hash去重统计即可,但要注意同一用户hash在不同cid下是否为同一人。B站弹幕用户hash是和视频cid绑定的,不同视频之间不通用,所以只能在单视频内统计参与人数。弹幕增速是按小时聚合发送时间,看视频发布后多久弹幕量达到峰值。内容留存率这个指标比较冷门,做法是把弹幕按视频时长切段,统计后50%时长的弹幕占比,占比高说明视频越往后越有吸引力。
def engagement_metrics(danmaku_list, video_duration): user_count = len(set(item["user_hash"] for item in danmaku_list)) avg_per_user = len(danmaku_list) / user_count if user_count > 0 else 0 mid_time = video_duration / 2 later_count = sum(1 for item in danmaku_list if item["time"] > mid_time) retention = later_count / len(danmaku_list) if danmaku_list else 0 return { "unique_users": user_count, "avg_danmaku_per_user": avg_per_user, "retention_rate": retention }unique_users只是弹幕参与人数,不等于视频真实观看人数,但能反映弹幕互动深度。retention_rate大于0.5说明弹幕用户越往后越活跃,小于0.3说明前期内容充斥大量弹幕,后期大量用户中途离开。这个指标做视频对比时很好用。
5.3 可视化大屏适配:ECharts和pyecharts的搭配
可视化部分用pyecharts最省事,它封装了ECharts的常用图表。弹幕密度曲线、情感时间曲线、词云、情感比例饼图、指标卡片可以拼成一张大屏。注意大屏适配的核心是尺寸,而不是组件数量,用Grid布局设置百分比宽度,比硬编码像素值靠谱。
from pyecharts.charts import Line, Bar, Grid from pyecharts import options as opts line_energy = ( Line() .add_xaxis([str(d["window_start"]) for d in density_list]) .add_yaxis("弹幕密度", [d["count"] for d in density_list], is_smooth=True, label_opts=opts.LabelOpts(is_show=False)) .set_global_opts(title_opts=opts.TitleOpts(title="弹幕密度曲线")) ) grid = Grid() grid.add(line_energy, grid_opts=opts.GridOpts(pos_left="15%", pos_right="15%")) grid.render("danmaku_density.html")Grid里同时放Line和Bar时,注意两个图表要共享x轴数据。这里没有设置xaxis数据类型,ECharts会把字符串当类目轴处理。如果弹幕时间跨度长,建议把x轴类型设为value,传数值而不是字符串。
6. 用弹幕指标反推视频内容质量:一个可落地的验证技巧
拿「弹幕情感低谷点」和「高能时刻」做交叉验证,能定位到视频的具体问题点。做法是分别找出情感均值最低的10秒窗口和高能密度点,然后去视频对应时间看画面内容。如果高能点的情感分反而很低,说明弹幕是在刷负面梗或吐槽画面;如果情感低谷不在任何高能点附近,说明观众在某个普通画面大量表达负面情绪,可能是内容节奏或剪辑出了问题。
lowest_sentiment = min(sent_time_data, key=lambda x: x["avg_sentiment"]) high_energy = find_high_energy_points(density_list) for he in high_energy: if abs(he["window_start"] - lowest_sentiment["time"]) < 30: print(f"高能点 {he['window_start']}s 与情感低谷 {lowest_sentiment['time']} 重叠")这里把窗口差30秒以内视为重叠。实际使用中,这个阈值要配合视频内容标记一起看。比如一个60秒的短视频,30秒窗口內几乎全部重叠,所以短视频要改成10秒阈值。视频弹幕量少于1000条时,情感低谷随机性很大,不建议做交叉分析。弹幕量上来了,这个验证方法才有效。
本文还有配套的精品资源,点击获取