前言
B站(哔哩哔哩)作为国内最大的弹幕视频网站,其弹幕文化一直是平台的核心特色。无论是为了做舆情分析、情感分析,还是仅仅想保存某视频的弹幕留作纪念,掌握B站弹幕的爬取方法都是一项非常实用的技能。
本文将基于真实项目代码,带你深入理解B站弹幕API的调用方式,从获取视频分P信息、抓取弹幕XML、到解析并保存为CSV文件,完整呈现一个B站弹幕爬虫的实现全流程。项目代码中还包含了异常处理、编码兼容等实战细节,非常适合学习和实战参考。
技术背景
本项目涉及以下核心技术和库:
- requests:发送HTTP请求,获取B站API数据和弹幕XML
- lxml(etree):解析弹幕XML格式数据,提取每条弹幕的属性
- pandas:将弹幕数据保存为CSV文件,方便后续分析
- B站API:
/x/player/pagelist 获取视频分P信息,comment.bilibili.com/{cid}.xml 获取弹幕
B站的弹幕并不是通过动态JSON接口返回的,而是以XML格式存储在固定URL的文件中。理解这一点是整个爬虫能够工作的关键。
实现思路
B站弹幕爬取的整体流程如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ┌─────────────────────────────────────────────┐ │ 输入:视频BV号 │ └──────────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 1: 获取视频所有分P的cid │ │ API: api.bilibili.com/x/player/pagelist │ │ 返回: [{"cid":xxx, "page":1}, ...] │ └──────────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 2: 遍历每个分P,抓取弹幕XML │ │ URL: comment.bilibili.com/{cid}.xml │ │ 每个cid对应一个分P的弹幕文件 │ └──────────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 3: 解析XML,提取弹幕属性 │ │ <d p="时间,模式,字号,颜色,时间戳, │ │ 弹幕池,用户hash,弹幕id">内容</d> │ └──────────────────┬──────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Step 4: 保存为CSV(pandas) │ │ 字段: page, time_in_video, mode, │ │ color, content... │ └─────────────────────────────────────────────┘
|
关键概念说明:
- BV号:B站视频的唯一标识,如
BV1wbpRzaEhz
- cid:每个视频分P(P1、P2…)对应的唯一ID,弹幕XML文件以cid命名
- 弹幕XML格式:每条弹幕是一个
<d> 标签,p 属性包含弹幕的元数据
核心代码解析
1. 获取视频分P信息(cid列表)
B站一个视频可能有多个分P(比如一个系列教程),需要先获取每个分P的 cid:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import requests
BVID = "BV1wbpRzaEhz" HEADERS = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' }
cid_api = f"https://api.bilibili.com/x/player/pagelist?bvid={BVID}&jsonp=jsonp" response = requests.get(cid_api, headers=HEADERS) cid_data = response.json() pages = cid_data['data']
|
注意:这里加了 timeout=10 和 response.raise_for_status() 来增强健壮性,是优化版代码的改进点。
优化版中将其封装为独立函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| def fetch_cid_list(bvid): """ 根据视频的BV号获取所有分P的cid列表。 参数: bvid (str) - 视频的BV号 返回: list - 包含每个分P信息的字典列表 """ cid_api = f"https://api.bilibili.com/x/player/pagelist?bvid={bvid}&jsonp=jsonp" try: response = requests.get(cid_api, headers=headers, timeout=10) response.raise_for_status() cid_data = response.json() return cid_data['data'] except requests.exceptions.RequestException as e: print("请求 cid 列表失败:", str(e)) raise
|
2. 抓取并解析弹幕XML
这是核心步骤。B站弹幕存储在 https://comment.bilibili.com/{cid}.xml,是一个标准XML文件:
1 2 3 4 5 6
| <?xml version="1.0" encoding="UTF-8"?> <i> <d p="0.001,1,25,16777215,1640000000,0,abcdef123456,12345678">欢迎来到B站!</d> <d p="1.523,1,25,16777215,1640000001,0,abcdef123457,12345679">这个视频太好了</d> ... </i>
|
每条 <d> 标签的 p 属性格式为:时间,模式,字号,颜色,发送时间戳,弹幕池,用户hash,弹幕id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| from lxml import etree
def fetch_danmu_for_cid(cid, page_index): """ 根据cid获取指定分P的弹幕数据。 参数: cid (int) - 分P对应的cid page_index (int) - 当前分P的索引编号 返回: list - 弹幕信息列表 """ danmu_url = f"https://comment.bilibili.com/{cid}.xml" try: xml_response = requests.get(danmu_url, headers=headers, timeout=10) xml_response.encoding = 'utf-8' xml_text = xml_response.text.strip()
if not (xml_text.startswith('<?xml') or xml_text.startswith('<')): print(f"第 {page_index} P 弹幕获取异常,内容预览:") print(xml_text[:500]) return []
root = etree.fromstring(xml_text.encode('utf-8')) danmus = [] for d in root.xpath("//d"): p_attr = d.attrib.get("p", "").split(",") if len(p_attr) < 8: continue danmus.append({ "page": page_index, "time_in_video": float(p_attr[0]), "mode": int(p_attr[1]), "font_size": int(p_attr[2]), "color": int(p_attr[3]), "send_time": p_attr[4], "danmu_pool": p_attr[5], "user_hash": p_attr[6], "danmu_id": p_attr[7], "content": d.text }) return danmus except requests.exceptions.RequestException as e: print(f"第 {page_index} P 请求失败:", str(e)) return [] except etree.XMLSyntaxError as e: print(f"第 {page_index} P XML解析错误:", str(e)) print(xml_text[:1000]) return []
|
弹幕模式(mode)说明:
| 值 |
含义 |
| 1 |
滚动弹幕(从右向左) |
| 4 |
底部弹幕 |
| 5 |
顶部弹幕 |
| 6 |
逆向滚动弹幕 |
| 7 |
精确弹幕 |
| 8 |
高级弹幕 |
3. 主流程:遍历所有分P
1 2 3 4 5 6 7 8 9 10 11 12
| pages = fetch_cid_list(BVID) danmus = []
for page in pages: cid = page['cid'] page_index = page.get('page', 1) print(f"正在抓取第 {page_index} P 弹幕,cid={cid} ...") danmus.extend(fetch_danmu_for_cid(cid, page_index))
print(f"总共抓取弹幕数量: {len(danmus)}")
|
4. 保存为CSV文件
使用 pandas.DataFrame 可以非常方便地将数据保存为CSV:
1 2 3 4 5 6
| import pandas as pd
df = pd.DataFrame(danmus)
df.to_csv(OUTPUT_CSV, index=False, encoding="utf-8-sig") print(f"弹幕已保存到 {OUTPUT_CSV}")
|
5. 处理Windows终端中文编码问题
在Windows的CMD中直接打印中文弹幕可能会遇到 UnicodeEncodeError,代码中做了兼容处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| for d in danmus: try: print(tuple(d.values())) except UnicodeEncodeError: safe_output = [] for item in d.values(): if isinstance(item, str): try: safe_output.append(item) except UnicodeEncodeError: safe_output.append(item.encode('gbk', errors='replace').decode('gbk')) else: safe_output.append(item) print(tuple(safe_output))
|
进阶:同时爬取弹幕和评论并生成词云
在 数据标注/index.py 中,项目还实现了更完整的功能:同时爬取弹幕和评论,并使用 wordcloud 生成词云图。
获取评论的API调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| def fetch_comments(oid, pn=1): """ 获取指定视频的评论。 oid: 视频的 aid(实际上是cid或aid,根据type而定) pn: 页码 """ api = f"https://api.bilibili.com/x/v2/reply/wbi/main?oid={oid}&type=1&mode=2&pn={pn}&ps=20&plat=1" try: response = requests.get(api, headers=headers, timeout=10) response.raise_for_status() data = response.json()
if data['code'] == 0: replies = data['data'].get('replies', []) if not replies: return []
comments = [] for reply in replies: comment = { 'user': reply['member']['uname'], 'content': reply['content']['message'], 'like': reply['like'], 'reply_count': reply['rcount'], 'ctime': reply['ctime'], } comments.append(comment) return comments else: print(f"评论 API 返回错误:{data['message']}") return [] except Exception as e: print(f"评论请求失败:{str(e)}") return []
|
生成词云图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import jieba from wordcloud import WordCloud import matplotlib.pyplot as plt
def generate_wordcloud(text_list, output_path, title="词云图"): """根据文本列表生成词云图""" text = ' '.join(text_list) words = jieba.cut(text) word_string = ' '.join(words)
wordcloud = WordCloud( font_path='simhei.ttf', width=1920, height=1080, background_color='white', max_words=200, ).generate(word_string)
plt.figure(figsize=(19.2, 10.8)) plt.imshow(wordcloud, interpolation='bilinear') plt.axis('off') plt.title(title, fontsize=30) plt.savefig(output_path, dpi=300, bbox_inches='tight') plt.close()
|
运行效果
程序运行后输出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| ============================================================ 开始爬取视频 BV1YcmFBzE7s 的弹幕和评论 ============================================================
【步骤 1】正在获取视频信息... ✅ 视频 CID: 135692677
【步骤 2】正在抓取弹幕... → 正在抓取第 1 P 弹幕,cid=135692677 ... 总共抓取弹幕数量: 3456
【步骤 3】正在保存弹幕数据... ✅ 弹幕已保存到 数据标注_BV1YcmFBzE7s_danmu.csv
【步骤 4】正在抓取评论... → 正在爬取第 1 页评论... → 已获取 20 条评论,总计 20 条 → 正在爬取第 2 页评论... → 已获取 20 条评论,总计 40 条 ... ✅ 总共抓取评论数量:158
【步骤 5】正在保存评论数据... ✅ 评论已保存到 数据标注_BV1YcmFBzE7s_comments.json
【步骤 6】正在生成弹幕词云... ✅ 词云图已保存为 数据标注_BV1YcmFBzE7s_danmu_wordcloud.png
【步骤 7】正在生成评论词云... ✅ 词云图已保存为 数据标注_BV1YcmFBzE7s_comments_wordcloud.png
============================================================ ✅ 全部任务完成! ============================================================
生成的文件: 📄 弹幕数据:数据标注_BV1YcmFBzE7s_danmu.csv 📄 评论数据:数据标注_BV1YcmFBzE7s_comments.json 🖼️ 弹幕词云:数据标注_BV1YcmFBzE7s_danmu_wordcloud.png 🖼️ 评论词云:数据标注_BV1YcmFBzE7s_comments_wordcloud.png
|
生成的CSV文件内容预览:
| page |
time_in_video |
mode |
color |
content |
| 1 |
0.001 |
1 |
16777215 |
欢迎来到B站! |
| 1 |
1.523 |
1 |
16777215 |
这个视频太好了 |
| 1 |
2.100 |
4 |
16711680 |
打卡打卡 |
总结与优化方向
通过本文的B站弹幕爬虫项目,我们学习了:
- B站API调用:掌握分页信息API和弹幕XML接口的调用方式
- XML解析:使用lxml解析XML格式数据,提取结构化信息
- 异常处理:网络请求、XML解析、编码兼容等多层面的异常处理
- 数据持久化:用pandas保存为CSV,用json保存评论数据
- 扩展功能:同时爬取评论、生成词云图进行数据可视化
可优化方向:
- WBI签名:B站新版本API需要WBI签名验证,可使用
bilibili-api-python 库自动处理
- 大规模弹幕分析:结合NLP进行情感分析、关键词提取
- 弹幕密度分析:统计视频时间轴上的弹幕密度,找出”高能时刻”
- 异步爬取:使用
asyncio + aiohttp 同时爬取多个视频的弹幕
- 去重处理:同一视频可能被多次爬取,增加去重逻辑
- Cookie管理:将Cookie集中管理,支持多用户切换
注意:B站弹幕和评论API均需要有效的Cookie才能获取完整数据。请将代码中的COOKIE替换为你自己的B站登录Cookie。