python

B站视频评论区爬虫

2026-07-06 #python#爬虫

前言

弹幕是B站特色的实时评论,而评论区则是用户深度讨论的阵地。无论是为了做舆情监控、用户情感分析,还是训练NLP模型,B站评论区的数据都具有极高的价值。

本文基于真实项目代码,详细介绍如何使用Python爬取B站视频的评论区数据。与弹幕不同,评论区数据通过JSON API返回,需要处理分页、用户信息提取、数据持久化等问题。文章还会对比原版代码和AI优化版代码的差异,展示如何让爬虫代码更健壮。

技术背景

本项目涉及以下核心技术和库:

  • requests:发送HTTP请求,调用B站评论API
  • json:解析API返回的JSON格式评论数据
  • os / datetime:从环境变量读取配置、动态生成文件名
  • B站评论API/x/v2/reply/wbi/main 是B站评论的核心接口,支持分页、排序

B站评论API需要携带有效的Cookie(尤其是 SESSDATAbili_jct 字段),否则可能无法获取完整数据或被限流。

实现思路

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
32
33
34
35
36
37
38
┌─────────────────────────────────────────────┐
│ 输入:视频 aid / oid │
│ (评论API使用oid标识视频,非BV号) │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 1: 构造评论API请求 │
│ URL: api.bilibili.com/x/v2/reply/wbi/main │
│ 参数: oid, type=1, mode, pn(页码), ps=20 │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 2: 发送请求并解析JSON │
│ data['code'] == 0 表示成功 │
│ data['data']['replies'] 是评论列表 │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 3: 提取评论字段 │
│ member.uname, content.message, │
│ like, rcount, ctime... │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 4: 分页爬取(循环或限定页数) │
│ 当 replies 为空或达到最大页数时停止 │
└──────────────────┬──────────────────────────┘


┌─────────────────────────────────────────────┐
│ Step 5: 保存为JSON文件 │
│ 使用 timestamp 动态生成文件名 │
│ json.dump(data, f, ensure_ascii=False) │
└─────────────────────────────────────────────┘

关键参数说明:

  • oid:视频的av号(aid),也可以通过BV号转换得到
  • type:评论类型,=1 表示视频评论
  • mode:排序方式,=2 表示按热度排序,=3 表示按时间排序
  • pn:页码,从1开始
  • ps:每页条数,默认20

核心代码解析

1. 原版代码:简单直接但不够健壮

原版代码直接写死API URL,缺乏异常处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import json
import random
import requests

# 直接写死API URL(包含签名参数w_rid和wts)
Url = 'https://api.bilibili.com/x/v2/reply/wbi/main?oid=115677092512169&type=1&mode=3&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=&web_location=1315875&w_rid=f3f3779e36f2e7dee6095fd18cd0b3b6&wts=1774276504'

user_agents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
# ... 更多User-Agent
]

headers = {
'User-Agent': random.choice(user_agents),
'Cookie': 'buvid3=FD4ACF55-91A3-A459-A0DB-DA90BBAA05FC75428infoc; ...'
}

# 直接发送请求
response = requests.get(url=Url, headers=headers)
jsonData = response.json()

# 保存为JSON文件(文件名写死)
with open('b站评论.json', 'w', encoding='utf-8') as f:
json.dump(jsonData, f, ensure_ascii=False, indent=4)

原版的问题:

  1. URL中的 w_ridwts 是签名参数,会过期,写死后无法长期使用
  2. 没有异常处理,请求失败会直接抛异常
  3. 文件名写死,多次运行会覆盖之前的数据
  4. 没有实现分页,只能获取第一页评论

2. AI优化版:增加异常处理和动态文件名

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
import json
import random
import requests
import os
from datetime import datetime

# 从环境变量中读取配置(更安全,便于部署)
api_url = os.getenv('BILIBILI_API_URL', 'https://api.bilibili.com/x/v2/reply/wbi/main?oid=113655756687306&type=1&mode=2&pagination_str=%7B%22offset%22:%22%22%7D&plat=1&seek_rpid=&web_location=1315875&w_rid=e4daf57743e496a55b309acfdd0a9e71&wts=1734759477')
cookie = os.getenv('BILIBILI_COOKIE', 'buvid3=7C93E44A-C719-6970-D124-BC5EF99BF56951575infoc; ...')

headers = {
'User-Agent': random.choice(user_agents),
'Cookie': cookie
}

try:
response = requests.get(url=api_url, headers=headers)

if response.status_code == 200:
jsonData = response.json()

# 动态生成文件名(使用时间戳避免覆盖)
timestamp = datetime.now().strftime('%Y%m%d%H%M%S')
filename = f'b站评论_{timestamp}.json'

with open(filename, 'w', encoding='utf-8') as f:
json.dump(jsonData, f, ensure_ascii=False, indent=4)
print(f"数据已成功保存到 {filename}")
else:
print(f"请求失败,状态码: {response.status_code}")

