B站字幕提取全攻略:从手动抓包到Python自动化脚本实现
1. 项目概述:为什么我们需要提取B站字幕?
做视频剪辑、内容学习或者二次创作的朋友,估计都遇到过这个需求:想把B站视频里的字幕单独弄出来。可能是为了做笔记,把精彩的讲解整理成文字;也可能是为了翻译,把中文视频配上外文字幕;又或者,你是个UP主,想借鉴一下同行的文案结构。直接手打?效率太低,而且B站网页和客户端通常不提供直接下载字幕的按钮。这时候,一个能稳定、高效提取字幕的方法就成了刚需。
我最近就因为要做一个知识分享系列,需要批量处理几十个B站科普视频的字幕,手动操作根本不可能,于是把市面上常见的方法都折腾了一遍。从在线的工具网站,到浏览器的开发者工具抓包,再到用Python脚本解析,踩了不少坑,也总结出了一套目前(根据我的测试环境)亲测有效、且可持续性比较好的方案。这个方法的核心,是绕开官方不提供的直接下载接口,通过技术手段找到视频背后真正的字幕数据源(通常是JSON格式),然后将其转换为我们常用的SRT字幕格式。整个过程不需要复杂的爬虫对抗,更多的是对网页结构和数据流的理解。
无论你是完全不懂编程的小白,还是有一定技术基础想了解原理的开发者,这篇文章都会给你一个清晰的路径。我会从最简单的“开箱即用”工具讲起,再到手动获取的详细步骤,最后深入讲解用Python实现自动化的原理和代码,你可以根据自己的情况选择合适的方法。
2. 核心思路与方案选型:字幕数据藏在哪里?
在动手之前,我们得先搞清楚B站字幕的工作原理,这样才能知道从哪里“下手”。B站视频的字幕,无论是UP主上传的,还是AI自动生成的,最终在网页上显示时,都不是以图片形式叠加的,而是通过Web技术动态加载和渲染的文本。这些文本数据,必然要从B站的服务器传输到你的浏览器。
2.1 字幕数据的常见来源
通过分析B站的页面加载过程,字幕数据主要来自以下几个可能的接口:
- 视频信息接口 (
view接口):当你打开一个B站视频页面时,浏览器会请求一个包含视频所有元数据的接口,其中可能内嵌了字幕列表信息,包括字幕ID和语言。 - 专用字幕接口 (
subtitle接口):更常见的是,页面会使用视频的cid(内容ID)或bvid(视频ID)去单独请求一个字幕文件。这个接口返回的数据,就是我们最终需要的核心——一个结构化的JSON对象。 - 播放器配置信息:有时字幕信息也会在播放器的初始化配置数据中。
对于我们提取者来说,最稳定、最直接的目标就是那个返回结构化JSON数据的subtitle接口。我们的核心任务就是:找到这个接口的请求地址,获取到JSON数据,然后将其解析、转换成SRT格式。
2.2 几种实现方案的利弊分析
知道了目标,接下来看看有哪些路可以走:
| 方案 | 原理 | 优点 | 缺点 | 适用人群 |
|---|---|---|---|---|
| 在线工具/浏览器插件 | 工具背后集成了抓取和转换逻辑,用户只需输入视频链接。 | 最简单,几乎零门槛,点点鼠标就行。 | 1. 有失效风险(依赖工具作者的维护)。 2. 可能存在广告、收费或次数限制。 3. 无法批量处理,隐私数据需上传到第三方服务器。 | 临时、单次提取,且对技术无要求的用户。 |
| 手动抓包(开发者工具) | 在浏览器中打开开发者工具(F12),监控网络请求,手动找到并下载字幕JSON文件,再用工具转换。 | 完全免费,可控性强,能最直观地理解数据获取过程。不依赖第三方服务。 | 步骤稍多,需要一点耐心和学习成本。每个视频都需要重复操作。 | 想了解原理、偶尔提取字幕,且不排斥动手的技术爱好者。 |
| Python脚本自动化 | 编写脚本,自动模拟请求、解析视频页、获取cid、请求字幕接口、处理JSON并生成SRT。 | 一次性投入,永久受益。可批量处理,效率极高。高度自定义,可集成到其他工作流中。 | 需要基本的Python编程环境,和理解代码的能力。 | 需要频繁或批量提取字幕的用户,以及开发者。 |
注意:无论哪种方案,其有效性都建立在B站前端接口未发生重大变更的基础上。如果某天发现方法失效,大概率是接口地址或参数格式变了,届时需要重新分析网络请求。
我个人从手动抓包入门,最终转向了Python自动化,因为批量需求是实实在在的痛点。下面,我就从手动抓包这个承上启下的方法开始详细讲解,它能帮你透彻理解整个过程,为后续的脚本编写打下坚实基础。
3. 手动抓包提取法:一步步找到字幕JSON
这个方法不需要安装特殊软件,只需要你电脑上有一个现代浏览器(Chrome、Edge、Firefox等)。其核心就是利用浏览器自带的“开发者工具”来监听网页的所有网络请求,从中筛选出我们需要的字幕数据。
3.1 详细操作步骤
步骤一:打开目标视频并启动开发者工具
- 用浏览器打开你想提取字幕的B站视频页面。
- 按下键盘上的F12键(或右键点击页面,选择“检查”),打开开发者工具。
- 切换到“网络” (Network)标签页。为了更清晰地捕捉数据,建议先点击标签页上的红色圆形按钮(或按Ctrl+E)清空当前的记录,然后勾选上“保留日志” (Preserve log)。
步骤二:触发字幕加载并寻找请求
- 在视频播放器下方,找到字幕选择按钮(通常是一个“字幕”或“CC”图标)。如果视频有字幕(包括AI生成),点击它,并选择一种语言(如“中文(自动生成)”)。这个操作会触发浏览器向B站服务器请求具体的字幕数据。
- 此时,你的“网络”标签页会刷出一系列新的请求。我们需要在这些请求中找到字幕文件。
步骤三:筛选并定位字幕接口
- 在“网络”标签页的筛选栏(Filter)中,输入关键词如
subtitle、caption或json。这能快速过滤出可能的请求。 - 仔细浏览筛选后的请求列表。目标请求通常具有以下特征:
- 名称 (Name):可能包含
subtitle、caption等字样。 - 类型 (Type):通常是
fetch或xhr。 - 地址 (URL):URL中很可能包含
subtitle或api.bilibili.com等路径。一个常见的模式是:https://api.bilibili.com/x/player/v2?cid=xxx&aid=xxx之类的,但更直接的是https://api.bilibili.com/x/player/wbi/v2/subtitle?...这种。
- 名称 (Name):可能包含
- 点击这个疑似请求,在右侧面板中查看“预览” (Preview)或“响应” (Response)标签页。如果你看到了一个结构清晰的JSON数据,里面包含
body字段,而body里是一段段带有from(开始时间)、to(结束时间)、content(字幕内容)的数组,那么恭喜,你找到了!
步骤四:下载并保存JSON数据
- 在找到的正确请求上右键点击,选择“复制” -> “复制链接地址” (Copy -> Copy link address)或“复制为cURL”。但更简单的方式是直接查看响应内容。
- 在“响应” (Response)标签页,你应该能看到完整的JSON文本。全选(Ctrl+A)、复制(Ctrl+C)这段文本。
- 打开一个文本编辑器(如记事本、VS Code、Sublime Text),将内容粘贴进去,保存为一个
.json文件,例如subtitle.json。
步骤五:将JSON转换为SRT格式现在你有了原始数据,但它不是通用的字幕格式。我们需要将其转换为.srt格式,这种格式能被绝大多数播放器和剪辑软件识别。
- 使用在线转换工具(推荐给新手):搜索“JSON to SRT converter”能找到很多在线网站。将你保存的
subtitle.json文件内容粘贴到输入框,或者直接上传文件,网站会自动转换并允许你下载.srt文件。务必注意隐私,避免上传敏感视频的字幕。 - 使用本地脚本(更安全可控):你可以使用一个简单的Python脚本来转换。假设你的JSON结构里,字幕列表在
data['body']里,每个条目有from(秒)、to(秒)、content字段。下面是一个极简的转换脚本示例:
运行这个脚本(需要安装Python),就能在本地生成import json # 1. 读取你保存的JSON文件 with open('subtitle.json', 'r', encoding='utf-8') as f: data = json.load(f) # 2. 假设字幕数据在 data['body'] 中,请根据你实际抓取的数据结构调整路径 # 例如,有时可能在 data['data']['body'] 或 data['subtitle']['body'] subtitles = data.get('body', []) # 如果不在body,尝试打印data.keys()看看结构 # print(data.keys()) # 3. 转换并写入SRT文件 with open('output.srt', 'w', encoding='utf-8') as srt_file: for i, sub in enumerate(subtitles, start=1): start_time = sub['from'] # 单位:秒 end_time = sub['to'] # 单位:秒 content = sub['content'] # 将秒转换为SRT时间格式:HH:MM:SS,mmm def sec_to_srt_time(sec): hrs = int(sec // 3600) mins = int((sec % 3600) // 60) secs = int(sec % 60) msec = int((sec - int(sec)) * 1000) return f"{hrs:02d}:{mins:02d}:{secs:02d},{msec:03d}" srt_start = sec_to_srt_time(start_time) srt_end = sec_to_srt_time(end_time) # 写入SRT块 srt_file.write(f"{i}\n") srt_file.write(f"{srt_start} --> {srt_end}\n") srt_file.write(f"{content}\n\n")output.srt文件。
3.2 实操心得与常见问题
- 找不到
subtitle请求?首先确认视频确实有字幕(包括AI字幕)。然后,在清空网络日志后,尝试刷新页面,或者切换一下字幕语言,确保动作被记录。有时接口名可能不包含“subtitle”,尝试过滤“json”或查看所有XHR请求,通过预览内容来判断。 - JSON结构看不懂?在开发者工具的“预览”窗格,JSON数据通常是折叠的。你可以点击展开,观察数据结构。核心是找到包含时间戳和文本内容的数组。如果上述脚本的
data['body']路径不对,就在脚本里添加print(json.dumps(data, indent=2, ensure_ascii=False))来漂亮地打印整个JSON,然后找到正确的路径。 - 时间格式不对?确认B站返回的时间单位是秒(通常是小数)。上述转换函数能正确处理。如果发现字幕不同步,检查一下时间戳计算是否正确。
- “保留日志”很重要:如果不勾选,页面刷新或导航时,之前的请求记录会被清空,你可能就抓不到加载字幕的那个请求了。
手动方法虽然每一步都很清晰,但处理多个视频时非常繁琐。接下来,我们就进入自动化阶段,用Python脚本一键搞定所有流程。
4. Python自动化脚本实现原理与代码解析
当你需要处理几十上百个视频时,手动抓包就成了体力活。编写一个Python脚本,让程序自动完成“获取视频信息->定位字幕接口->下载并转换”的全流程,是最高效的解决方案。这里,我将拆解一个相对稳定且考虑了常见问题的脚本。
4.1 环境准备与依赖库
首先,确保你的电脑安装了Python 3.6及以上版本。我们需要安装几个关键的库:
requests:用于发送HTTP请求,获取网页和API数据。json:Python标准库,用于解析JSON数据。re:正则表达式库,用于从网页源码中提取关键ID。
安装命令非常简单,在命令行中执行:
pip install requests4.2 脚本核心步骤拆解
一个健壮的自动化脚本应该包含以下模块:
1. 获取视频的bvid和cidB站视频有两个重要ID:bvid(如BV1xx411c7mh)是视频的唯一标识,出现在网址中;cid是分P(章节)的标识,一个bvid可能对应多个cid(多P视频)。字幕接口通常需要cid。 我们可以通过解析视频页面HTML,或者调用B站的公开API来获取这些ID。从页面源码提取是一种直接且不易过时的方法。
2. 请求字幕列表接口,获取字幕ID有了cid,我们可以请求一个字幕列表接口,获取该视频所有可用字幕的信息,包括每种语言的字幕ID(subtitle_id)。
3. 使用字幕ID请求具体的字幕JSON数据用上一步得到的subtitle_id,去请求最终的字幕内容接口,拿到包含时间轴和文本的JSON数据。
4. 解析JSON并转换为SRT格式这部分逻辑与手动方法中的转换脚本类似,将获取到的JSON数据解析,并按照SRT格式写入文件。
4.3 完整脚本示例与逐行解读
下面是一个整合了以上步骤,并加入了错误处理和简单重试机制的Python脚本示例。请将其保存为bilibili_subtitle_downloader.py。
import requests import json import re import time from urllib.parse import urlparse def get_bvid_and_cid(url): """ 从B站视频URL中提取bvid,并通过API获取cid。 """ # 解析URL,提取bvid (BV号) parsed = urlparse(url) path = parsed.path bvid_match = re.search(r'(BV[0-9A-Za-z]{10})', path) if not bvid_match: raise ValueError("无法从URL中提取有效的BV号。") bvid = bvid_match.group(0) print(f"提取到BVID: {bvid}") # 使用B站API,通过bvid获取视频信息(包含cid) # 这个API相对稳定,但未来可能变化 info_api_url = f"https://api.bilibili.com/x/web-interface/view?bvid={bvid}" headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 'Referer': 'https://www.bilibili.com' } try: resp = requests.get(info_api_url, headers=headers, timeout=10) resp.raise_for_status() # 检查HTTP错误 info_data = resp.json() if info_data['code'] != 0: raise Exception(f"API返回错误: {info_data.get('message')}") # 获取第一个分P的cid(通常是最主要的) cid = info_data['data']['cid'] title = info_data['data']['title'] print(f"获取到CID: {cid}, 视频标题: {title}") return bvid, cid, title except requests.exceptions.RequestException as e: print(f"请求视频信息失败: {e}") # 备用方案:尝试从旧版页面源码中查找cid(稳定性较差) print("尝试备用方案从页面源码提取...") page_resp = requests.get(url, headers=headers, timeout=10) cid_match = re.search(r'"cid":(\d+)', page_resp.text) if cid_match: cid = cid_match.group(1) print(f"从页面源码提取到CID: {cid}") return bvid, cid, "Unknown Title" else: raise Exception("无法获取视频CID,请检查链接或网络。") def get_subtitle_list(bvid, cid): """ 获取视频的字幕列表,返回字幕信息。 """ subtitle_list_api = f"https://api.bilibili.com/x/player/v2?bvid={bvid}&cid={cid}" headers = { 'User-Agent': 'Mozilla/5.0 ...', # 同上 'Referer': f'https://www.bilibili.com/video/{bvid}' } resp = requests.get(subtitle_list_api, headers=headers) data = resp.json() if data['code'] == 0 and data['data']['subtitle']['subtitles']: subtitles_info = data['data']['subtitle']['subtitles'] print(f"找到 {len(subtitles_info)} 个字幕轨道。") for idx, sub in enumerate(subtitles_info): print(f" [{idx}] 语言: {sub['lan_doc']} (ID: {sub['id']})") return subtitles_info else: print("未找到字幕列表,或视频无字幕。") return [] def download_subtitle_json(subtitle_id, cid, bvid): """ 根据字幕ID下载具体的字幕JSON数据。 """ # 这个接口地址和参数可能会变化,这是当前可用的模式之一 subtitle_api_url = f"https://api.bilibili.com/x/player/wbi/v2/subtitle?subtitle_id={subtitle_id}" # 注意:新接口可能需要额外的签名参数(wbi签名),上述简单接口可能不稳定。 # 更稳定的方法是使用web接口,它通常不需要复杂签名。 # 备用接口:https://api.bilibili.com/x/player/wbi/v2/subtitle/web?subtitle_id=... # 如果上述接口失效,请按第3节方法抓包获取最新的有效接口。 headers = { 'User-Agent': 'Mozilla/5.0 ...', 'Referer': f'https://www.bilibili.com/video/{bvid}' } # 添加常见参数,有些接口需要 params = { 'subtitle_id': subtitle_id, 'cid': cid, 'bvid': bvid } resp = requests.get(subtitle_api_url, headers=headers, params=params) data = resp.json() # 打印一下数据结构,方便调试 # print(json.dumps(data, indent=2, ensure_ascii=False)) if data['code'] == 0: # 字幕正文通常在 data['data']['body'] 里 body = data['data']['body'] return body else: print(f"下载字幕失败,响应: {data}") return None def json_to_srt(subtitle_body, output_filename='output.srt'): """ 将字幕JSON body转换为SRT格式文件。 """ with open(output_filename, 'w', encoding='utf-8-sig') as f: # utf-8-sig 解决某些播放器乱码 for i, item in enumerate(subtitle_body, start=1): from_time = item['from'] to_time = item['to'] content = item['content'].strip() # 转换时间格式 def sec_to_srt(t): hours = int(t // 3600) minutes = int((t % 3600) // 60) seconds = int(t % 60) milliseconds = int((t - int(t)) * 1000) return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}" start_srt = sec_to_srt(from_time) end_srt = sec_to_srt(to_time) f.write(f"{i}\n") f.write(f"{start_srt} --> {end_srt}\n") f.write(f"{content}\n\n") print(f"字幕已保存为: {output_filename}") def main(): video_url = input("请输入B站视频链接: ").strip() try: # 1. 获取基础ID bvid, cid, title = get_bvid_and_cid(video_url) # 2. 获取字幕列表 subtitle_list = get_subtitle_list(bvid, cid) if not subtitle_list: print("程序退出。") return # 3. 让用户选择(这里默认选第一个,通常是中文) choice = 0 if len(subtitle_list) > 1: try: choice = int(input(f"请输入要下载的字幕编号 (0-{len(subtitle_list)-1}),默认[0]: ") or "0") except ValueError: choice = 0 selected_sub = subtitle_list[choice] print(f"正在下载字幕: {selected_sub['lan_doc']}...") # 4. 下载字幕JSON subtitle_body = download_subtitle_json(selected_sub['id'], cid, bvid) if subtitle_body: # 5. 转换为SRT safe_title = re.sub(r'[\\/*?:"<>|]', "_", title) # 清理文件名非法字符 output_file = f"{safe_title}_{selected_sub['lan_doc']}.srt" json_to_srt(subtitle_body, output_file) else: print("字幕内容为空或下载失败。") except Exception as e: print(f"程序运行出错: {e}") # 建议用户使用手动抓包法作为后备 print("\n自动脚本可能因接口更新而失效。") print("建议使用文章第3部分介绍的‘手动抓包法’进行提取,该方法更底层,适应性更强。") if __name__ == '__main__': main()4.4 脚本使用与自定义说明
- 运行脚本:在命令行中,进入脚本所在目录,运行
python bilibili_subtitle_downloader.py。根据提示输入视频链接即可。 - 接口失效处理:脚本中最脆弱的环节是
download_subtitle_json函数中的API地址。如果B站更新了接口,这里可能会返回错误。此时的最佳策略是回归到“手动抓包法”,用开发者工具抓取到最新的、有效的字幕接口URL和必要的参数(如wbi签名参数),然后替换脚本中的subtitle_api_url和params。这是自动化脚本维护的常态。 - 批量处理:你可以修改
main函数,从一个文本文件中读取多个视频链接,用循环遍历处理,实现批量下载。 - 错误处理:脚本中包含了基本的网络请求超时和JSON解析错误处理。对于生产环境,你可能需要增加重试机制和更详细的日志记录。
5. 常见问题排查与进阶技巧
即使按照步骤操作,你也可能会遇到一些棘手的情况。这里我汇总了一些常见问题及其解决方案。
5.1 问题排查速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 手动抓包时找不到任何字幕相关请求 | 1. 视频本身无字幕(包括AI字幕)。 2. 字幕已内嵌到视频流中(较少见)。 3. 网络请求被过滤或未触发。 | 1. 确认视频播放器有字幕开关且已开启。 2. 清空网络日志,刷新页面,重新点击字幕按钮。 3. 在Network筛选栏尝试搜索“json”或“subtitle”,并检查所有XHR请求。 |
Python脚本报错,无法获取cid或bvid | 1. 视频链接格式不正确。 2. B站API接口发生变化。 3. 网络问题或请求被限制。 | 1. 确保链接是标准的B站视频页链接(含BV号)。 2. 使用手动抓包法,从页面源码中搜索 "cid":来验证当前结构。3. 在脚本中增加 print(resp.text)打印原始响应,分析数据结构。 |
| 脚本能获取列表但下载字幕JSON失败 | 字幕内容接口URL或参数已更新。 | 这是最常见的问题。立即使用手动抓包法,抓取最新的有效请求,更新脚本中的subtitle_api_url和请求参数(特别是注意是否有w_rid和wts这类签名参数)。 |
| 转换后的SRT文件时间轴错乱 | 1. 时间戳单位不是秒。 2. JSON中的时间戳格式有误。 3. 转换函数计算错误。 | 1. 检查原始JSON数据中from和to字段的值,确认是秒(如 123.456)。2. 调试转换函数 sec_to_srt,打印中间值核对。 |
| SRT文件在播放器中显示乱码 | 文件编码问题。 | 确保保存SRT文件时使用UTF-8编码,特别是Windows记事本默认可能是ANSI。在Python中,使用encoding='utf-8-sig'打开文件可以添加BOM头,兼容更多软件。 |
| 批量处理时被IP限制或封禁 | 请求频率过高,触发了B站的反爬机制。 | 1. 在请求间增加随机延时(如time.sleep(random.uniform(1, 3)))。2. 使用代理IP池(复杂度较高)。 3. 降低处理速度,模拟人工操作。 |
5.2 进阶技巧与注意事项
- 优先选择“AI生成字幕”:对于没有UP主上传字幕的视频,B站提供的“AI生成字幕”准确率已经相当高,是提取的主要来源。在抓包或脚本中,它通常对应
lan_doc为“中文(自动生成)”的轨道。 - 处理多P视频:一个
bvid可能对应多个cid(分集)。上述脚本默认获取第一个cid。如果你需要所有分集字幕,需要先从视频信息API的data['pages']字段中获取所有分P的cid列表,然后循环处理。 - 字幕翻译:获取到SRT后,你可以利用翻译API(如Google Translate, DeepL等)或本地翻译工具,快速生成外文字幕文件,这对于内容出海非常有用。
- 法律与道德边界:提取字幕用于个人学习、笔记、无障碍访问是合理的。但请务必尊重UP主的著作权,不要将提取的字幕用于商业用途、重新分发或制作盗版视频。技术是中立的,但使用技术的人需要负责任。
- 保持更新:网页技术日新月异,本文介绍的方法在2024年中测试有效。如果未来失效,核心思路(抓包找接口)不会变,只是具体的接口地址和参数需要你通过开发者工具重新抓取分析。掌握这个“渔”比拿到现成的“鱼”更重要。
这个从手动到自动的完整流程,不仅解决了B站字幕提取的问题,更提供了一套应对类似网页数据抓取需求的通用思路:观察 -> 定位 -> 获取 -> 解析 -> 转换。