从数据采集到可视化:B站弹幕分析全流程实战教程 📅 发布时间:2026/9/11 16:09:30 👁 浏览次数: 简介一套面向毕业设计、课程设计与项目演示的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, paramsparams, 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 fhttps://comment.bilibili.com/{cid}.xml resp requests.get(xml_url, headers{Referer: https://www.bilibili.com/}) # 弹幕内容提取 danmaku_list re.findall(rd p(.*?)(.*?)/d, resp.text) return danmaku_list if __name__ __main__: bv BV1xx411c7mD result fetch_danmaku(bv) print(result[:3])这段代码里get_cid负责从BV号拿cidfetch_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_workers8) 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_workers8是线程池大小。实际运行中线程池开到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, encodingutf-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_n50): 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, encodingutf-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.ttcLinux下用/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_n200)) wc WordCloud( font_pathC:/Windows/Fonts/msyh.ttc, width1600, height900, max_words200, background_colorwhite, colormapviridis, max_font_size120 ).generate_from_frequencies(word_freq_dict) plt.figure(figsize(12, 7)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.show()max_words控制显示词数max_font_size控制最大字号colormap影响颜色。如果词云太稀疏把max_words调小或调整min_font_size如果词云糊成一片降低max_font_size。不要用background_colorblack配深色词视觉上会很难看。注意generate_from_frequencies要求字典的key是词、value是词频。如果value是0WordCloud会直接抛错所以清洗词频时要把词频为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, modeluer/roberta-base-finetuned-jd-binary-chinese) def bert_sentiment(text): result classifier(text, truncationTrue, max_length128) label result[0][label] score result[0][score] if label negative: return 1 - score return scorepipeline会自动做分词和编码truncationTrue确保长弹幕被截断到128个token。这里返回的score是模型置信度注意BERT模型的输出标签是positive和negative和SnowNLP的数值口径不同计算正负比例时要统一映射。实际项目中我会把两种模型都跑一遍对比差异然后把差异大的弹幕单独抽出来人工检查。4.3 情感分析的衍生维度分P情感对比和弹幕时间情感曲线单个视频的总体情感比例不够直观更有价值的是情感随播放时间的变化。比如一段游戏视频的P1部分情感分高P2突然降低说明P2内容出了问题。具体做法是把弹幕按item[time]分桶每10秒一个桶然后对每个桶内的弹幕情感分求均值。def sentiment_by_time(danmaku_list, bucket10): 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-{tbucket}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, window20): 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_smoothTrue, label_optsopts.LabelOpts(is_showFalse)) .set_global_opts(title_optsopts.TitleOpts(title弹幕密度曲线)) ) grid Grid() grid.add(line_energy, grid_optsopts.GridOpts(pos_left15%, pos_right15%)) grid.render(danmaku_density.html)Grid里同时放Line和Bar时注意两个图表要共享x轴数据。这里没有设置xaxis数据类型ECharts会把字符串当类目轴处理。如果弹幕时间跨度长建议把x轴类型设为value传数值而不是字符串。6. 用弹幕指标反推视频内容质量一个可落地的验证技巧拿「弹幕情感低谷点」和「高能时刻」做交叉验证能定位到视频的具体问题点。做法是分别找出情感均值最低的10秒窗口和高能密度点然后去视频对应时间看画面内容。如果高能点的情感分反而很低说明弹幕是在刷负面梗或吐槽画面如果情感低谷不在任何高能点附近说明观众在某个普通画面大量表达负面情绪可能是内容节奏或剪辑出了问题。lowest_sentiment min(sent_time_data, keylambda 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条时情感低谷随机性很大不建议做交叉分析。弹幕量上来了这个验证方法才有效。本文还有配套的精品资源点击获取