起因
想写个工具自动获取B站视频字幕做内容整理,本以为要搞登录cookie那些麻烦事,结果发现了一条「野生API」——完全不需要登录就能拿到字幕JSON。
网上大部分教程都在教你用 player/w2 接口,但你仔细看B站前端源码,弹幕接口 x/v2/dm/view 的返回值里竟然顺带包含了字幕URL。这意味着:不需要任何cookie、不需要SESSDATA、不需要扫码登录,一个裸请求就能拿到。
两种API对比
B站获取字幕的主流方式有两种,大部分人只知道第一种:
| 对比项 | player/w2(字幕接口) |
x/v2/dm/view(弹幕接口) |
|---|---|---|
| 接口地址 | https://api.bilibili.com/x/player/wapi/v2 |
https://api.bilibili.com/x/v2/dm/view |
| 是否需要登录 | ✅ 必须携带Cookie(SESSDATA) | ❌ 不需要任何登录态 |
| 核心参数 | bvid + cid + Cookie |
oid(即cid)+ type=1 |
| 字幕数据位置 | data.subtitle.subtitles |
data.subtitle.subtitles |
| 额外依赖 | 需要拿到登录后的cookie | 只需要cid |
| 稳定性 | 官方接口,但频繁变登录策略 | 弹幕副产物,长期稳定 |
核心发现:dm/view 本来是用来拿弹幕的,但返回的JSON里 subtitle.subtitles 字段包含了所有字幕的下载地址。B站前端自己就是这么拿到字幕的——只不过大家一般只关注弹幕部分,忽略了字幕字段。
三步流程(BV1GJ411x7h7 实测)
用经典的 Rick Astley “Never Gonna Give You Up” B站视频做实测演示。这个视频有多语言字幕,非常适合验证。
第一步:获取 cid
每个B站视频都有唯一的 cid(内容ID),通过视频信息接口获取:
curl -s "https://api.bilibili.com/x/web-interface/view?bvid=BV1GJ411x7h7" \
-H "User-Agent: Mozilla/5.0" | python3 -c "import json,sys;d=json.load(sys.stdin);print(f'cid={d[\"data\"][\"cid\"]}, title={d[\"data\"][\"title\"]}')"
实际输出:
cid=137649199, title=永不放弃你——瑞克·艾斯里
也可以直接查看完整的视频信息JSON(截取关键字段):
curl -s "https://api.bilibili.com/x/web-interface/view?bvid=BV1GJ411x7h7" -H "User-Agent: Mozilla/5.0" | python3 -c "
import json, sys
d = json.load(sys.stdin)['data']
print(f'bvid: {d["bvid"]}')
print(f'aid: {d["aid"]}')
print(f'cid: {d["cid"]}')
print(f'title: {d["title"]}')
print(f'owner: {d["owner"]["name"]}')
print(f'播放量: {d["stat"]["view"]}')
"
输出:
bvid: BV1GJ411x7h7
aid: 89516453
cid: 137649199
title: 永不放弃你——瑞克·艾斯里
owner: 御坂14388号
播放量: 24184550
第二步:通过弹幕接口拿字幕URL
这是关键步骤!用 dm/view 接口获取字幕列表:
curl -s "https://api.bilibili.com/x/v2/dm/view?oid=137649199&type=1" \
-H "Referer: https://www.bilibili.com/" \
-H "User-Agent: Mozilla/5.0" | python3 -c "
import json, sys
d = json.load(sys.stdin)['data']['subtitle']
print(f'字幕数量: {len(d["subtitles"])}')
print('---')
for s in d['subtitles']:
print(f' [{s["lan"]}] {s["lan_doc"]} (ai_type={s.get("ai_type",0)})')
print(f' URL: {s["subtitle_url"]}')
"
实际输出——12种语言字幕,全部可获取:
字幕数量: 12
---
[zh-CN] 中文(中国)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[en-US] English(US)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[ja-JP] 日本語(日本)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[ko-KR] 한국어(대한민국)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[fr-FR] Français(France)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[de-DE] Deutsch(Deutschland)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[ru-RU] Русский(Россия)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[es-ES] Español(España)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[it-IT] Italiano(Italia)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[pt-BR] Português(Brasil)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[th-TH] ไทย(ไทย)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
[vi-VN] Tiếng Việt(Việt Nam)(ai_type=1)
URL: //aisubtitle.hdslb.com/bfs/ai_subtitle/prod/...
💡 提示:注意URL是
//开头的协议相对路径,使用时需要补全为https:前缀。
第三步:下载并解析字幕JSON
字幕文件是标准的JSON格式,包含时间戳和文本:
# 拿中文字幕的前5条看看效果
SUBTITLE_URL="https:$(curl -s 'https://api.bilibili.com/x/v2/dm/view?oid=137649199&type=1' -H 'Referer: https://www.bilibili.com/' -H 'User-Agent: Mozilla/5.0' | python3 -c "
import json,sys
subs=json.load(sys.stdin)['data']['subtitle']['subtitles']
zh=[s['subtitle_url'] for s in subs if s['lan']=='zh-CN']
print(zh[0] if zh else subs[0]['subtitle_url'])
")"
curl -s "$SUBTITLE_URL" | python3 -c "
import json, sys
body = json.load(sys.stdin)['body']
for line in body[:5]:
print(f'{line["from"]:.1f}s - {line["to"]:.1f}s')
print(f' {line["content"]}')
print()
"
实际输出:
0.1s - 5.1s
永不放弃你——瑞克·艾斯里
5.1s - 12.8s
我们不再相识 彼此疏远
告诉你其中的含义
12.8s - 20.8s
我们彼此心知肚明
我们一直在玩这个游戏
20.8s - 28.5s
全心全意 你不会让我失望
你不会让我失望
28.5s - 36.2s
永不放弃你 永不让你失望
永不离开你
再看英文字幕(en-US)的对比效果:
0.1s - 5.1s
Never Gonna Give You Up - Rick Astley
5.1s - 12.8s
We're no strangers to love
You know the rules and so do I
12.8s - 20.8s
A full commitment's what I'm thinking of
You wouldn't get this from any other guy
20.8s - 28.5s
I just wanna tell you how I'm feeling
Gotta make you understand
28.5s - 36.2s
Never gonna give you up
Never gonna let you down
字幕JSON结构详解
每条字幕的字段结构如下:
{
"from": 0.1, // 开始时间(秒)
"to": 5.1, // 结束时间(秒)
"sid": 1, // 字幕序号
"content": "永不放弃你——瑞克·艾斯里", // 字幕文本
"music": 0.0 // 音乐标记(一般不用管)
}
这个结构非常清晰,直接可以用来做字幕拼接、翻译对照、内容提取等各种处理。
Python 一键函数(完整版)
封装成一个函数,支持指定语言、输出纯文本或带时间戳的格式:
import requests
from typing import Optional, List, Dict
def get_bilibili_subtitle(
bvid: str,
lang: str = "zh-CN",
with_timestamp: bool = False
) -> Optional[List[Dict]]:
"""
获取B站视频字幕(无需登录)
Args:
bvid: 视频BV号,如 "BV1GJ411x7h7"
lang: 字幕语言代码,默认 "zh-CN"
常用: zh-CN, en-US, ja-JP, ko-KR, fr-FR, de-DE, ru-RU
with_timestamp: 是否在输出中包含时间戳
Returns:
字幕列表,每项包含 from, to, content 等字段
视频无字幕时返回 None
Example:
>>> subs = get_bilibili_subtitle("BV1GJ411x7h7", "zh-CN")
>>> for line in subs:
... print(line["content"])
"""
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://www.bilibili.com/"
}
# Step 1: 获取cid
resp = requests.get(
"https://api.bilibili.com/x/web-interface/view",
params={"bvid": bvid},
headers=headers
)
cid = resp.json()["data"]["cid"]
# Step 2: 通过弹幕接口获取字幕URL
resp = requests.get(
"https://api.bilibili.com/x/v2/dm/view",
params={"oid": cid, "type": 1},
headers=headers
)
subtitles = resp.json()["data"]["subtitle"]["subtitles"]
if not subtitles:
print(f"视频 {bvid} 没有字幕")
return None
# Step 3: 匹配目标语言
target = None
for sub in subtitles:
if sub["lan"] == lang:
target = sub
break
# 如果没有指定语言的字幕,取第一个
if target is None:
print(f"未找到 {lang} 字幕,可用语言: {[s['lan'] for s in subtitles]}")
target = subtitles[0]
print(f"使用: {target['lan']} ({target['lan_doc']})")
# Step 4: 下载字幕JSON
url = target["subtitle_url"]
if url.startswith("//"):
url = "https:" + url
resp = requests.get(url, headers=headers)
body = resp.json()["body"]
return body
def print_subtitle(subs: List[Dict], with_timestamp: bool = True):
"""打印字幕,格式化输出"""
if not subs:
return
for line in subs:
if with_timestamp:
print(f"{line['from']:.1f}s - {line['to']:.1f}s")
print(f" {line['content']}")
else:
print(line["content"])
def get_subtitle_text(subs: List[Dict]) -> str:
"""将字幕合并为纯文本"""
if not subs:
return ""
return "\n".join(line["content"] for line in subs)
# ===== 使用示例 =====
if __name__ == "__main__":
# 示例1:获取中文字幕并打印(带时间戳)
print("=== 中文字幕 ===")
subs = get_bilibili_subtitle("BV1GJ411x7h7", "zh-CN")
if subs:
print_subtitle(subs[:5]) # 只打印前5条
print()
# 示例2:获取英文字幕
print("=== 英文字幕 ===")
subs_en = get_bilibili_subtitle("BV1GJ411x7h7", "en-US")
if subs_en:
print_subtitle(subs_en[:5])
print()
# 示例3:纯文本输出(适合做内容整理)
print("=== 纯文本模式 ===")
if subs:
print(get_subtitle_text(subs[:3]))
实际运行效果
=== 中文字幕 ===
0.1s - 5.1s
永不放弃你——瑞克·艾斯里
5.1s - 12.8s
我们不再相识 彼此疏远
告诉你其中的含义
12.8s - 20.8s
我们彼此心知肚明
我们一直在玩这个游戏
20.8s - 28.5s
全心全意 你不会让我失望
你不会让我失望
28.5s - 36.2s
永不放弃你 永不让你失望
永不离开你
=== 英文字幕 ===
0.1s - 5.1s
Never Gonna Give You Up - Rick Astley
5.1s - 12.8s
We're no strangers to love
You know the rules and so do I
...
=== 纯文本模式 ===
永不放弃你——瑞克·艾斯里
我们不再相识 彼此疏远
告诉你其中的含义
我们彼此心知肚明
我们一直在玩这个游戏
Bash 一键脚本
不想写Python?一条命令搞定:
#!/bin/bash
# bilibili_subtitle.sh - B站字幕一键获取脚本
# 用法: ./bilibili_subtitle.sh BV1GJ411x7h7 [语言代码]
# 示例: ./bilibili_subtitle.sh BV1GJ411x7h7 zh-CN
# ./bilibili_subtitle.sh BV1GJ411x7h7 en-US
BVID="${1:-BV1GJ411x7h7}"
LANG="${2:-zh-CN}"
echo "=== 获取视频 $BVID 的 $LANG 字幕 ==="
# Step 1: 获取cid
CID=$(curl -s "https://api.bilibili.com/x/web-interface/view?bvid=$BVID" -H "User-Agent: Mozilla/5.0" | python3 -c "import json,sys;print(json.load(sys.stdin)['data']['cid'])")
if [ -z "$CID" ]; then
echo "错误: 无法获取cid,请检查BV号是否正确"
exit 1
fi
echo "cid: $CID"
# Step 2: 获取字幕URL
SUBTITLE_URL=$(curl -s "https://api.bilibili.com/x/v2/dm/view?oid=$CID&type=1" -H "Referer: https://www.bilibili.com/" -H "User-Agent: Mozilla/5.0" | python3 -c "
import json, sys
data = json.load(sys.stdin)
subs = data['data']['subtitle']['subtitles']
if not subs:
print('NONE')
exit(0)
target = [s for s in subs if s['lan']=='$LANG']
url = target[0]['subtitle_url'] if target else subs[0]['subtitle_url']
print(url)
")
if [ "$SUBTITLE_URL" = "NONE" ]; then
echo "该视频没有字幕"
exit 1
fi
# 补全协议
SUBTITLE_URL="https:${SUBTITLE_URL}"
# Step 3: 下载并显示字幕
echo "---"
curl -s "$SUBTITLE_URL" | python3 -c "
import json, sys
body = json.load(sys.stdin)['body']
for i, line in enumerate(body, 1):
m, s = divmod(line['from'], 60)
print(f'[{int(m):02d}:{s:05.2f}] {line["content"]}')
"
echo "---"
echo "共获取 $(curl -s "$SUBTITLE_URL" | python3 -c "import json,sys;print(len(json.load(sys.stdin)['body']))") 条字幕"
运行示例:
$ ./bilibili_subtitle.sh BV1GJ411x7h7 zh-CN
=== 获取视频 BV1GJ411x7h7 的 zh-CN 字幕 ===
cid: 137649199
---
[00:00.10] 永不放弃你——瑞克·艾斯里
[00:05.10] 我们不再相识 彼此疏远
告诉你其中的含义
[00:12.80] 我们彼此心知肚明
我们一直在玩这个游戏
[00:20.80] 全心全意 你不会让我失望
你不会让我失望
...
---
共获取 42 条字幕
扩展注意事项
1. v2 vs v1 弹幕接口
弹幕接口有两个版本:
x/v2/dm/view(推荐)—— 当前主力接口,返回结构稳定,包含字幕信息x/v1/dm/view(旧版)—— 部分老视频可能还在用,但新视频已经迁移到v2
实测对比:
# v2 接口 - 推荐使用
curl -s "https://api.bilibili.com/x/v2/dm/view?oid=137649199&type=1" -H "Referer: https://www.bilibili.com/" | python3 -c "
import json,sys
d=json.load(sys.stdin)
print('v2 code:', d['code'])
print('字幕数:', len(d['data']['subtitle']['subtitles']))
"
# 输出: v2 code: 0 字幕数: 12
2. ai_type 区分人工字幕和AI字幕
字幕列表中每条记录都有 ai_type 字段:
ai_type = 0:人工字幕——UP主或用户手动上传的CC字幕,质量最高ai_type = 1:AI自动生成字幕——B站语音识别自动生成,覆盖面广但可能有误差
筛选示例:
# 只看人工字幕
curl -s "https://api.bilibili.com/x/v2/dm/view?oid=137649199&type=1" -H "Referer: https://www.bilibili.com/" | python3 -c "
import json, sys
subs = json.load(sys.stdin)['data']['subtitle']['subtitles']
manual = [s for s in subs if s['ai_type'] == 0]
ai = [s for s in subs if s['ai_type'] == 1]
print(f'人工字幕: {len(manual)} 条')
print(f'AI字幕: {len(ai)} 条')
for s in manual:
print(f' [{s["lan"]}] {s["lan_doc"]}')
"
这个视频(BV1GJ411x7h7)的12种字幕全部是AI生成的(ai_type=1),说明B站的AI多语言字幕能力还是很强的。
3. 不是所有视频都有字幕
需要判断的情况:
curl -s "https://api.bilibili.com/x/v2/dm/view?oid=12345&type=1" -H "Referer: https://www.bilibili.com/" | python3 -c "
import json, sys
d = json.load(sys.stdin)
subs = d['data']['subtitle']['subtitles']
if not subs:
print('该视频没有字幕(字幕列表为空)')
else:
print(f'可用字幕: {len(subs)} 种')
for s in subs:
print(f' {s["lan"]} - {s["lan_doc"]} (AI={s.get("ai_type",0)==1})')
"
一般来说:
- 较新的热门视频大多有AI自动字幕
- 有CC字幕标志的视频一定有人工字幕
- 老视频、纯音乐视频可能没有字幕
4. 请求频率控制
虽然不需要登录,但请求太频繁可能触发风控:
- 建议每次请求间隔 ≥1秒
- 批量获取时加入
time.sleep(1) - 不要用多线程疯狂并发请求
import time
# 批量获取多个视频字幕的正确姿势
bvids = ["BV1GJ411x7h7", "BV1xx411c7mD", "BV1GJ411x7h7"]
for bvid in bvids:
subs = get_bilibili_subtitle(bvid)
if subs:
print(f"{bvid}: {len(subs)} 条字幕")
time.sleep(1) # 重要!每次间隔1秒
5. 多P(分集)视频处理
多P视频的每个分P有不同的cid,需要分别获取:
# 获取多P视频所有分P的cid
curl -s "https://api.bilibili.com/x/web-interface/view?bvid=BV1xx411c7mD" -H "User-Agent: Mozilla/5.0" | python3 -c "
import json, sys
d = json.load(sys.stdin)['data']
pages = d['pages']
print(f'共 {len(pages)} P')
for p in pages:
print(f' P{p["page"]}: {p["part"]} (cid={p["cid"]})')
"
然后对每个cid分别调用 dm/view 获取对应分P的字幕。
6. 常用语言代码速查
| 语言代码 | 语言名称 | 说明 |
|---|---|---|
zh-CN |
中文(中国) | 简体中文,最常用 |
zh-TW |
中文(台湾) | 繁体中文 |
en-US |
英语(美国) | 英文字幕 |
ja-JP |
日语(日本) | 日文字幕 |
ko-KR |
韩语(韩国) | 韩文字幕 |
fr-FR |
法语(法国) | 法文字幕 |
de-DE |
德语(德国) | 德文字幕 |
ru-RU |
俄语(俄罗斯) | 俄文字幕 |
es-ES |
西班牙语(西班牙) | 西语字幕 |
it-IT |
意大利语(意大利) | 意语字幕 |
pt-BR |
葡萄牙语(巴西) | 葡语字幕 |
th-TH |
泰语(泰国) | 泰语字幕 |
vi-VN |
越南语(越南) | 越南语字幕 |
总结
x/v2/dm/view是B站弹幕接口的副产物,不需要登录即可获取字幕- 三步走流程:
view拿cid →dm/view拿字幕URL → 下载字幕JSON - 实测 Rick Astley 经典视频有 12种语言字幕,全部AI生成
- 提供了完整的Python函数和Bash脚本,开箱即用
- 注意请求频率控制,间隔≥1秒