except requests.RequestException as e:
print(f"请求过程中发生错误: {e}")
except json.JSONDecodeError as e:
print(f"JSON解析失败: {e}")
except Exception as e:
print(f"发生未知错误: {e}")

优化版的改进:

  1. 使用 try-except 捕获各类异常
  2. 检查HTTP状态码再解析JSON
  3. 动态文件名,避免数据覆盖
  4. 支持从环境变量读取敏感配置

3. 完整的分页爬取实现

要结合分页逻辑,需要不断翻页直到没有更多评论:

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
def fetch_all_comments(oid, max_pages=10):
"""
爬取视频的所有评论(分页)
oid: 视频的aid
max_pages: 最大爬取页数(防止无限循环)
"""
all_comments = []
page_num = 1

while page_num <= max_pages:
# mode=2 按热度排序,每页20条
api = f"https://api.bilibili.com/x/v2/reply/wbi/main?oid={oid}&type=1&mode=2&pn={page_num}&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:
print(f" 第 {page_num} 页没有评论了,爬取结束")
break

for reply in replies:
comment = {
'user': reply['member']['uname'],
'content': reply['content']['message'],
'like': reply['like'],
'reply_count': reply['rcount'],
'ctime': reply['ctime'],
'sex': reply['member']['sex'],
'level': reply['member']['level_info']['current_level'],
}
all_comments.append(comment)

print(f" 第 {page_num} 页:获取 {len(replies)} 条评论,总计 {len(all_comments)} 条")
page_num += 1
time.sleep(1) # 友好爬取,避免被限流

else:
print(f" API返回错误:{data['message']}")
break

except Exception as e:
print(f" 第 {page_num} 页请求失败:{e}")
break

return all_comments

4. 评论数据字段详解

B站评论API返回的JSON结构非常丰富,常用字段如下:

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
{
"code": 0,
"data": {
"replies": [
{
"rpid": 123456789, # 评论ID
"oid": 113655756687306, # 视频aid
"type": 1, # 评论类型
"mid": 123456, # 评论者UID
"root": 0, # 根评论ID(0表示一级评论)
"parent": 0, # 父评论ID
"dialog": 0, # 对话ID
"ctime": 1734759477, # 评论时间戳
"mtime": 1734759500, # 最后修改时间
"content": {
"message": "这个视频太棒了!",
"plat": 1, # 平台(1=Web)
"device": "", # 设备信息
},
"member": {
"mid": 123456,
"uname": "用户名",
"sex": "男", # 性别
"face": "http://...", # 头像URL
"level_info": {
"current_level": 5 # 用户等级
}
},
"like": 123, # 点赞数
"rcount": 5, # 回复数
"up_action": {
"like": false, # UP主是否点赞
"reply": false # UP主是否回复
}
}
],
"page": {
"num": 10,
"size": 20,
"count": 200 # 评论总数
}
}
}

运行效果

1
2
3
4
5
6
7
8
9
10
11
开始爬取视频评论...
→ 正在爬取第 1 页评论...
→ 已获取 20 条评论,总计 20 条
→ 正在爬取第 2 页评论...
→ 已获取 20 条评论,总计 40 条
→ 正在爬取第 3 页评论...
→ 已获取 18 条评论,总计 58 条
→ 第 4 页没有评论了,爬取结束

✅ 共获取 58 条评论
数据已保存到 b站评论_20241221135639.json

生成的JSON文件内容预览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[
{
"user": "用户A",
"content": "这个视频讲得太好了!",
"like": 123,
"reply_count": 5,
"ctime": 1734759477,
"sex": "男",
"level": 5
},
{
"user": "用户B",
"content": "打卡打卡,先点赞后看",
"like": 45,
"reply_count": 1,
"ctime": 1734759400,
"sex": "女",
"level": 3
}
]

总结与优化方向

通过本文的B站评论区爬虫项目,我们学习了:

  1. B站评论API调用:掌握分页、排序等参数的使用
  2. 异常处理:多层次的异常捕获,让爬虫更健壮
  3. 数据持久化:动态文件名、JSON格式化输出
  4. 配置管理:使用环境变量管理Cookie等敏感信息

可优化方向:

  • WBI签名生成:B站新版本API需要在请求中携带 w_rid(签名)和 wts(时间戳)参数。可以使用 bilibili-api-python 库自动生成签名,无需手动构造
  • 子评论爬取:本文只爬取了一级评论,可以继续爬取每个评论下的回复(rpid 作为 root 参数)
  • 增量爬取:记录已爬取的最大 rpid,下次只爬取新增评论
  • 异步爬取:使用 asyncio + aiohttp 并发爬取多页评论
  • 数据清洗:去除emoji、HTML标签,统一编码格式
  • 结合弹幕数据:将评论和弹幕数据合并分析,获得更全面的舆情画像

注意:请合理设置请求频率(建议每页间隔至少1秒),避免对B站服务器造成过大压力。请将代码中的Cookie替换为你自己的B站登录Cookie。

评论
分享