哔哩哔哩 Bilibili 视频与字幕 API 教程:搜索、详情与字幕提取
通过 Just One API 搜索哔哩哔哩 Bilibili 视频,关联 BVID、AID 和分 P 的 CID,获取字幕轨道与资源链接。包含下载状态区分和文本保存的 Python 示例。
从视频搜索到可整理的字幕文本
整理哔哩哔哩 Bilibili 教程、课程或产品讲解时,可以先搜索视频,再获取详情和可用字幕。字幕文本便于检索、整理知识点,也可以作为后续 AI 摘要的输入。
Just One API 提供视频搜索、视频详情和视频字幕接口。本教程以“Python 入门”为关键词,选择最多两个视频,查询各自第一个分 P 的字幕轨道,并尝试保存第一个语言轨道的文本。视频有无字幕、语言种类和资源是否可下载,都需要根据实际响应判断。
获取 Token 并测试视频搜索
注册 Just One API 后获取 API Token,通过哔哩哔哩视频搜索在线文档免费测试。一个 Token 可用于多个平台接口,后续按实际调用量付费;可用权限及免费测试次数以账户显示为准。
本文使用三个 GET 接口,都通过 token 参数鉴权。先检查外层业务 code=0;视频搜索结果还包含一层平台响应,脚本也会检查这一层的状态,再读取列表。
第一步:搜索视频并保存 BVID
调用 /api/bilibili/search-video/v2,传入 keyword=Python 入门、page=1 和 order=general。接口还提供 click、pubdate、dm、stow 等排序值,具体含义见视频搜索 V2 文档。
当前公开样例的视频列表在 data.data.result,每条记录的 bvid 用于查询详情。示例只读取一页,并按 BVID 去重后选两个视频。若扩展分页,可以递增 page,结合平台返回的页数与空列表设置停止条件,同时限制总页数。
搜索结果可能包含格式化标题。保存原值之后再生成清洗后的标题,避免误把格式标记当成实际视频内容。
第二步:通过详情关联 AID 和分 P 的 CID
把 BVID 传给视频详情 V2,路径为 /api/bilibili/get-video-detail/v2。当前公开样例的主要视频对象在 data.videoData。
bvid:用于本次视频详情请求的视频标识。data.videoData.aid:视频的 AID。data.videoData.pages:分 P 列表,各条目包含自己的cid、序号和分 P 名称。
字幕属于具体分 P。多 P 视频要使用你选择的那一项 cid,不能把 AID 当成 CID,也不要从响应里的相关推荐视频中取 ID。本例使用 pages 第一项,因此并未覆盖同一视频的所有分 P。
第三步:获取字幕轨道,再下载字幕资源
调用视频字幕 V2,路径为 /api/bilibili/get-video-caption/v2,同时传入 bvid、aid 和 cid。
当前公开响应样例在 data.data 中返回字幕轨道,轨道包含 lan、lan_doc 和 subtitle_url。接口返回的是语言与资源链接,需要再请求资源地址才能尝试得到字幕内容。
资源链接可能带有效期或访问条件。获取到了链接,不代表下载一定成功。示例把无轨道、下载失败、空文本和成功下载分别记录,避免把一次 HTTP 403 误判成视频没有字幕。下载时不向资源服务器发送 Just One API Token。
下方代码只接受 HTTPS 的 hdslb.com 及其子域资源,不自动跟随重定向;下载 JSON 后,检查其是否包含 body 数组及字符串 content 字段,再拼接为文本。实际资源结构不符合这一格式时会记录失败,保留原始 API 结果供排查,而不是猜测字段。
Python 示例:保存视频资料和可用字幕
在 macOS 或 Linux 终端安装依赖并设置 Token,把占位符换成自己的值。
python3 -m pip install requests
export JUSTONEAPI_TOKEN='your_token'保存为 bilibili_sample.py,运行 python3 bilibili_sample.py。最多一次搜索、两个视频的详情与字幕轨道请求,共 5 次 API 请求,另外最多两次字幕资源下载。脚本只处理每个视频的第一个分 P 和第一个字幕轨道。
import json
import os
from datetime import datetime, timezone
from pathlib import Path
import requests
BASE_URL = "https://api.justoneapi.com"
TOKEN = os.environ.get("JUSTONEAPI_TOKEN", "")
def call_api(path, **params):
try:
response = requests.get(
BASE_URL + path,
params={"token": TOKEN, **params},
timeout=120, allow_redirects=False,
)
except requests.RequestException:
raise RuntimeError("API network error or timeout") from None
if response.status_code != 200:
raise RuntimeError(f"API HTTP {response.status_code}")
try:
result = response.json()
except ValueError:
raise RuntimeError("API response is not JSON") from None
if not isinstance(result, dict) or result.get("code") != 0:
code = result.get("code") if isinstance(result, dict) else None
raise RuntimeError(f"API failed: code={code}; check the docs")
data = result.get("data")
if not isinstance(data, dict):
raise RuntimeError("Unexpected data shape; inspect the docs")
inner = data.get("base_resp")
if isinstance(inner, dict) and inner.get("status_code") not in (0, "0"):
raise RuntimeError("Platform response was unsuccessful")
return result
def save_json(folder, name, value):
(folder / name).write_text(
json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8"
)
def require_list(data, key):
value = data.get(key)
if not isinstance(value, list):
raise RuntimeError(f"Expected a list at {key}; inspect saved JSON")
return value
def output_folder(topic):
if not TOKEN or TOKEN == "your_token":
raise RuntimeError("Set JUSTONEAPI_TOKEN before running")
run_id = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
folder = Path(topic + "-output") / run_id
folder.mkdir(parents=True, exist_ok=False)
return folder
from urllib.parse import urlparse
KEYWORD = "Python 入门"
MAX_VIDEOS = 2
def download_subtitle(url):
if url.startswith("//"):
url = "https:" + url
parsed = urlparse(url)
host = parsed.hostname or ""
if (parsed.scheme != "https" or parsed.username or parsed.password
or parsed.port not in (None, 443)
or not (host == "hdslb.com" or host.endswith(".hdslb.com"))):
raise RuntimeError("Subtitle host needs manual review")
try:
response = requests.get(url, timeout=30, allow_redirects=False)
except requests.RequestException:
raise RuntimeError("Subtitle download failed") from None
if response.status_code != 200:
raise RuntimeError(f"Subtitle HTTP {response.status_code}")
try:
payload = response.json()
except ValueError:
raise RuntimeError("Subtitle response is not JSON") from None
if not isinstance(payload, dict):
raise RuntimeError("Unexpected subtitle document")
segments = require_list(payload, "body")
if any(not isinstance(s, dict) or not isinstance(s.get("content"), str)
for s in segments):
raise RuntimeError("Unexpected subtitle segment")
return payload, "\n".join(s["content"] for s in segments)
def main():
folder = output_folder("bilibili")
result = call_api(
"/api/bilibili/search-video/v2", keyword=KEYWORD,
page="1", order="general",
)
save_json(folder, "search-1.json", result)
search = result["data"]
if search.get("code") != 0 or not isinstance(search.get("data"), dict):
raise RuntimeError("Video search was unsuccessful")
rows = require_list(search["data"], "result")
ids, seen = [], set()
for row in rows:
bvid = row.get("bvid") if isinstance(row, dict) else None
if bvid and str(bvid) not in seen:
seen.add(str(bvid))
ids.append(str(bvid))
selected = ids[:MAX_VIDEOS]
save_json(folder, "selected-videos.json", selected)
outcomes = []
for index, bvid in enumerate(selected, start=1):
detail = call_api("/api/bilibili/get-video-detail/v2", bvid=bvid)
save_json(folder, f"video-{index}-detail.json", detail)
video = detail["data"].get("videoData")
if not isinstance(video, dict) or not video.get("aid"):
raise RuntimeError("Missing videoData/AID; inspect saved JSON")
pages = require_list(video, "pages")
if not pages or not isinstance(pages[0], dict) or not pages[0].get("cid"):
raise RuntimeError("Missing segment CID; inspect saved JSON")
cid = str(pages[0]["cid"])
captions = call_api(
"/api/bilibili/get-video-caption/v2",
bvid=bvid, aid=str(video["aid"]), cid=cid,
)
save_json(folder, f"video-{index}-captions.json", captions)
tracks = require_list(captions["data"], "data")
if not tracks:
outcomes.append({"bvid": bvid, "cid": cid, "status": "no_tracks"})
continue
track = tracks[0]
url = track.get("subtitle_url") if isinstance(track, dict) else None
if not isinstance(url, str) or not url:
raise RuntimeError("Missing subtitle URL; inspect saved JSON")
outcome = {"bvid": bvid, "cid": cid, "language": track.get("lan")}
try:
payload, text = download_subtitle(url)
except (RuntimeError, ValueError) as error:
outcome.update(status="download_failed", error=str(error))
else:
save_json(folder, f"video-{index}-subtitle.json", payload)
(folder / f"video-{index}-subtitle.txt").write_text(text, encoding="utf-8")
outcome["status"] = "downloaded" if text else "empty_text"
outcomes.append(outcome)
save_json(folder, "subtitle-outcomes.json", outcomes)
print(f"Saved {len(selected)} video samples to {folder}")
if __name__ == "__main__":
try:
main()
except RuntimeError as error:
raise SystemExit(str(error)) from None用输出状态判断下一步
结果写入 bilibili-output 下的独立时间目录。subtitle-outcomes.json 汇总每个视频的字幕处理结果:
downloaded:已下载并保存非空文本,可查看对应的video-*-subtitle.txt。empty_text:资源结构被识别,但拼接后的文本为空。no_tracks:当次字幕接口返回空轨道列表。download_failed:有轨道但资源下载或解析未完成,应结合错误信息与原始响应排查。
成功时还会保存完整字幕 JSON,便于保留时间片段等信息;详情与字幕轨道 API 响应也单独保存。资源下载失败可以先核对链接时效与访问条件,再决定是否重新获取轨道,避免对过期地址无限重试。
把字幕交给 AI 整理
取得非空文本后,可以按段落或时间片段提交给 AI,要求总结主题并保留引用位置。建议使用这样的提示词:
请仅根据提供的字幕整理本视频的主要知识点。
列出每个知识点对应的原文片段;若提供了时间信息,也保留时间位置。
字幕没有说明的内容标为“字幕未说明”,不要自行补充。自动字幕可能包含识别错误,视频画面中的信息也可能未出现在字幕里。重要结论仍需回看原视频。本文脚本只负责采集和保存,没有调用额外 AI 服务。
常见问题
可以获取没有字幕的视频文本吗?
本流程读取现有字幕轨道,不执行语音转写。空轨道响应不能直接生成文本;如果需要转写,应另行确认可用音频及相应处理流程。
多 P 和多语言怎么处理?
遍历详情的 pages,用每一项自己的 CID 请求字幕,再按 lan 选择需要的语言。扩大范围时,请重新计算接口调用和资源下载数量,而不是假设一次请求覆盖整个合集。
获取到字幕链接为什么还会失败?
API 查询与字幕资源访问是两个独立步骤。保存两者的状态,检查时效、访问条件和响应格式,再按需重试。通用 API 错误处理可参考接口失败后的处理策略。
从视频搜索文档找到一个熟悉的视频,核对它的分 P 与字幕语言,再运行完整示例。
继续使用 Just One API
登录 Dashboard 获取 Token,查看完整 API 文档,或打开 MCP GitHub 项目了解最新配置。