B站 UP 主全量视频抓取:wbi 签名与系列筛选实战
# B站 UP 主全量视频抓取:wbi 签名与系列筛选实战
> 摘要:新版 B站接口全面引入 wbi 签名,让"拉取某 UP 主全部视频"从几行 requests 变成一道坎。本文给出完整的 wbi 签名实现(mixin key 算法 + nav 接口取密钥),并用真实案例演示:从 262 个视频中精确筛出 128 期系列节目,包括处理标题不带编号的"漏网之鱼"。
## 背景
想批量整理某 B站 UP 主的某个系列视频(比如周更栏目),第一步就是拿到 TA 的**全部视频列表**。老接口 `x/space/arc/search` 已废弃,新接口 `x/space/wbi/arc/search` 强制要求 wbi 签名——参数里要带 `w_rid` 和 `wts`,否则直接 412。
## 正文
### 1. wbi 签名原理
wbi 签名的核心是 **mixin key**:
1. 请求 `x/web-interface/nav` 拿 `wbi_img.img_url` 和 `sub_url`
2. 取两个 URL 文件名(去扩展名)拼起来:`img_key + sub_key`
3. 按固定的 64 位重排表 `mixinKeyEncTab` 重排,取前 32 位 → mixin key
4. 参数按 key 排序,拼上 `wts`(当前时间戳),再拼 mixin key,整体做 MD5 → `w_rid`
代码实现(Python):
```python
import hashlib, time, urllib.parse
from functools import reduce
mixinKeyEncTab = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
]
def get_mixin_key(orig: str) -> str:
return reduce(lambda s, i: s + orig[i], mixinKeyEncTab, '')[:32]
def enc_wbi(params: dict, img_key: str, sub_key: str) -> dict:
mixin_key = get_mixin_key(img_key + sub_key)
params['wts'] = round(time.time())
params = dict(sorted(params.items()))
query = urllib.parse.urlencode(params)
params['w_rid'] = hashlib.md5((query + mixin_key).encode()).hexdigest()
return params
```
### 2. 拿密钥 + 分页拉全量
密钥从 `x/web-interface/nav` 拿(该接口本身不需要签名):
```python
def get_wbi_keys(session):
d = session.get('https://api.bilibili.com/x/web-interface/nav').json()
img = d['data']['wbi_img']['img_url']
sub = d['data']['wbi_img']['sub_url']
return img.rsplit('/', 1)[-1].split('.')[0], sub.rsplit('/', 1)[-1].split('.')[0]
```
分页拉取(`ps=50` 每页,翻到 `count` 为止):
```python
all_videos = []
pn = 1
while True:
params = enc_wbi({'mid': MID, 'ps': 50, 'pn': pn, 'order': 'pubdate'},
img_key, sub_key)
d = session.get('https://api.bilibili.com/x/space/wbi/arc/search',
params=params, timeout=20).json()
vlist = d['data']['list']['vlist']
all_videos.extend(vlist)
if len(all_videos) >= d['data']['page']['count'] or not vlist:
break
pn += 1
time.sleep(1) # 温柔一点,别把自己送进风控
```
几个细节:
- **必须带登录 cookie**(浏览器导出,含 SESSDATA),否则 nav 拿不到完整响应
- **请求头**:`User-Agent` 用真实 Chrome 版本号,`Referer` 指向 UP 主空间页
- 请求库推荐 `curl_cffi`(`impersonate='chrome'` 模拟浏览器指纹),比裸 requests 稳得多
### 3. 系列筛选:不只是关键词匹配
262 个视频里筛出 128 期系列,难点不在"关键词",而在**标题格式不统一**:
| 标题形态 | 例子 | 处理 |
|---------|------|------|
| 标准编号 | 「Github一周热点126期」 | 正则 `(\d+)期` |
| 无「期」字 | 「Github一周热点19」 | 正则 `热点(\d+)` |
| 无编号 | 「GitHub本周必收藏的5个热点项目!…」 | 人工映射表兜底 |
| 无关键词 | 「AI Agent的记忆系统、超省钱的…」 | 按发布时间窗口 + 人工确认 |
结论:**纯正则筛不全,必须加一张"特殊标题 → 期号"的映射表**,配合发布时间窗口校验(相邻期号间隔约一周,可用于发现漏网视频)。
## 踩坑记录
1. **wbi 接口裸调必 412**——不是被风控,是缺 `w_rid`。先确认签名实现正确,再谈别的
2. **nav 接口拿到的 key 会轮换**——建议每次会话重新拉一次,不要缓存复用
3. **搜索接口 ≠ 全量**:`x/web-interface/search/type` 只能搜到部分视频,全量必须走 `arc/search`
## 总结
- wbi 签名 = mixinKeyEncTab 重排 + wts + MD5,半小时能实现
- 全量列表抓取的正确姿势:nav 取密钥 → 签名分页 → 落盘 JSON
- 系列筛选要用「正则 + 映射表 + 时间窗口」三件套,别只靠关键词
下一步:拿到列表后如何批量下载视频字幕(限流、假残缺排查),见系列下一篇。

浙公网安备 33010602011771号