python

B站视频弹幕爬取实战

2026-07-06 #python#爬虫

前言

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" # 目标视频的BV号
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}

# 构造获取视频分P信息的API地址
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'] # 每个元素对应一个分P

# pages 的数据结构:
# [
# {"cid": 135692677, "page": 1, "part": "第一集", ...},
# {"cid": 135692678, "page": 2, "part": "第二集", ...}
# ]

注意:这里加了 timeout=10response.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() # 自动抛出HTTP错误
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()

# 检查是否是合法的XML内容(防止返回错误页面)
if not (xml_text.startswith('<?xml') or xml_text.startswith('<')):
print(f"第 {page_index} P 弹幕获取异常,内容预览:")
print(xml_text[:500])
return []

# 使用 lxml 解析 XML
root = etree.fromstring(xml_text.encode('utf-8'))
danmus = []

# 使用 XPath 选取所有 <d> 标签
for d in root.xpath("//d"):
# 解析 p 属性(逗号分隔的8个字段)
p_attr = d.attrib.get("p", "").split(",")
if len(p_attr) < 8:
continue # 跳过格式异常的弹幕

danmus.append({
"page": page_index, # 所属分P编号
"time_in_video": float(p_attr[0]), # 弹幕在视频中的出现时间(秒)
"mode": int(p_attr[1]), # 弹幕类型
"font_size": int(p_attr[2]), # 字体大小
"color": int(p_attr[3]), # 颜色RGB值(十进制)
"send_time": p_attr[4], # 发送时间戳(Unix时间)
"danmu_pool": p_attr[5], # 弹幕池分类(0=普通池)
"user_hash": p_attr[6], # 用户ID哈希值(匿名)
"danmu_id": p_attr[7], # 弹幕唯一ID
"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
# 获取所有分P的cid信息
pages = fetch_cid_list(BVID)
danmus = []

# 遍历所有分P并抓取弹幕
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)
# encoding='utf-8-sig' 解决Excel打开中文乱码问题
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:
# GBK编码无法表示的字符用 ? 替换
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: 页码
"""
# mode=2 表示按热度排序,mode=3 表示按时间排序
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="词云图"):
"""根据文本列表生成词云图"""
# 使用 jieba 分词(中文分词)
text = ' '.join(text_list)
words = jieba.cut(text)
word_string = ' '.join(words)

# 创建词云(需要 simhei.ttf 中文字体文件)
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站弹幕爬虫项目,我们学习了:

  1. B站API调用:掌握分页信息API和弹幕XML接口的调用方式
  2. XML解析:使用lxml解析XML格式数据,提取结构化信息
  3. 异常处理:网络请求、XML解析、编码兼容等多层面的异常处理
  4. 数据持久化:用pandas保存为CSV,用json保存评论数据
  5. 扩展功能:同时爬取评论、生成词云图进行数据可视化

可优化方向:

  • WBI签名:B站新版本API需要WBI签名验证,可使用 bilibili-api-python 库自动处理
  • 大规模弹幕分析:结合NLP进行情感分析、关键词提取
  • 弹幕密度分析:统计视频时间轴上的弹幕密度,找出”高能时刻”
  • 异步爬取:使用 asyncio + aiohttp 同时爬取多个视频的弹幕
  • 去重处理:同一视频可能被多次爬取,增加去重逻辑
  • Cookie管理:将Cookie集中管理,支持多用户切换

注意:B站弹幕和评论API均需要有效的Cookie才能获取完整数据。请将代码中的COOKIE替换为你自己的B站登录Cookie。

评论
分享