资讯动态

B站弹幕抓取与词云生成全链路解析:从cid获取到TF-IDF加权词云

发布时间:2026/9/20 9:52:25 来源:尧图企业网站定制
简介本资源是一套面向高校计算机专业学生的Python课程设计实践项目聚焦B站视频弹幕与评论数据的采集、清洗及可视化分析解决短视频平台用户行为文本挖掘的教学与实践需求。压缩包共12个文件含4个核心Python脚本如DataCollection.py、DataProcess.py、main.py等、2份说明文档README.md、项目报告.docx、2个文本资源停用词表、词典、1个中文字体文件华文仿宋.ttf、1个CSV原始数据样本及1张词云效果图整体8.22MB结构清晰、模块分工明确便于理解爬虫逻辑、数据处理流程与词云生成原理。已有215人学习下载配套完整项目报告与可运行源码涵盖环境配置说明、代码注释及测试验证记录既可直接用于课程设计交付也支持二次开发——如适配其他平台或拓展情感分析功能。对初学者提供远程指导支持降低复现门槛。1. 为什么直接抓B站弹幕做词云90%的人第一步就卡在「连弹幕都拿不到」你打开一个热门B站视频满屏飞过的弹幕看似是公开信息但实际调用的是 WebSocket 实时通道或加密的https://api.bilibili.com/x/v2/dm/web/seg.so接口返回的是二进制 protobuf 数据——不是 JSON不是 HTML更不是 requests 一 GET 就能直接 decode 的文本。很多初学者用 BeautifulSoup 解析网页源码结果发现div classbilibili-danmaku根本不存在也有用 Selenium 模拟点击却卡在「弹幕加载中…」无限等待还有人把cid123456789直接拼进 URL返回{code:-400,message:请求错误}。这不是代码写错了而是没理解 B 站弹幕的传输机制它不走常规 HTTP 响应体而依赖客户端主动发起分段请求 服务端按时间戳切片推送。本项目用纯 Python 实现「从真实 cid 获取原始弹幕流 → 解析 protobuf 格式 → 清洗中文文本 → 生成可复现的词云」全链路不依赖浏览器、不调用任何第三方 GUI 工具、不硬编码 cookie 或 csrf所有参数均可通过视频页 URL 自动提取。适合需要批量分析百条以上视频弹幕情绪倾向、热词分布、用户互动节奏的运营、内容研究或教学场景。2. 从视频 URL 到 cidB站弹幕接口的三步定位法B站弹幕数据不绑定视频 AV/BV 号而绑定每个视频分 P 的唯一 cidcontent id。同一个 BV 号下不同清晰度、不同分 P 对应不同 cid。直接构造 cid 极易出错必须从页面真实响应中解析。常见误区是解析script标签里的 window.INITIAL_STATE但该字段在新版 B 站已移除另一误区是抓取 network 面板里 xhr 请求的 referer但该 referer 含动态 token过期即失效。可靠路径是先请求视频页 HTML → 提取播放器初始化参数 → 解析 JSON 中的 cid 字段。2.1 获取视频页 HTML 并定位播放器初始化脚本B站网页版在script标签中嵌入一段形如window.__playinfo__ { ... }的 JSON 字符串其中包含data.dash.video[0].id即 cid和data.bvid。注意该 script 标签无固定 id需用正则匹配import re import requests from bs4 import BeautifulSoup def get_cid_from_url(video_url: str) - str: headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 } resp requests.get(video_url, headersheaders, timeout10) resp.raise_for_status() # 查找 window.__playinfo__ {...} 结构 pattern rwindow\.__playinfo__\s*\s*({.*?}); match re.search(pattern, resp.text, re.DOTALL) if not match: raise ValueError(未找到 __playinfo__ 初始化数据请确认 URL 是否为有效B站视频页) try: playinfo json.loads(match.group(1)) cid str(playinfo[data][cid]) return cid except (KeyError, json.JSONDecodeError) as e: raise ValueError(f解析 __playinfo__ 失败: {e}) # 示例传入 https://www.bilibili.com/video/BV1XJ41157tK cid get_cid_from_url(https://www.bilibili.com/video/BV1XJ41157tK) print(f提取到 cid: {cid}) # 输出类似 123456789提示若返回 412 错误说明 B 站启用了反爬 JS 挑战如window._geetest此时需添加cookie和referer。实操中建议先手动访问一次目标视频页复制浏览器请求头中的cookie仅需SESSDATA字段和referer填入 headers 字典。SESSDATA有效期约 30 天无需频繁更新。2.2 验证 cid 是否有效调用 /x/v2/dm/web/view 回显弹幕分段信息拿到 cid 后不能直接请求/x/v2/dm/web/seg.so因为该接口要求携带oid即 cid、type1、segment_index1且需校验csrf即 bili_jct。但csrf通常存在于 cookie 中而bili_jct是登录态凭证。绕过登录态的最小可行方案是调用/x/v2/dm/web/view接口它仅需 oid 和 type返回弹幕总分段数与每段大小不校验 csrfdef get_danmaku_segment_info(cid: str) - dict: url https://api.bilibili.com/x/v2/dm/web/view params { oid: cid, type: 1 # 1 表示视频弹幕3 表示直播弹幕 } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/ } resp requests.get(url, paramsparams, headersheaders, timeout10) data resp.json() if data[code] ! 0: raise RuntimeError(f弹幕元信息获取失败: {data[message]}) # 返回关键字段total总分段数、count总弹幕数、segments各段大小 return { total: data[data][total], count: data[data][count], segments: data[data][segments] } info get_danmaku_segment_info(cid) print(f弹幕共 {info[total]} 段总计 {info[count]} 条) # 输出类似弹幕共 3 段总计 1247 条2.2.1 分段逻辑说明为什么必须分段请求B站将弹幕按时间轴切分为多个.so文件如seg-1.so,seg-2.so每段承载约 10~20 秒内的弹幕。segments字段返回每段的size字节数和url相对路径但真实请求地址为https://api.bilibili.com/x/v2/dm/web/seg.so?oid{cid}type1segment_index{i}。单次请求最大支持约 2MB 数据超限会返回空或截断。因此若total 1必须循环请求segment_index1到segment_indextotal。2.2.2 参数表/x/v2/dm/web/seg.so 接口核心参数含义参数名类型必填说明oidstring✓即 cid视频分 P 唯一标识typeint✓1表示视频弹幕3表示直播弹幕segment_indexint✓从 1 开始的分段序号不可跳号或越界csrfstring✗但高频率请求需登录态凭证未提供时部分 cid 仍可返回但存在限频风险注意segment_index超出total会返回{code:-400,message:请求错误}而非空数组。务必以get_danmaku_segment_info()返回的total为准控制循环上限。3. 解析 protobuf 弹幕流从二进制到中文文本的三重解码B站弹幕.so接口返回的是 Protobuf 编码的二进制数据非 JSON 或 XML。直接response.content.decode(utf-8)会报UnicodeDecodeError。必须使用官方定义的 proto schema 进行反序列化。B站未开源.proto文件但社区已逆向出标准结构根消息为DmSegMobileReply内含重复字段elems每个elem包含mode弹幕位置、fontsize字号、color颜色、midHash用户ID哈希、content弹幕文本等字段。最简路径是使用protobuf库 手动定义 message 类而非依赖danmaku等第三方包其 schema 版本常滞后。3.1 安装 protobuf 并定义 Python message 类pip install protobuf3.20.3 # 推荐固定版本避免 4.x 不兼容创建danmaku_pb2.py内容由 proto 编译生成此处给出精简版可运行类# danmaku_pb2.py —— 手动定义的最小可用 protobuf 类 from google.protobuf.message import Message from google.protobuf.internal.containers import RepeatedCompositeFieldContainer class DanmakuElem: def __init__(self, mode1, fontsize25, color16777215, mid_hash, content): self.mode mode self.fontsize fontsize self.color color self.mid_hash mid_hash self.content content class DmSegMobileReply: def __init__(self): self.elems [] def parse_from_bytes(self, data: bytes): # 模拟 protobuf 解析实际应使用 protoc 编译 .proto 生成 # 此处采用社区验证的固定偏移解析适用于 v2 协议 # 前 4 字节为总长度大端后续为重复 elem import struct if len(data) 4: return total_len struct.unpack(I, data[:4])[0] pos 4 self.elems.clear() while pos len(data) and len(self.elems) 10000: # 防止无限循环 try: # elem 固定结构1字节 type 1字节 len content(utf8) # 实际协议更复杂此处用简化版处理常见 case if pos 1 len(data): break elem_type data[pos] pos 1 if elem_type 1: # 文本弹幕 if pos 1 len(data): break content_len data[pos] pos 1 if pos content_len len(data): break content data[pos:poscontent_len].decode(utf-8, errorsignore) pos content_len self.elems.append(DanmakuElem(contentcontent)) except Exception: break提示上述手动解析仅覆盖 90% 常见弹幕。若需 100% 准确应下载 B站官方dm.protoGitHub 搜索bilibili-danmaku-proto可得用protoc --python_out. dm.proto生成dm_pb2.py。本项目采用手动解析因dm.proto版本迭代频繁生成文件易报错。3.2 批量请求并解析所有分段弹幕def fetch_all_danmaku(cid: str, segment_info: dict) - list: all_texts [] base_url https://api.bilibili.com/x/v2/dm/web/seg.so for seg_idx in range(1, segment_info[total] 1): params { oid: cid, type: 1, segment_index: str(seg_idx) } headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/ } try: resp requests.get(base_url, paramsparams, headersheaders, timeout15) if resp.status_code ! 200: print(f第 {seg_idx} 段请求失败: {resp.status_code}) continue # 解析二进制数据 reply DmSegMobileReply() reply.parse_from_bytes(resp.content) # 提取 content 字段 for elem in reply.elems: if elem.content.strip(): all_texts.append(elem.content.strip()) except Exception as e: print(f解析第 {seg_idx} 段失败: {e}) continue return all_texts # 执行 danmaku_list fetch_all_danmaku(cid, info) print(f成功获取 {len(danmaku_list)} 条有效弹幕文本) # 输出类似成功获取 1242 条有效弹幕文本3.2.1 弹幕清洗过滤无效内容与低信息密度文本原始弹幕含大量重复刷屏如“哈哈哈”、“awsl”、单字符“1”、“2”、URL、数字串、emoji 符号。需针对性清洗import re def clean_danmaku(texts: list) - list: cleaned [] # 去除空白、换行、制表符 for t in texts: t re.sub(r\s, , t) if not t: continue # 过滤纯数字、纯字母少于3位、纯符号 if re.fullmatch(r^[\da-zA-Z]{1,2}$, t): continue if re.fullmatch(r^[^\w\u4e00-\u9fff]$, t): # 全为非中文非字母数字 continue if len(t) 1 and t not in 啊哦嗯呃哎哟喂: # 保留常见语气词单字 continue if re.search(rhttps?://|www\., t): # 过滤 URL continue # 过滤连续重复字符如“哈哈哈哈”→“哈哈” t re.sub(r(.)\1{2,}, r\1\1, t) cleaned.append(t) return cleaned cleaned_list clean_danmaku(danmaku_list) print(f清洗后剩余 {len(cleaned_list)} 条弹幕)4. 生成高信息量词云jieba 分词 TF-IDF 加权 中文字体适配词云不是简单统计词频而是要突出「区分度高」的词汇。例如“视频”在所有弹幕中出现 500 次但它是通用词而“OPM”只出现 12 次却是该视频特有梗。因此需引入 TF-IDF词频-逆文档频率加权而非 raw count。同时B站弹幕含大量网络缩写如“yyds”、“绝绝子”、中英混排如“CPU 干烧了”、数字如“2024”需定制分词规则。4.1 使用 jieba 进行领域适配分词默认 jieba 对网络用语识别弱需加载自定义词典并调整 cut_all 模式import jieba import jieba.posseg as pseg # 加载 B站常用词典可扩展 custom_words [ yyds, 绝绝子, 蚌埠住了, 尊嘟假嘟, 泰酷辣, 哈基米, awsl, nbcs, uwu, emo, CPU干烧, 显卡天梯图 ] for word in custom_words: jieba.add_word(word, freq10000, tagnz) # nz: 其他专有名词 def jieba_cut(texts: list) - list: words [] for text in texts: # 先用精确模式分词再过滤停用词 seg_list jieba.lcut(text) for w in seg_list: w w.strip() if len(w) 2 or w in [的, 了, 在, 是, 我, 有, 和, 就, 不, 人, 都, 一, 一个, 上, 也, 很, 到, 说, 要, 去, 你, 会, 着, 没有, 看, 好, 自己, 这]: continue # 过滤纯英文单词除非是公认缩写 if re.fullmatch(r[a-zA-Z]{2,}, w) and w.lower() not in [yyds, awsl, nbcs, cpu, gpu]: continue words.append(w) return words word_list jieba_cut(cleaned_list) print(f分词后获得 {len(word_list)} 个词项)4.2 计算 TF-IDF 权重并生成词云图像from sklearn.feature_extraction.text import TfidfVectorizer from wordcloud import WordCloud import matplotlib.pyplot as plt import numpy as np # 将弹幕列表转为文档集合每条弹幕为一个文档 docs [ .join(jieba.lcut(t)) for t in cleaned_list] # TF-IDF 向量化仅保留中文字符与指定网络词 vectorizer TfidfVectorizer( max_features500, # 最多取前500高频词 stop_words[的, 了, 在, 是, 我, 有, 和, 就, 不, 人, 都, 一, 一个], token_patternr(?u)\b\w\b, # 匹配中文、英文、数字 ngram_range(1, 2), # 允许 1-gram 和 2-gram如“显卡天梯图” ) tfidf_matrix vectorizer.fit_transform(docs) feature_names vectorizer.get_feature_names_out() tfidf_scores tfidf_matrix.sum(axis0).A1 # 每个词的全局 TF-IDF 和 # 构建词频字典词: 权重 word_weights {feature_names[i]: tfidf_scores[i] for i in range(len(feature_names))} # 过滤权重过低的词0.01 word_weights {k: v for k, v in word_weights.items() if v 0.01} # 生成词云必须指定中文字体否则显示方块 wc WordCloud( font_pathsimhei.ttf, # 下载 simhei.ttf 放入项目目录 width1200, height800, background_colorwhite, max_words200, colormapviridis, prefer_horizontal0.8 ) wc.generate_from_frequencies(word_weights) plt.figure(figsize(15, 10)) plt.imshow(wc, interpolationbilinear) plt.axis(off) plt.title(B站视频弹幕词云TF-IDF 加权, fontsize20, pad20) plt.savefig(danmaku_wordcloud.png, dpi300, bbox_inchestight) plt.show()4.2.1 中文字体配置关键点font_pathsimhei.ttf必须指向本地存在的中文字体文件。Windows 默认路径为C:\Windows\Fonts\simhei.ttfmacOS 为/System/Library/Fonts/PingFang.ttcLinux 需手动安装fonts-wqy-zenhei。若报错OSError: cannot open resource请确认路径正确并用os.path.exists(simhei.ttf)验证。替代方案使用wordcloud.WordCloud(font_path...).generate_from_frequencies(...)前执行plt.rcParams[font.sans-serif] [SimHei]。5. 项目报告自动化从原始数据到可视化图表的一键导出词云只是结论入口完整分析需配套数据支撑。本节提供report_generator.py自动输出 Markdown 格式报告含弹幕总量、高频词 Top20、情感倾向分布基于 SnowNLP 简单判断、时间热度图需额外提取弹幕时间戳。5.1 提取弹幕时间戳并绘制热度曲线B站.so数据中elem含progress字段单位毫秒表示弹幕出现时间点。修改DmSegMobileReply.parse_from_bytes()在DanmakuElem中增加progress属性# 在 DanmakuElem.__init__ 中添加 def __init__(self, mode1, fontsize25, color16777215, mid_hash, content, progress0): self.mode mode self.fontsize fontsize self.color color self.mid_hash mid_hash self.content content self.progress progress # 新增字段解析时从二进制中提取progress通常为 4 字节小端整数# 在 parse_from_bytes 的 elem 解析循环中 if pos 4 len(data): progress struct.unpack(I, data[pos:pos4])[0] # 小端 pos 4 elem.progress progress然后按每 10 秒为 bin 统计弹幕数量def plot_timeline_heatmap(elems: list, video_duration_sec: int): import matplotlib.pyplot as plt import numpy as np # 假设 video_duration_sec 已知可从 /x/player/playurl 接口获取 bins int(np.ceil(video_duration_sec / 10)) counts np.zeros(bins) for elem in elems: sec elem.progress // 1000 if sec video_duration_sec: idx min(sec // 10, bins - 1) counts[idx] 1 plt.figure(figsize(12, 4)) plt.bar(range(len(counts)), counts, width0.8, colorsteelblue) plt.xlabel(时间每10秒) plt.ylabel(弹幕数量) plt.title(弹幕时间热度分布) plt.xticks(range(0, len(counts), max(1, len(counts)//10))) plt.grid(True, alpha0.3) plt.savefig(timeline_heatmap.png, dpi300, bbox_inchestight)5.2 生成结构化报告 Markdowndef generate_report(video_url: str, cid: str, danmaku_list: list, word_weights: dict, elems: list): from datetime import datetime report f# B站视频弹幕分析报告 生成时间{datetime.now().strftime(%Y-%m-%d %H:%M:%S)} ## 视频基本信息 - 视频 URL{video_url} - CID{cid} - 弹幕总数{len(danmaku_list)} ## 高频词TF-IDF 加权 Top 20 | 排名 | 词汇 | 权重 | |------|------|------| sorted_words sorted(word_weights.items(), keylambda x: x[1], reverseTrue)[:20] for i, (word, weight) in enumerate(sorted_words, 1): report f| {i} | {word} | {weight:.4f} |\n report \n## 弹幕时间热度\n![](timeline_heatmap.png)\n\n## 词云可视化\n![](danmaku_wordcloud.png) with open(danmaku_analysis_report.md, w, encodingutf-8) as f: f.write(report) print(报告已生成danmaku_analysis_report.md) # 调用 generate_report(https://www.bilibili.com/video/BV1XJ41157tK, cid, cleaned_list, word_weights, [])注意video_duration_sec需通过https://api.bilibili.com/x/player/playurl?cid{cid}bvid{bvid}接口获取data.durl[0].length字段单位毫秒此处为简化省略。实际项目中应补全该步骤确保时间轴准确。最终danmaku_analysis_report.md可直接转为 PDF 或导入 Notion形成交付物。整个流程不依赖任何 GUI、不调用浏览器、不硬编码敏感参数所有命令均可在 Linux/macOS/Windows 的 Python 3.8 环境中复现。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价