简介:这份资源是一套基于Python的B站视频批量下载与处理工具,整合了yt-dlp和ffmpeg两大库,面向需要下载高清视频、提取弹幕或进行多格式转换的B站用户,尤其适合视频学习、素材整理与内容分析场景。工具支持AV号、BV号和URL三种输入方式,可自动批量下载并解析高画质视频,还能抓取弹幕数据,便于后续做评论趋势或热度分析;同时提供MP4、MKV等多格式输出,兼顾不同设备播放需求。压缩包共7个文件,包含1个Python主脚本、2个文本说明、1个Word文档、1个License、1个gitignore及1个Markdown文件,整体仅40KB,轻量小巧,便于快速部署和修改。目前已有326人学习下载,适合有一定Python基础、希望自动化处理B站视频的初学者或进阶用户。
1. 从一条命令说起:这个工具到底在解决 B 站用户的什么痛点
在 B 站下载视频,很多人第一反应是找在线解析站,但解析站不是限速就是失效,更别说批量下载和弹幕存档。这个项目标题其实把 B 站自动化下载的完整链路说清楚了:用 Python 做调度,用 yt-dlp 解析并拉流,用 ffmpeg 做后处理,一次解决批量下载、弹幕获取、多格式支持和高清解析。我第一次跑通时最意外的不是下载速度,而是意识到 B 站的高清视频其实是音频和视频分两条流传输的,合并这一步绕不开 ffmpeg。这篇文章适合想自建下载工具的人:做剪辑、离线存课、批量备份,或者想把弹幕一起留档。
2. B 站视频的下载机制与工具链选型:为什么非 yt-dlp 和 ffmpeg 不可
2.1 高清视频的真相:音画分离与分片传输
B 站从多清晰度时代开始就采用 DASH 动态流——视频和音频拆成两条独立的流,视频轨又按清晰度拆成多个分片,播放器拿到播放凭证后逐片请求。所以你在网页里看到的 1080P,本质上是"1080P 视频轨 + 对应音频轨"的组合。
这个结构带来两个直接结果。其一,直接拿浏览器里的网络请求去下载一个 mp4 链接,往往只有画面没有声音,或者只有声音和低清画面;其二,想要最高清晰度,必须同时拿到两条流再合并。任何声称"直接解析出完整 mp4"的方案,要么是绕过了 DASH 用兼容格式(牺牲清晰度),要么是后台悄悄帮你调用了 ffmpeg。
这也是为什么工具链里 ffmpeg 是刚需而不是可选项。yt-dlp 只负责把音视频轨拉下来,后半程的合并、转封装、字幕合成全靠 ffmpeg。我之前有一阵子图省事没装 ffmpeg,结果所有批量任务都在最后一步失败,纯属给自己挖坑。
2.2 yt-dlp 的价值:签名、防盗链和格式协商
下载器层面,核心工作不是"请求 URL 拿文件",而是先拿到播放凭证。B 站的播放接口做了一套签名机制和防盗链校验,而且对请求频率非常敏感,频率一高直接返回 412 状态码。yt-dlp 把这些脏活全部封装了,这也是我最终放弃手写 requests 方案的原因。
它对 B 站的解析流程大致是:把 BV 号/AV 号/URL 归一化成视频 ID → 请求预览接口拿清晰度清单 → 按用户给的格式规则选出最佳音视频轨 → 带正确的请求头分片下载。格式协商这部分是可编程的,这也是后面所有批量、多格式操作的基础。
拿它和浏览器直接下载、在线解析站对比,yt-dlp 的优势不是单文件下载快,而是可预测、可脚本化、可重试。解析站今天能用明天可能就挂,自己维护脚本才能长期用,而且出问题你知道去哪看日志。
2.3 ffmpeg 在后处理里的具体分工
ffmpeg 在整套流程里承担四件事:音视频合并、格式转换、字幕烧录和封装、音频提取。以封装 mp4 为例,yt-dlp 默认把视频轨和音频轨逐段下到临时目录,再由 ffmpeg 封装合并,合并失败通常不是 ffmpeg 坏了,而是音频轨下载不完整。
选型上有一个常见误区:觉得 yt-dlp 输出的临时文件可以直接给播放器播放。实际上合并前的文件是分离的音视频流,只有经过 ffmpeg 合并才是完整文件。理解了这层关系,后面调试也会容易很多。
| 角色 | 职责 | 不可替代的理由 |
|---|---|---|
| Python | 调度、批量、解析输入 | 把机械操作变成可重复的流程 |
| yt-dlp | 解析接口、签名、下载 | 持续维护签名和防爬逻辑 |
| ffmpeg | 合并、转换、封装 | 音视频后处理的事实标准 |
3. 最小复现:搭好环境并用一条命令跑通单视频下载
3.1 环境准备:Python、yt-dlp 与 ffmpeg 的安装与路径
先准备一个 Python 3.9 以上的环境,然后安装 yt-dlp。
python -m pip install -U yt-dlp # 验证安装 yt-dlp --version参数说明:-U表示升级到最新版本,yt-dlp 迭代很快,老版本解析 B 站接口经常失效;--version确认命令可用,也方便后面排查问题时核对版本。
ffmpeg 建议按系统级方式安装:
# macOS brew install ffmpeg # Debian/Ubuntu sudo apt install ffmpeg # Windows 可以用自带包管理器安装,或手动下载解压后把 bin 目录加入 PATH判断是否装好,直接跑ffmpeg -version。我在 Windows 上踩过这个坑:装完 yt-dlp 直接跑,报ffmpeg not found,因为在 Windows 上 yt-dlp 需要在 PATH 里能找到 ffmpeg.exe,光有文件不配路径等于没装。
注意:yt-dlp 不会帮你安装 ffmpeg,它只负责调用。合并步骤报错时,第一先确认 ffmpeg 命令本身可用。
3.2 第一条下载命令:从 URL 到完整视频
yt-dlp "https://www.bilibili.com/video/BVxxxxxxxxx" -f "bv*+ba/b" -o "%(title)s.%(ext)s" --merge-output-format mp4 --embed-metadata这个命令的核心是-f "bv*+ba/b":bv*是带完整帧率的视频轨,ba是唯一音频轨,加号表示两者合并,斜杠/表示如果合并失败就退化到单文件流。-o指定文件命名为标题加扩展名;--merge-output-format mp4把封装格式锁定为 mp4;--embed-metadata把视频标题、UP 主信息写进文件元数据。
为什么不直接-f best?因为 B 站很多视频的best命中的是 480P 单文件流,拿不到真正的最高清晰度。想确认当前视频有哪些可选的清晰度,先列出格式清单:
yt-dlp -F "URL"输出里能看到 format_id、分辨率、编码格式。记下想要的格式 id,再写进-f,这是高清解析排错的第一步。比如列表里有个137是 1080P 视频轨、140是音频轨,就可以写-f 137+140。
3.3 指定清晰度与格式优先级规则
格式选择是这套工具里最值得花时间调的部分,不同场景对应不同写法:
| 场景 | -f 写法 | 说明 |
|---|---|---|
| 最高画质 | bv*+ba/b | 高清视频轨加音频轨 |
| 限制 1080P | bv*[height<=1080]+ba | 按高度过滤视频轨 |
| 只要 mp4 | bv*+ba/b | 合并后封装成 mp4 |
| 只要音频 | ba | 配合--extract-audio转码 |
bv*[height<=1080]这种写法是 yt-dlp 的格式过滤语法,height字段代表视频高度,想要 720P 就把数字改成 720。过滤条件加得越细,越不容易在批量下载时拿到超预期的大文件。
B 站的高清流(1080P 及以上)有些需要登录 cookie 才会在格式列表里返回,否则-F只看得到低清晰度。这属于权限问题,我在第 5 章展开讲。
4. 批量下载与多来源归一化:AV 号、BV 号与 URL 的统一处理
4.1 先用 Python 造一个批量调度脚本
单条命令跑通之后,批量只是把命令套进循环。
import subprocess import sys URLS_FILE = "urls.txt" def run_download(url: str) -> None: cmd = [ "yt-dlp", url, "-f", "bv*+ba/b", "--merge-output-format", "mp4", "-o", "%(title).80s [%(id)s].%(ext)s", "--retries", "5", "--continue", "--no-overwrites", "--embed-metadata", ] proc = subprocess.run(cmd, capture_output=True, text=True) if proc.returncode != 0: print(f"[失败] {url}\n{proc.stderr[-800:]}") else: print(f"[完成] {url}") def main(): with open(URLS_FILE, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] for line in lines: run_download(line) if __name__ == "__main__": main()逻辑说明:逐行读取urls.txt,每行一个输入;subprocess.run调 yt-dlp 并捕获输出;失败时打印 stderr 最后 800 个字符,方便定位是权限问题、格式问题还是网络问题。
参数说明:--continue开启断点续传,--no-overwrites表示已存在的文件不重复下载,--retries 5让单文件失败时重试最多 5 次。-o里加了%(id)s,文件名带视频 ID,不同 UP 主视频重名时不会互相覆盖。
批量场景里最容易忽略的是输入格式。urls.txt 每行只放一个原始输入,不要用分号或逗号分隔,也不要带多余空格。
4.2 AV 号、BV 号与 URL 的归一化
用户给的输入不一定是完整 URL,可能是裸的 BV 号,也可能是一段带分 P 参数的链接。写一个归一化函数:
import re def normalize_bilibili_input(raw: str) -> str: raw = raw.strip() if raw.startswith(("http://", "https://")): return raw if re.match(r"^(BV[0-9A-Za-z]+|av\d+)$", raw, re.IGNORECASE): return f"https://www.bilibili.com/video/{raw}" raise ValueError(f"无法识别的输入: {raw}")逻辑说明:带协议的直接返回原样;BV 号或 av 号开头的小写也统一转成标准视频链接。批量脚本里先过这个函数再交给下载器,能少传错格式。
B 站现在主推 BV 号,av 号是老编号,两种在 yt-dlp 里都能直接识别,但 Python 层归一化可以让日志更干净。另外要注意,URL 里带?p=2这种分 P 参数不要去掉,那是多 P 视频的定位参数,去掉会默认下第一 P。
4.3 多格式支持:mp4、mkv、音频提取与封装差异
多格式支持的思路是三层分离:-f决定拿哪些源流,--merge-output-format决定封装容器,--recode-video才决定要不要重新编码。
# 封装为 mkv,适合多轨和字幕外挂 yt-dlp "URL" -f "bv*+ba" --merge-output-format mkv # 只抽音频,转成 m4a yt-dlp "URL" -f "ba" -o "%(title)s.m4a" # 强制重新编码成标准 mp4 yt-dlp "URL" -f "bv*+ba" --recode-video mp4说明:mkv 容器对多音轨、多字幕轨支持更好,mp4 兼容性更强。--recode-video是真正做视频重新编码,耗时且吃 CPU,日常使用应该优先用--merge-output-format做流拷贝封装,只有遇到播放器不认的编码时才考虑重编码。
B 站普通视频没有外挂字幕流,官方的 CC 字幕和弹幕都不是直接嵌在媒体流里的,需要单独获取。弹幕获取的具体做法在第 6 章。
4.4 断点续传与已下载清单
批量下载跑一半断网或脚本崩溃是常态,archive 文件能避免重头再来:
yt-dlp "URL" --download-archive archive.txt说明:archive.txt会记录每个已下载视频的 ID,第二次跑同一个清单时自动跳过已完成的条目。跑几百个视频的存量备份时,这个参数能省大量时间和流量,而且脚本重启不会造成重复下载。
配合 Python 脚本使用时,把--download-archive加进 cmd 列表即可。要注意 archive 文件是所有任务共享一本账还是每个任务独立一本,取决于你的使用场景,我一般按批量任务主题各建一个,方便统计哪些还没下完。
5. B 站批量下载避坑:四个高频问题的排查方法
5.1 下载中途卡住或返回 412
现象:单文件下到一半进度条不动,或者直接报HTTP Error 412: Precondition Failed。
原因:412 是 B 站风控的典型状态码。请求频率太快,或者同一 IP 同时并发下载多个视频,会被判定为脚本访问。视频分片请求会被短暂封禁,过一段时间自动恢复。
解决:一是降并发和限速,二是把浏览器 cookie 传给 yt-dlp。
yt-dlp "URL" --cookies-from-browser chrome --limit-rate 5M --sleep-interval 3说明:--cookies-from-browser chrome读取本地浏览器的登录 cookie,用真实登录态绕过匿名风控;--limit-rate 5M限制单文件下载速度;--sleep-interval 3在任务之间加 3 秒延迟。批量脚本里我一般把并发控制在 2 以内,再配合随机延迟,基本能避开 412。
5.2 下载完成只有画面没有声音
现象:下载产物是一个无声 mp4,或者播放时音画不同步。
原因:DASH 流下只拉了视频轨,音频轨漏掉了。常见于-f只写了bv*或者bestvideo而没有加音频轨,又或者 ffmpeg 没装导致合并步骤被跳过。
解决:改用-f "bv*+ba/b",并确认 ffmpeg 可用。如果文件已经下载完,发现音轨缺失,可以用 ffmpeg 手动合并:
ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4说明:-c copy是流拷贝,不重新编码,速度快。如果两个流时长不一致,需要加-shortest参数,不过大多数情况是网络丢片导致音频轨不完整,重新下载音频轨比强行对齐更可靠。
5.3 高清视频提示需要登录或返回 403
现象:-F列出的格式最高只有 480P,指定 1080P 时报 403 Forbidden。
原因:B 站对高清流做了登录态校验,部分 UP 主也设置了仅登录可看,甚至还有仅会员可看的内容。
解决:导出浏览器 cookies 后加入下载命令。
yt-dlp "URL" --cookies-from-browser chrome注意:--cookies-from-browser依赖浏览器本身的存储格式,如果浏览器有独立加密存储导致读取失败,可以手动导出 cookies 文件再换成--cookies "cookies.txt"。这个参数只是把请求身份换成你的账号,下载行为本身不受影响。
5.4 文件名过长或包含非法字符导致保存失败
现象:Windows 下报文件创建失败,或者文件名乱码。
原因:标题里包含: * ? " < > |等 Windows 非法字符,或者标题超过 255 字符。B 站有些视频标题会带各种符号,批量下载时一个文件失败往往会让整批任务中断。
解决:
yt-dlp "URL" -o "%(title).80s [%(id)s].%(ext)s" --restrict-filenames说明:%(title).80s把标题截断到 80 字符,--restrict-filenames把空格和特殊符号替换为下划线。批量任务我建议永远加这两个参数,不然遇到一个标题全是特殊符号的视频,整批任务就断了。
6. 弹幕获取与合成:把 XML 弹幕变成可播放的字幕
6.1 从视频信息里拿 cid 并请求弹幕 XML
弹幕不是媒体流的一部分,需要单独请求弹幕接口。第一步先用 yt-dlp 拿视频的元数据:
yt-dlp --dump-json "URL"输出的 JSON 里有id和cid字段,cid是弹幕接口需要的编号。拿到 cid 后请求弹幕接口:
import requests cid = "视频的cid" url = f"https://comment.bilibili.com/{cid}.xml" headers = {"Referer": "https://www.bilibili.com"} r = requests.get(url, headers=headers) r.encoding = "utf-8" with open(f"{cid}.xml", "w", encoding="utf-8") as f: f.write(r.text)说明:弹幕接口现在有 protobuf 新格式,老 xml 接口仍然可用,但弹幕量特别大的视频,单次请求只会返回最新的一部分弹幕。日常存档场景 xml 够用,要做完整弹幕备份需要按时间分段请求,这里不展开。
6.2 XML 弹幕转换 ASS 并合并进视频
B 站弹幕 XML 里每条弹幕在<d>标签的 p 属性携带时间、字号、颜色等字段。简单转成 ASS 字幕:
import re def fmt_time(seconds: float) -> str: h = int(seconds // 3600) m = int(seconds % 3600 // 60) s = seconds % 60 return f"{h}:{m:02d}:{s:05.2f}" def danmaku_xml_to_ass(xml_text: str) -> str: header = """[Script Info] PlayResX: 1920 PlayResY: 1080 [V4+ Styles] Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BackColour, Bold, Outline, Shadow, Alignment, MarginL, MarginR, MarginV Style: Default, Microsoft YaHei, 48, &H00FFFFFF, &H00000000, &H80000000, 0, 2, 1, 2, 30, 30, 40 [Events] Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text """ events = [] for d in re.finditer(r'<d p="([^"]+)">(.*?)</d>', xml_text, re.S): fields, text = d.group(1).split(","), d.group(2) appear_time = float(fields[0]) start = fmt_time(appear_time) end = fmt_time(appear_time + 4.0) events.append(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{text}") return header + "\n".join(events)逻辑说明:p 属性的第 1 个字段是弹幕出现时间,单位秒;弹幕默认停留 4 秒,所以结束时间加 4;fmt_time把秒转成 ASS 要求的时间格式。转换后文本里如果有 XML 转义符,需要先做反转义再写入。
ASS 生成后用 ffmpeg 烧录进视频:
ffmpeg -i video.mp4 -vf ass=danmaku.ass -c:v libx264 -c:a copy output.mp4说明:ass=danmaku.ass是字幕滤镜,-c:v libx264重新编码视频轨,-c:a copy音频直接拷贝。烧录后弹幕成为画面一部分,发到其他平台也能看到;不想要烧录,也可以把 ass 封装进 mkv 容器,播放器里自己开关字幕。个人习惯是批量任务里优先下载原始视频留档,弹幕单独存 XML 一份,再按需生成烧录版本——这样视频本体和弹幕都能独立归档。希望帮到你。
本文还有配套的精品资源,点击获取