做了几年数据采集,我越来越觉得弹幕是最被低估的分析素材。B站视频弹幕数据不像评论区那样有组织,但它胜在够真实、够即时,用户看到哪一帧就喷哪一句,情绪颗粒度是文本分析里很难得的一类数据。前阵子心血来潮,把一个热门视频的弹幕全量抓下来做了词云分析,整个过程走下来发现坑比想象中多:接口地址变了、中文乱码了、分词把"哈哈哈"算成关键词、词云字体发虚……这篇文章把完整流程复盘一遍,从技术选型、弹幕接口解析,到数据清洗、词云可视化,该给的代码都给,该避的坑也一起列出来。适合刚接触数据爬取的朋友,也适合想用弹幕做用户反馈分析的运营同学参考。
1. 弹幕数据到底能分析出什么:先想清楚再动手
很多教程上来就教代码,结果读者爬完数据不知道下一步该干嘛。所以在动手之前,先把这次项目的目标和边界说清楚。
1.1 弹幕是用户情绪的实时快照
弹幕和评论最大的区别是时间锚点。用户发弹幕时,必定附着在视频的某个时间点上,这意味着你可以分析出视频哪个片段最让人激动、哪个片段被吐槽最多、哪个片段让人反复"前方高能"。词云分析只是第一步,它把文本频率转化为视觉上最容易感知的关键词分布,帮你回答"观众到底在聊什么"。如果要再深一层,还可以结合时间轴做分段词频统计,技术路线是通用的。
1.2 这次项目的边界:公开视频、公开弹幕、学习用途
本次采集对象只针对开放平台的公开视频弹幕,数据本身属于公开内容,采集频率必须控制在对目标站点无压力的水平,且只用于个人学习和分析,不商用、不做数据倒卖。爬虫技术本身是中性的,但用不好容易涉及法律和合规问题,这一点后面专门有一节讲。先说技术:从B站获取弹幕数据,核心并不在"爬"字上,而在于找到正确的接口和解析格式。
弹幕数据字段至少有三种:发送时间、弹幕内容、发送者UID(脱敏后可看)。我们在分析阶段主要使用内容是文本,所以清洗阶段要重点处理内容字段。
2. 技术选型:绕过页面解析,直取官方弹幕接口
如果你一开始想的是"打开B站视频页,用Selenium控制浏览器滚动,从HTML里抠弹幕",那方向就偏了。弹幕是动态加载内容,页面解析不仅效率低,而且Selenium每次启动一个浏览器实例,内存开销大,触发风控的风险也高。正确做法是直接找B站的弹幕数据接口,一条请求拿全整包数据。
2.1 为什么选择官方公开接口
B站每个视频都有唯一的BV号,通过视频详情接口可以拿到一个叫 cid 的数值,这个数值对应视频分P的ID。弹幕接口以 cid 为入参,返回XML或分段二进制数据。这个链路是官方公开的,Web前端自己也在用,所以只要按规范请求,不需要模拟点击,不需要OCR,更不需要什么逆向破解。相比页面解析,这种方式稳定、高效、代码量少。
2.2 核心依赖清单与安装注意事项
语言使用Python 3.9+,建议直接上新一点的版本。依赖列表非常短:
| 库名 | 用途 | 备注 |
|---|---|---|
| requests | 发送HTTP请求 | 替代urllib,处理Header更方便 |
| jieba | 中文分词 | 词云分析前必须做分词 |
| wordcloud | 生成词云 | 注意中文字体路径 |
| matplotlib | 绘图与图像显示 | wordcloud底层依赖 |
| pandas | 数据存储与清洗 | 可选,但用了会舒服很多 |
| lxml | 解析弹幕XML | 也可以只用正则 |
安装命令一行搞定:
pip install requests jieba wordcloud matplotlib pandas lxml如果安装wordcloud时出现Microsoft Visual C++ 14.0报错,首选方案是直接下载对应的.whl文件安装,不要硬编译源码。
2.3 从BV号到cid:唯一绕不过去的关卡
B站视频的播放页地址通常长这样:
https://www.bilibili.com/video/BV1xx411c7mDBV号就是BV1xx411c7mD这一段。我们要的cid需要通过视频详情接口获取:
https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD这个接口返回JSON,里面包含页面标题、UP主信息、分P列表,每个分P都有对应的cid。需要注意的是:请求这个接口时最好带上Referer头,否则部分请求会返回风险提示。原因很简单,B站API在判断场景时会校验来源页面。下面代码演示完整获取过程。
3. 手把手实现弹幕爬取:关键代码与执行链路
这一段从请求头设置、获取cid、请求弹幕、解析数据一直讲到落盘。我不喜欢放一堆零碎片段,所以直接给出一段可运行的核心代码,然后逐段解释。
3.1 第一步:设置请求头和会话
import requests import json import re HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36", "Referer": "https://www.bilibili.com/", "Accept": "application/json, text/plain, */*", } session = requests.Session() session.headers.update(HEADERS)不要小看这个请求头。很多新手爬B站只带User-Agent也能通,但遇到反爬加严的账号或IP段,没有Referer的请求很容易被识别为异常。最稳妥的做法就是从上到下都保持浏览器常见字段。
3.2 第二步:获取cid并构造弹幕地址
bvid = "BV1xx411c7mD" view_url = "https://api.bilibili.com/x/web-interface/view" resp = session.get(view_url, params={"bvid": bvid}) data = resp.json() if data["code"] != 0: raise RuntimeError(f"接口返回错误: {data}") pages = data["data"]["pages"] cid = pages[0]["cid"] print(f"标题: {data['data']['title']}") print(f"分P数量: {len(pages)},第一分P cid: {cid}")注意data["code"]必须判断是否等于0,这是B站接口的统一约定。很多教程拿到数据不判断状态,直接解析,遇到“风险验证”或“参数错误”时只能一脸懵。分P视频必须逐P获取cid,因为弹幕是跟着分P走的,不同分P的弹幕池互相独立。
3.3 第三步:两种弹幕接口格式的选择
B站弹幕接口常见有两种方案,我都测试过:
方案一是老版XML全文接口:
https://comment.bilibili.com/{cid}.xml这个地址直接返回XML,里面所有弹幕一锅端,解析简单,适合学习。缺点是弹幕数量比较大时,接口实际只返回部分数据,且没有分段能力。
方案二是新版分段接口:
https://api.bilibili.com/x/v2/dm/web/seg.so?type=1&oid={cid}&segment_index={index}这个接口把弹幕按时间段切成了多个segment,单段返回protobuf二进制。好处是能获取更完整的历史弹幕,坏处是需要处理protobuf格式,复杂度瞬间上来了。
对词云分析来说,方案一完全够用,绝大多数普通视频的弹幕量级不会大到XML接口装不下。但如果你想做长视频的完整弹幕档案,或者想拿趋势数据,最好还是用分段接口。为了兼顾可读性,下面我用方案一,因为它能让读者把注意力集中在数据处理上,而不是被protobuf劝退。
3.4 第四步:解析XML弹幕并转为结构化数据
XML弹幕内容长这样:
<d p="1.123,1,25,1678,0,0,3b4f2a1b,1234567">前方高能</d><d>标签里的p属性包含逗号分隔的元信息,常见字段依次是:弹幕出现时间(秒)、弹幕类型、字体大小、颜色、发送时间戳、弹幕池、用户Hash、弹幕ID。标签内的文本就是弹幕内容。
用lxml解析最省事:
from lxml import etree xml_text = session.get(f"https://comment.bilibili.com/{cid}.xml").content.decode("utf-8", errors="ignore") root = etree.fromstring(xml_text.encode("utf-8")) danmaku_list = [] for d in root.xpath("//d"): p_attr = d.get("p", "").split(",") content = d.text if not content: continue danmaku_list.append({ "time": float(p_attr[0]) if p_attr else None, "content": content, "timestamp": int(p_attr[4]) if len(p_attr) > 4 else None, }) print(f"共获取 {len(danmaku_list)} 条弹幕")这里有一个特别容易踩的坑:etree.fromstring接收的是字节串,如果你直接传字符串,中文编码不对就会报ValueError: Unicode strings with encoding declaration are not supported。我在第一次写时就是在这里卡了好久。上面的写法先把content转为字节再解析,是从根源上避开这个坑。
3.5 第五步:多P视频合并与数据落盘
如果视频有多个分P,循环遍历每个分P即可:
all_danmaku = [] for p in pages: cid = p["cid"] page_text = session.get(f"https://comment.bilibili.com/{cid}.xml").content root = etree.fromstring(page_text) for d in root.xpath("//d"): p_attr = d.get("p", "").split(",") content = d.text if content: all_danmaku.append({ "page": p["page"], "part": p["part"], "time": float(p_attr[0]) if p_attr else None, "content": content, }) import pandas as pd df = pd.DataFrame(all_danmaku) df.to_csv("danmaku.csv", index=False, encoding="utf-8-sig")utf-8-sig编码写入很关键。如果直接写utf-8,用Excel打开CSV时中文大概率乱码,因为Excel默认按ANSI解析。用utf-8-sig加BOM头,Excel才能正确识别。这是数据分析落地时最常见的国服特供坑。
到这里,弹幕数据本身已经拿到手了,但是几百上千条"哈哈哈哈""救命""666"还不能直接做词云,必须进行清洗。
4. 词云分析的正确打开方式:分词、停用词与可视化调优
词云生成的本质是词频统计,但中文不像英文天然按空格分词。如果你直接统计字符串频率,会出现"前方高能"被切成"前方"和"高能"两个词,或者大量无意义的语气词霸占视觉中心。所以词云好看不好看,七成取决于分词和过滤,三成才是wordcloud参数。
4.1 jieba分词在弹幕场景里的特殊处理
jieba默认分词词典对网络流行语覆盖不足,像"yyds""绝绝子""AWSL"这类词很容易被切碎。解决思路分两步:
先加自定义词典,让jieba优先识别这些梗词:
import jieba custom_words = ["yyds", "绝绝子", "AWSL", "破防了", "泪目", "名场面", "前方高能", "爷青回", "一键三连"] for w in custom_words: jieba.add_word(w, freq=10000)freq参数就是为了让这个词在切分时优先成词,值越大优先级越高,但也不宜设得太大,否则会影响附近正常词的切分。
然后是分词:
import jieba.analyse def segment(texts): words = [] for t in texts: if not isinstance(t, str): continue t = t.strip() if len(t) < 2: continue words.extend(jieba.lcut(t)) return words seg_list = segment(df["content"].tolist())4.2 自定义停用词表与弹幕语气词过滤
弹幕里出现频率最高的往往不是有效主题词,而是"哈哈""哈哈哈哈""啊""哦""666""111"等感叹词和刷屏数字。这些词不清掉,词云出来全是尖叫声。我整理了一份针对弹幕场景的停用词表,放在代码里直接过滤:
stopwords = set() with open("stopwords.txt", "r", encoding="utf-8") as f: for line in f: word = line.strip() if word: stopwords.add(word) # 额外补充弹幕特有停用词 extra_stopwords = {"哈哈", "哈哈哈", "哈哈哈哈", "hhhh", "hhh", "666", "111", "222", "333", "绝了", "救命", "卧槽", "我去", "牛批", "给力", "来了", "来了来了", "啊这", "???", "。。。"} stopwords.update(extra_stopwords)注意这里有个矛盾点:像"卧槽""救命"这类词,虽然可能反映情绪,但作为高频词放进词云,会掩盖真正的内容关键词。所以我倾向于把它们过滤掉。如果你的分析目标是情绪识别,那另当别论,可以保留并做情感极性分类。
过滤完的完整流程:
filtered_words = [w for w in seg_list if w not in stopwords and len(w) > 1 and not w.isdigit() and w.strip()]还有一个很多人忽略的点:弹幕文本里的英文字母大小写。像"yyds"和"YYDS"会被jieba看成两个词,统计出来都很少。所以分词前统一转小写:
df["content"] = df["content"].str.lower()4.3 词云参数调优:字体、背景、形状与中文乱码
中文词云最经典的报错就是:
OSError: cannot open resource这是因为wordcloud默认找英文渲染字体,没法处理中文。解决办法是指定中文字体路径。Windows系统一般用:
C:/Windows/Fonts/simhei.ttf也可以下载思源黑体放到项目目录里。生成词云的完整代码:
from wordcloud import WordCloud import matplotlib.pyplot as plt from collections import Counter counter = Counter(filtered_words) font_path = "C:/Windows/Fonts/simhei.ttf" wc = WordCloud( font_path=font_path, width=1920, height=1080, background_color="black", max_words=200, collocations=False, ) wc.generate_from_frequencies(counter) plt.figure(figsize=(16, 9)) plt.imshow(wc, interpolation="bilinear") plt.axis("off") plt.savefig("danmaku_wordcloud.png", dpi=300, bbox_inches="tight")这里两个参数很关键。一个是collocations=False,如果不关掉,wordcloud会默认把相邻搭配识别成词组,比如"前方""高能"拼成"前方高能"然后再计数,导致重复统计。另一个是interpolation="bilinear",让图片边缘更平滑,尤其在浅色背景上,能明显减少锯齿感。
形状定制也简单,用PIL处理一下蒙版图:
import numpy as np from PIL import Image mask = np.array(Image.open("mask.png").convert("L")) wc = WordCloud(font_path=font_path, mask=mask, background_color="white", collocations=False)使用形状遮罩时注意,mask图片必须是黑底白图案,白色区域才是词云绘制区。很多人用普通彩色图片直接当mask,结果输出一片空白,就是因为没转成灰度且没有处理好黑白关系。
4.4 词云结果怎么看:高频词背后的信息
出一张词云图不是终点。比如我给一个游戏区视频做分析时,最大的词是"联机",其次是"教程""配置""卡顿"。这说明弹幕讨论重心在实操层面,而非剧情或画面。你可以根据词云反推:
- 视频剪辑节奏是否过慢(比如"太慢了""倍速"高频)
- 哪些画面最受欢迎("名场面""回放")
- 用户对UP主的期待是什么("下一期""更新")
这些结论如果配合弹幕时间轴做分段统计,效果会更进一步。比如把视频前10%时间段的弹幕单独拿出来做词云,通常能看出开头的保留率问题;把后半段弹幕拿出来,能看出内容是否烂尾。词云本身只是工具,关键是你想让它替你回答什么问题。
5. 实测中的常见坑:反爬、编码、时间与合规边界
最后这一段是实战中最需要留意的部分。我按踩过的坑排列,由技术到合规,逐个拆开说。
5.1 请求频率与请求头伪装
B站接口并不算严格,但如果你连续用同一个IP高频请求几百个视频,会触发一个叫"请求过于频繁"的提示,接口返回code: -412。这是个风控信号,意味着你的IP段被短期观察了。解决办法不是换IP池,而是从源头控制频率。比如每请求一个视频,time.sleep(1)到2秒,并发控制在2至3个以内。毕竟你是做学习分析,不是做数据工厂,没必要和风控硬刚。
另外,User-Agent最好保持较新的Chrome版本字符串,并且不要每次请求都换一个随机UA,那样反而更容易被识别为爬虫。一个稳定UA,配上固定Referer,表现上更像真实用户。
5.2 XML解析时的中文编码问题
前面提到用lxml解析时,etree.fromstring的编码坑是个高频问题。还有一个常见问题是接口返回的XML内容如果包含&、<这些特殊字符,直接用正则解析容易崩。用etree.XMLParser(resolve_entities=True)可以处理实体符号,但更省事的是不要用正则解析XML。记住一条铁律:XML就用XML解析器,HTML就用HTML解析器,不要试图用正则去硬刚结构化文本。
5.3 弹幕数量与“字少词高频”的天然偏误
弹幕文本普遍短,一条弹幕可能就四五个字,所以词频统计天然偏向短小的语气词和网络梗。即使做了停用词过滤,"绝了""救命"这种词还是容易冒出来。如果某个词你想排除但因为它变体太多排不干净,可以考虑用TF-IDF或TextRank来替代纯词频统计。jieba自带jieba.analyse.textrank,它能根据词之间的共现关系给词加权,效果比单纯Counter适合弹幕场景:
top_kw = jieba.analyse.textrank(" ".join(filtered_words), topK=50, withWeight=True)TextRank出来的关键词往往更有代表性,因为它在多个弹幕中反复以相似语义共现才会获得高权重。对词云来说,你也可以只取前50个关键词生成词云,避免噪音词干扰。
5.4 合规边界:学习用途也要守住底线
弹幕数据虽然公开,但爬取和使用仍需遵循以下底线:
- 只采集公开接口返回的公开弹幕,不碰私信、隐私、付费内容。
- 严格控制请求频率,不制造访问压力,不逆向强加密接口。
- 所有代码和结果仅用于个人学习研究,不商用,不提供批量下载服务,不转售数据。
- 不在博客放其他人能直接复制就去批量抓全站数据的"一键脚本",这既是对平台资源的保护,也是对自己的保护。
我在这里不会提供绕过验证码、模拟登录获取敏感数据的方法,那部分已经超出技术分享的范畴。你能从这篇文章里拿到的,是公开接口的规范用法和完整的分析思路。
最后说一点个人体会
弹幕词云这个项目,技术上不复杂,但把它做顺了,你对HTTP请求、数据解析、文本清洗和可视化的理解会扎实很多。我实际跑下来最大感受是:B站弹幕接口最大的门槛不是反爬,而是“你以为它很难,于是照着错误教程绕远路”。只要找准cid这个钥匙,后面就是流水线操作。
踩过几次坑之后,我现在做这类分析会多备份一步:把清洗前的原始弹幕数据单独存一份。因为词云参数可以反复调,停用词表也会改,如果原始数据被覆盖,所有调优都得重新爬一遍。这个习惯帮我省了不少时间。
如果你后面想继续深入,可以在词云基础上做弹幕情感分析、按时间轴分段热度曲线,甚至把多个视频的弹幕横向对比看UP主选题方向。技术都是一通百通的,先把这条链路跑通,后面自然有更多玩法。