
简介面向B站用户打造的一款自动化视频下载与处理工具基于Python语言并整合yt-dlp、ffmpeg两大开源库能够以AV号、BV号或直接URL为入口实现视频批量下载、高清解析、弹幕抽取及多格式转换。资源定位清晰适用于视频学习、素材整理、弹幕数据分析等场景对熟悉Python或希望自动化处理B站视频的用户尤其友好。压缩包共7个文件体积仅40KB包含Python核心脚本、txt与md说明文档、docx附加说明以及License、.gitignore等配置脚本负责下载与处理流程文档辅助环境搭建和参数配置轻量且结构清晰。已有326人浏览学习说明该工具有一定实用价值。通过该包可获得可直接运行的下载脚本、弹幕获取与转码思路、完整使用说明以及附赠文档帮助读者快速搭建自己的B站视频自动下载管线。1. B站视频下载从yt-dlp命令行到Python自动化拿着一个AV号或BV号想把一个视频连封面带弹幕一起存到本地B站官方客户端不给你这个选项网页端嗅探又经常拿到一堆分片文件。这正是yt-dlp这类下载器存在的意义它把B站的播放地址解析、音视频流分离、格式封装这些脏活全部包好你只需要告诉它“下载哪个视频、要什么画质、存成什么格式”。加上ffmpeg做后处理B站视频的下载和处理就能变成一条稳定、可重复的流水线。这篇文章要讲的是我在处理B站视频时总结的一套方法用Python封装yt-dlp的调用批量解析BV号和AV号拉取弹幕同时解决多P视频、字幕、封面和高画质格式这些落地问题。标题里提到的“多格式支持”和“高清视频解析”在实际操作中主要落在yt-dlp的格式选择参数和ffmpeg的封装命令上。适合谁看想批量备份B站视频的UP主投稿、想把弹幕和视频一起存档的收藏爱好者以及要给团队做内部视频抓取工具而没有现成方案的开发者。2. 为什么选yt-dlp而不是B站API直连解析逻辑和选型理由2.1 B站视频地址的解析链路从BV号到CDN分片B站网页端的播放地址不是写死的你打开一个视频页面浏览器先在后台请求一个接口拿到当前清晰度对应的视频流地址和音频流地址然后再用播放器拉流。早期B站允许直接拿到完整的mp4文件现在高清视频基本拆成了视频轨和音频轨两路流下载后需要合并这一步是ffmpeg介入的第一个理由。yt-dlp这类下载器做的事情就是把“请求页面 - 解析接口 - 拿流地址 - 下载音视频 - 封装合并”整条链路自动化。它内部有专门的B站提取器能够识别BV号、AV号、URL链接也可以从用户主页或收藏夹批量展开。这个提取逻辑不是我需要重写的直接用就好。真正需要自己动手的是处理播放列表、选择清晰度、给文件命名以及把弹幕从XML或protobuf转成我们能看的格式。这里有一个常见的误区有人觉得B站API直连更可控非要自己去调接口拿数据。真实情况是B站的接口有风控逻辑请求频率一高就可能触发验证而yt-dlp积累了很长时间的适配逻辑踩坑的概率远小于自己从零开始。我一般只在需要拿“视频标题、UP主信息”这类元数据时才单独请求接口下载本体始终交给yt-dlp。2.2 安装yt-dlp和ffmpeg版本搭配和二进制路径先说环境。yt-dlp是纯Python项目支持Python 3.8以上版本但B站的高画质格式解析依赖较新的yt-dlp版本所以安装时不要用系统自带的旧包直接用pip升级到最新版本即可。ffmpeg则需要单独下载可执行文件Windows下解压后将bin目录加入系统PATHLinux直接用包管理器安装。# 在Python虚拟环境中安装yt-dlp python3 -m venv bili_env source bili_env/bin/activate pip install -U yt-dlp # 检查yt-dlp版本确保能够解析B站最新格式 yt-dlp --version # Linux下安装ffmpeg apt install -y ffmpeg # 验证ffmpeg可用 ffmpeg -version这段命令里虚拟环境是第一步和系统Python隔离的好处是不会把依赖装乱。yt-dlp升级很频繁有的版本只活一两个月就因为B站改接口而失效所以我的习惯是在每次批处理前先执行一遍pip install -U yt-dlp确保用的是最新支持代码。另外注意ffmpeg是独立于yt-dlp的进程yt-dlp在下载完成后会去PATH里找ffmpeg来合并音视频流。如果找不到它会自动退化为只下载单独的视频流低画质还好高清视频就会得到一个只有画面没有声音的文件这是新手最容易翻车的地方。我一般会在脚本里加上路径检测避免这种问题。2.3 Python调用yt-dlp的最小闭包模拟浏览器请求和Cookie配置B站对未登录用户开放的是480P清晰度高清视频需要登录账号才能解析这一点在yt-dlp的命令行里体现为需要给下载器传入登录凭据。处理思路有两种一是用yt-dlp的--cookies-from-browser参数直接读取本地浏览器的登录态二是把Cookie保存为Netscape格式后传给脚本。第一种方便但依赖浏览器第二种适合放到服务器上跑。import yt_dlp ydl_opts { format: bv*ba/b, merge_output_format: mp4, outtmpl: %(title)s.%(ext)s, cookiesfrombrowser: chrome, # 读取Chrome浏览器中的B站登录态 quiet: False, } url https://www.bilibili.com/video/BV1xx411c7mD with yt_dlp.YoutubeDL(ydl_opts) as ydl: info ydl.extract_info(url, downloadTrue)这段代码是一个最小闭包不需要维护登录状态就能下载高清视频前提是电脑里装了Chrome并且登录过B站。format: bv*ba/b是yt-dlp的格式选择语法bv*表示最好的视频流ba表示最好的音频流中间的代表合并最后面的/b表示如果分离流失败则退化为单一格式。参数outtmpl控制输出文件的命名模板B站返回的标题有时会包含特殊字符做好转义能避免Windows下的命名报错。3. 批量下载B站视频BV号与AV号的统一入口和任务队列3.1 用URL批量输入代替手工拼接从收藏夹到文本文件的转换批量下载场景一般有这么几种把某个UP主的主页全部视频拉下来、把某个收藏夹的视频存到本地、给一个包含几十个BV号的文本文件批量解析。这三种场景中第一种和第二种yt-dlp都已经支持从URL直接展开你只需要把主页地址或收藏夹地址传进去。第三种需要我们自己写一个读取文件、循环下载的脚本。“支持AV号BV号URL多种”这个需求点实际上y-dlp内部已经做了归一化它可以把AV号转换成BV号也可以接受带参数跳转的短链接。我的做法是写一个通用的任务解析函数把各种输入形式统一成标准URL列表再交给下载队列处理。import re def parse_bilibili_input(text): 把用户输入统一转换成可下载的URL列表 text text.strip() # 支持直接粘贴完整URL if text.startswith(https://www.bilibili.com/video/): return [text] # 支持BV号补全URL bv_match re.search(rBV[0-9A-Za-z], text) if bv_match: return [fhttps://www.bilibili.com/video/{bv_match.group()}] # 支持AV号转成BV后补全 av_match re.search(rav([0-9]), text, re.IGNORECASE) if av_match: return [fhttps://www.bilibili.com/video/av{av_match.group(1)}] raise ValueError(f无法识别的输入: {text})这个函数的逻辑很简单但很实用用户不管是从网页复制链接、手动输入BV号、还是拿一个旧的AV号粘贴进来最后都能拿到一个可下载的标准URL。正则匹配中BV号的规则是固定的字母数字组合需要注意大小写都要支持。AV号转BV号这一步yt-dlp在内部会处理不需要我们自己去查转换表。3.2 多P视频的展开分P选择和批量命名规则B站视频分为单P和多P多P视频在URL里显示的是一整个合集但实际视频流是分成多个片段。yt-dlp默认会把多P全部下载下来文件的命名规则需要专门设计不然就会出现下载完一堆文件却看不出顺序的问题。我一般会在outtmpl里加入集数占位符例如%(playlist_index)02d_%(title)s.%(ext)s这样文件名会带上01_、02_这样的序号前缀。这里有第二个坑B站多P视频的标题往往到P数和P标题为止不包含UP主的名字所以如果同时下载多个UP主的视频建议在文件命名时加入频道信息%(channel)s避免重名覆盖。ydl_opts { format: bv*ba/b, merge_output_format: mp4, outtmpl: %(channel)s/%(playlist_title)s/%(playlist_index)02d_%(title)s.%(ext)s, noplaylist: False, }这里的noplaylist: False让yt-dlp保留展开播放列表的行为也就是多P视频会全部下载。文件路径按频道/合集名/序号_标题的三级结构组织对后续批量处理很友好。整个目录结构是在下载时自动创建的不需要提前手动建文件夹。playlist_index是整数02d格式化后在排序时不会出现第10集排在第二集前面的问题。3.3 断点续传和失败重试批量下载不中断的三个参数批量下载最怕的是中途失败后重头再来一个几十集的合集下载到第20集断掉确实很折磨人。yt-dlp本身就支持断点续传它下载过程中会使用.part临时文件重跑命令时会自动检测并继续未完成的部分。针对失败请求我习惯加三个参数retries表示每个片段的请求重试次数fragment_retries是分片下载的重试次数continuedl确保断点续传开关打开。还有一个更隐蔽的concurrent_fragment_downloads它可以控制分片并发数值大小直接影响带宽占用和下载速度默认1是串行我一般设为4到8B站对分片并发有一定的容忍空间。ydl_opts { format: bv*ba/b, merge_output_format: mp4, retries: 10, # 请求失败后的重试次数 fragment_retries: 10, # 分片下载失败的重试次数 continuedl: True, # 断点续传 concurrent_fragment_downloads: 4, # 分片并发数 ignoreerrors: True, # 单集下载失败时跳过不中断整个队列 }ignoreerrors: True是批量下载的生命线配置某些视频因版权限制或UP主删除导致无法解析如果没有这个参数整个脚本会抛异常终止后面所有的视频都不下载了。有了它失败的条目只会打印一条警告任务队列继续往前走全部结束后我再回头处理失败列表。代价是会掩盖部分真实网络问题所以我一般会在脚本结尾打印失败计数保证至少知道它发生了什么。4. 弹幕获取与多格式支持从XML到ass字幕的处理链路4.1 B站弹幕的三种获取途径和yt-dlp的弹幕参数B站的弹幕文件有两种主流格式XML格式可以直接解析出弹幕内容和发送时间另一种是protobuf格式需要额外的解码库。yt-dlp自带弹幕下载选项官方文档里支持的选项有--write-subs和--write-auto-subs这些命令配合格式化参数使用能够下载B站的外挂字幕。但弹幕的真实情况是yt-dlp目前对B站弹幕的支持还不够完整它有时候能拿到CC字幕但获取所有弹幕需要单独调用B站接口。我常用的方案是视频下载完成后单独请求B站的弹幕接口拉取XML文件再解析需要的部分。B站弹幕接口需要视频的cid参数这个值在yt-dlp返回的视频信息字典里可以拿到。import requests import re def fetch_danmaku_by_cid(cid): 根据视频cid获取XML弹幕文件 url fhttps://comment.bilibili.com/{cid}.xml headers { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Referer: https://www.bilibili.com/ } resp requests.get(url, headersheaders, timeout10) if resp.status_code ! 200: raise RuntimeError(f弹幕获取失败HTTP {resp.status_code}) return resp.content这里的关键是cid和视频ID之间的对应关系。在yt-dlp提取信息后信息字典中有cid字段它是弹幕接口真正需要的参数。如果直接用BV号去请求弹幕B站会返回参数错误。headers里的Referer必须保留这个接口需要校验请求来源否则部分部署会返回403。4.2 弹幕转ASS把滚动弹幕压进视频的ffmpeg滤镜思路如果只是把弹幕存成XML文件很多播放器并不直接支持想在看视频的时候看到弹幕常见的做法是转换成ASS字幕格式再用ffmpeg把ASS字幕烧录进视频画面。我做弹幕转换时用现成的方案把XML的标准字段映射成ASS的Dialogue行时间轴从毫秒转成ASS时间格式同时把弹幕类型映射成不同样式。def xml_time_to_ass(raw_time): 将XML弹幕的时间戳转成ASS字幕格式的时间 seconds int(raw_time) / 1000 hours int(seconds // 3600) minutes int((seconds % 3600) // 60) secs int(seconds % 60) centsecs int((seconds - int(seconds)) * 100) return f{hours:02d}:{minutes:02d}:{secs:02d}.{centsecs:02d}时间戳转换逻辑是整个链路里最容易出错的地方。B站弹幕XML的p属性里第一项是弹幕出现的毫秒时间直接除以1000就能得到秒数转成ASS格式时要注意毫秒位是两位的厘秒制直接保留三位会出问题。转换后用ffmpeg的ass滤镜烧录到视频里可以得到带弹幕的成品文件ffmpeg -i input.mp4 -vf assdanmaku.ass -c:v libx264 -c:a aac output.mp4这个命令属于视频后处理环节通常在全部下载完成后统一执行。-vf assdanmaku.ass滤镜会把字幕直接渲染到画面上输出的是硬字幕播放设备不需要额外安装字库。缺点是烧录过程无法撤销所以我在批量执行前会先在单个视频上做预览。4.3 多格式支持为什么mp4不是唯一答案标题里提到“多格式支持”实际落地时有几层含义一是下载时音视频流的封装格式可控二是输出容器的可选性。B站原视频流的容器有mp4、webm等yt-dlp的merge_output_format参数可以强制输出指定的容器默认是mkv因为mkv同时容纳多种编码的音视频流。但很多用户拿到mp4后更容易直接导入手机和剪辑软件所以我会改成mp4。这里有一个比较隐蔽的坑mp4容器要求视频流必须是H264或者H265编码音频流必须是AAC。如果B站返回的视频流是AV1编码合并成mp4时ffmpeg会直接报错或者输出一个播放器不认的文件。我一般会加上--recode-video mp4参数yt-dlp会用ffmpeg重新转码确保输出兼容性。ydl_opts { format: bv*ba/b, merge_output_format: mp4, # 不允许转码只重新封装 recode_video: None, }recode_video: None的含义是“如果容器和编码匹配直接直播拷贝不转码”速度快画质零损伤。但前提是得保证原始流编码和容器匹配。如果遇到容器匹配失败的情况再把recode_video改成mp4让ffmpeg做一次完整转码代价是cpu占用和画质衰减。我一般在下载参数里把format限定为bv*[vcodec^avc]ba[acodec^aac]先把不兼容的编码过滤掉再考虑容器问题。5. 高清视频解析从480P到4K的格式选择和清晰度锁定5.1 B站清晰度等级与yt-dlp格式标识的对应关系B站对视频清晰度的定义和yt-dlp的格式标识不是直接对应的这一点容易让人困惑。B站页面上标注的360P、480P、720P、1080P、4K等选项在yt-dlp解析出来的formats列表里体现为不同的分辨率、视频编码和带宽组合。同一个分辨率可能对应多个视频流视频编码可能是AVC也可能是HEVC它们的格式ID都不同。搞清楚清晰度等级和格式ID的对应关系需要查看视频的具体格式列表import yt_dlp from pprint import pprint with yt_dlp.YoutubeDL({quiet: True}) as ydl: info ydl.extract_info( https://www.bilibili.com/video/BV1xx411c7mD, downloadFalse ) for fmt in info[formats]: print(fmt.get(format_id), fmt.get(height), fmt.get(vcodec), fmt.get(acodec), fmt.get(tbr))这段代码只打印不下执行结果中第一列是yt-dlp内部的格式ID如30032、30280等第二列是高度后面是视频编码和音频编码。B站4K视频往往会有多个不同编码的流AVC编码兼容性最好但不是所有的4K视频都有AVC版本有时只有HEVC或AV1。这时如果强求mp4容器就得接受转码开销。5.2 用format参数锁定最高清晰度格式选择语法的优先级问题我见过很多人的format参数写成bestvideobestaudio这个写法在多数网站没问题但在B站可能会出现选到一个AV1视频流的情况AV1视频流的播放对CPU要求高而且封装兼容性差。针对B站的实际情况我推荐自己定义格式优先级format_spec ( bv*[height2160][vcodec^avc]ba[acodec^aac]/ bv*[height2160][vcodec^hev1]ba[acodec^aac]/ bv*[height2160]ba/b )这个格式串的结构是三条优先级递减的规则用斜杠隔开。第一条优先找不超过4K的AVC编码视频流找不到就尝试HEVC编码再找不到就允许任意编码。这样锁定的结果是既有最高清晰度又没有让编码格式成为后续封装的隐患。需要说明的是height2160中的2160是像素高度4K视频通常满足这个条件如果你只想下1080P改成height1080即可。5.3 高画质视频的下载速度和稳定性分片并发和限速的平衡点高清视频文件大动辄几十个GB下载稳定性和速度控制比清晰度选择更实际。B站的分片机制是把视频切成一个个几秒的小块然后提示开发者自行下载并拼接。默认的并发数是1到2高速网络下往往跑不满带宽但并发过高又容易遇到B站限流或封IP。ydl_opts { format: format_spec, concurrent_fragment_downloads: 8, http_chunk_size: 10485760, # 10MB一个分片 retries: 30, fragment_retries: 30, }http_chunk_size指定每个HTTP请求拉取的分片大小默认值较小请求次数多频繁握手容易触发风控。调到10MB后请求次数减少对稳定性的帮助很明显。retries和fragment_retries都设为30在高清场景下是合理的因为网络波动一次会毁掉好几个分片重试不够的话会造成文件缺块坏掉。注意这两个参数的单位是“次”不是“倍”具体数值按网络情况来调整。6. 批量下载全流程的避坑记录从Cookie失效到弹幕时间来维权踩坑一Cookie失效导致的高清视频全部失效现象前一天还能下载1080P第二天突然所有的视频都只能拿到480P甚至更低。排查原因后发现Cookie过期cookiesfrombrowser虽然能读取浏览器登录态但浏览器本身可能已经清除了B站的登录会话或者该浏览器没有登录B站。解决在写脚本前先手动打开浏览器访问bilibili.com确认登录状态是正常且保持有效的状态。批量下载数量巨大时建议长期跑脚本用独立的Netscape格式Cookie文件退出浏览器后Cookie文件依然有效也能避免浏览器占用文件锁定导致读取失败。踩坑二弹幕时间戳错位视频和弹幕对不上现象用XML转ASS后烧录到视频里发现弹幕出现的时间比视频大概延迟了或者在某些位置凭空出现提前量。原因多半是我用的时间基准和分析的视频起点不一致B站存在以分段为单位的时间偏移量视频有片头时弹幕时间轴从正片开始。解决弹幕转换脚本只处理正片区间下载时用--download-sections截掉片头区间或者在记录视频分段时把裁掉的偏移量手动补偿回弹幕时间轴里。加上一段偏移补偿逻辑就解决了手动记录下来每一次的分段偏移。踩坑三文件名太长导致Windows路径报错现象某些B站视频标题特别长加上频道名和编号后组合出的文件名超过Windows路径上限脚本在文件创建阶段就抛异常。原因是outtmpl模板组合时没有做长度检查。解决在outtmpl里加一个%(title).50s之类的截断表达式限制标题长度同时把频道名作为目录层级来区分而不是全拼进文件名。B站标题加频道名的组合模式在多级目录结构中出现截断问题用截断表达式做限定后整个流程稳定很多。踩坑四视频有音无画或者有画无音现象下载完成后的mp4只有音频没有画面或者用播放器打开黑屏只有声音。原因是在不同的时间点B站返回的视频轨编码不同有时会返回AV1流部分播放器和剪辑软件根本不支持AV1解码导致画面播放异常。解决在下载参数里强制限定[vcodec^avc]或[vcodec^hev1]把AV1过滤掉。如果确实需要AV1的高压缩率那么用ffmpeg转成H264再封装但要做好CPU占用升高的准备。踩坑五批量下载单集失败导致整个队列中断现象跑了20集到第21集时视频因版权问题下载失败整个脚本直接退出后面30集都没留下记录。原因是批量脚本没有对单集异常做捕获yt-dlp抛出的下载错误直接向上冒。解决在循环外包一层异常捕获配合ignoreerrors: True确保单集失败只记日志循环立刻进入下一集。我还会把每个视频的下载结果写进一个JSON文件跑完后能看到哪些成功哪些失败失败的原因是什么不需要靠人眼去翻终端日志。7. 进阶用法把整套流程封装成一个可复用的Python下载器类到这一步前面的功能已经能解决B站个人用户日常下载需求了但真正把工具变成可持续落地的方案需要把它封装成一个可复用的类不然每次换一批视频就要改一遍脚本。我在实际项目中使用的做法是定义一个BilibiliDownloader类把任务解析、下载器初始化、弹幕获取、异常处理全部收纳进去。import yt_dlp import requests from pathlib import Path class BilibiliDownloader: def __init__(self, output_dirdownloads): self.output_dir Path(output_dir) self.output_dir.mkdir(parentsTrue, exist_okTrue) self.base_opts { format: bv*[height2160][vcodec^avc]ba[acodec^aac]/b, merge_output_format: mp4, outtmpl: str(self.output_dir / %(channel)s / %(title)s.%(ext)s), ignoreerrors: True, retries: 10, fragment_retries: 10, } def download(self, url_or_ids): urls self._normalize_input(url_or_ids) with yt_dlp.YoutubeDL(self.base_opts) as ydl: return ydl.download(urls) def _normalize_input(self, raw_input): # 支持用逗号或换行分隔多个BV号 text raw_input.replace(,, \n).replace(, \n) lines [ln.strip() for ln in text.splitlines() if ln.strip()] return [self._to_url(ln) for ln in lines]这个封装解决了我日常最大的痛点“输入格式混乱”和“下载参数不统一”。调用时不管是传一个URL还是一个文本块内部都会先归一化成URL列表再交给yt-dlp输出目录自动创建单集失败自动跳过。类封装的价值在于把配置集中到一个字典里参数一改所有调用都同步更新不需要每次新建脚本再复制粘贴那一段参数。验证这个工具是否可靠我一般会跑三组测试一组是单人单P视频确认清晰度满足预期一组是合集多P视频确认文件命名和目录结构正确最后一组混合故意失败的链接确认ignoreerrors真的不会中断整个队列。测试通过后再跑大批量就不会有意外惊喜了。这个方案目前在我的日常使用中稳定运行遇到的绝大多数问题都能靠升级yt-dlp版本解决。在下一次的实践中踩坑五的JSON日志记录已经加进了类的实现里每次下载完看一份JSON就知道有没有漏网之鱼不用再对着终端输出逐条去找。希望帮到你。本文还有配套的精品资源点击获取