Python实现B站充电视频下载器:接口解析与ffmpeg合并实战
发布时间:2026/10/2 13:07:49来源:尧图网络
写这个B站充电视频下载器起因其实挺直白的我给几位UP主充过电有几期充电专属视频确实质量高想存一份离线看但搜索一圈下来不是让你注册来路不明的解析网站就是让你装一堆看不懂的软件。作为一个习惯用Python解决问题的开发者我干脆自己动手写了一个小脚本登录自己的账号带上Cookie去请求B站的官方接口解析出视频流地址下载后用ffmpeg合并成MP4。今天的文章就把整条链路完整拆开讲一遍从接口原理到代码实现再到各种报错排查适合有一定Python基础、想研究B站接口或视频下载原理的同学参考。先说清楚这个脚本只能用于下载你自己有权限访问的视频仅供学习交流请勿商用或传播下载内容。1. 项目整体思路先搞懂B站充电视频为什么不能“一键下载”在动手写代码之前得先弄明白一个事儿同样是在网页上播放为什么普通B站视频有很多现成工具能一键下载充电专属视频却少见好用的下载器这背后其实是一套权限模型在起作用。1.1 充电专属视频的权限模型B站的内容权限大体分几层公开视频、大会员专享、付费课程以及今天要聊的充电专属。充电专属是UP主在开启“充电计划”后可以发布的一种仅对充电用户开放的视频。你在网页端能正常播放是因为登录状态下的Cookie带有你的身份信息B站判定你有这个权限。但权限判定不只是“看一眼Cookie”这么简单。B站的播放地址接口在返回真实视频流地址时会校验三个东西是否登录有没有有效的SESSDATA、是否购买过充电身份是否在允许列表里、以及当前请求的头信息是否合规。换句话说视频文件本身并没有做复杂的加密它就是普通的m4s分片流和公开视频的技术形态一样。区别在于获取这个流的地址之前服务器会先做一次严格的权限检查。这也就是为什么市面上的解析网站不太愿意碰充电视频接口返回的是动态签名地址还经常带防盗链参数如果请求头里少了Referer或者User-Agent地址即使拿到了也下载不回来。自己做下载器核心就是把这些校验条件原样模拟出来。1.2 为什么不直接用现成工具而要用Python自己写你可能会问BBDown、you-get、yutto这些开源工具难道不支持吗其实这些工具本身很优秀但有几个现实问题。第一充电视频接口的参数细节更新比较频繁通用型工具往往优先保证公开视频和大会员视频的稳定性充电这类长尾场景经常是“能看不能下”或“解析失败”。第二这些工具为了兼容性会封装很多层逻辑出了问题你很难判断是Cookie的锅、接口的锅还是网络的锅。自己写脚本的好处是链路短一个请求拿元信息一个请求拿播放地址下载合并全程心里有数。第三写这个项目的过程本身就是一次很好的接口调试训练你对HTTP请求、防盗链、Cookie鉴权这些概念的理解会完全不一样。技术选型上也不需要多复杂requests负责HTTP请求ffmpeg负责把分离的音视频流合并成MP4全程不到200行代码够轻量也够用。2. 开工前准备Python环境、Cookie获取与权限自检代码不难但准备工作做不好后面全是坑。这一章把环境搭建和Cookie获取讲细一点尤其是Cookie这部分很多人就是栽在这里。2.1 搭建可复现的Python环境建议直接用Python 3.8以上的版本我在3.10和3.11上都实测过没有兼容性问题。依赖非常少只需要requestspip install requests如果你还在用Python 2.x那别想了赶紧换。另外建议装一个ffmpeg后续合并音视频流要用。Windows用户可以到ffmpeg官网下载静态编译版解压后把bin目录加到系统PATH里macOS用户直接brew install ffmpegLinux用户用apt install ffmpeg或yum install ffmpeg就行。验证ffmpeg是否安装成功在终端输入ffmpeg -version能看到版本号就说明没问题。这一步不要省略很多人代码都跑通了最后卡在合并这一步才发现没装ffmpeg很尴尬。2.2 三种拿Cookie的方式和推荐做法Cookie是整个下载器的“门票”。先说结论推荐用手动复制的方式简单可靠不需要额外写代码。方式一浏览器开发者工具手动复制用Chrome或Edge打开B站登录你的账号找一个你能正常播放的充电视频页面。按F12打开开发者工具切到Network网络面板刷新页面然后在筛选框里输入“api”。随便点开一个名为view或playurl的请求在右侧Headers面板里找到Request Headers区域往下翻就能看到Cookie字段。右键点击Cookie那行选择Copy Value把完整内容保存到一个cookie.txt文件里。这份Cookie里的关键字段是SESSDATAB站的登录凭证有效期一般是半年左右。只要这个字段没过期配合其他字段一起用就没问题。注意完整Cookie是身份凭证泄露给任何人等同于把账号交给对方自己保管好。方式二用脚本自动读取浏览器Cookie如果你不想手动复制可以用browser_cookie3这个库它能从浏览器本地数据库中直接读取Cookie。但有一个问题读取Chrome的Cookie需要本机解密而且新版Chrome对Cookie文件的加密策略越来越严经常出现读不出来或者报错的情况。实测下来在Windows上成功率尚可macOS上有时候会读不全最终还是要手动补。所以这个方式适合做自动化但不推荐作为首选。方式三Selenium模拟登录用Selenium驱动浏览器自动完成扫码登录再自动提取Cookie写起来也不复杂。但这属于“杀鸡用牛刀”了Selenium依赖浏览器驱动版本一升级就崩维护成本比项目本身还高。除非你需要批量处理大量Cookie否则不建议用。综合下来手动复制Cookie最简单十秒钟搞定还不会引入额外的依赖。Cookie保存格式我习惯用纯文本SESSDATAxxxxx; bili_jctyyyyy; DedeUserIDzzzzz代码里读到这段字符串后用分号切分成字典即可。2.3 动手前先验证自己的观看权限这个步骤很多人忽略但非常重要。在写代码之前先确认你要下载的视频在网页端用当前账号能正常播放。如果你根本没有给这个UP主充电或者充电已经过期那后面的接口必然返回权限不足代码写得再对也没用。验证方法很简单浏览器登录账号打开视频页面能正常播放就是有权限。这里要特别提醒B站对充电专属视频的权限校验是跟着账号走的不是跟着链接走。你把视频链接发给别人对方打不开不代表链接有问题而是权限校验没过。所以脚本里也不要加任何“绕过权限”的逻辑没权限就老老实实返回错误这是底线。3. 核心代码实现从解析到合并的一整套流程这部分是全文的重头戏。我会把完整流程拆成四个步骤拿视频元信息、拿播放地址、下载音视频流、合并封装。每一步都讲清楚接口参数的含义和代码意图。3.1 用view接口拿到cid和标题B站视频的标识符有两套体系av号avid和BV号bvid现在网页端URL里基本是BV号。但播放器真正需要的是cid它代表视频分P的唯一ID。要从BV号拿到cid需要调用view接口import requests import json import time COOKIE_FILE cookie.txt def load_cookie_from_file(path): with open(path, r, encodingutf-8) as f: cookie_str f.read().strip() return parse_cookie_string(cookie_str) def parse_cookie_string(cookie_str): cookies {} for item in cookie_str.split(;): item item.strip() if in item: key, value item.split(, 1) cookies[key] value return cookies def get_headers(): return { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36, Referer: https://www.bilibili.com/, Origin: https://www.bilibili.com } def get_video_info(bvid, cookies): url https://api.bilibili.com/x/web-interface/view params {bvid: bvid} headers get_headers() resp requests.get(url, paramsparams, headersheaders, cookiescookies, timeout10) data resp.json() if data[code] ! 0: raise RuntimeError(f获取视频信息失败, code{data[code]}, message{data[message]}) info data[data] return { title: info[title], cid: info[cid], bvid: info[bvid], }这里有几个细节值得展开。请求头里的Referer一定要带B站很多接口会校验Referer缺失或错误的值可能导致返回正常但后续取流失败。User-Agent也要用浏览器标准的UA别用Python默认的否则很容易触发风控。Cookie通过cookies参数传入requests库后会自动拼接到请求头里不用手动写。view接口返回的JSON结构里data.cid就是我们要的对于多P视频可以通过data.pages数组循环获取每一P的cid。大部分充电视频是单P这里先按单P处理。3.2 用playurl接口拿到真实的音视频流拿到cid之后接下来调用播放地址接口。B站现在的主流格式是DASH流也就是视频和音频分开传输视频是纯视频轨m4s音频是纯音频轨m4s播放器在网页端临时合成。下载器要做的就是分别拿到这两个流然后手动合并。def get_play_url(bvid, cid, cookies): url https://api.bilibili.com/x/player/playurl params { bvid: bvid, cid: cid, fnval: 4048, fnver: 0, fourk: 1, } headers get_headers() resp requests.get(url, paramsparams, headersheaders, cookiescookies, timeout10) data resp.json() if data[code] ! 0: raise RuntimeError(f获取播放地址失败, code{data[code]}, message{data[message]}) return data[data]fnval这个参数很多人第一次见会懵它是个位掩码用来告诉服务器客户端支持什么格式。4048这个值表示允许返回DASH格式且支持杜比音效。如果fnval填0服务器会返回老的durl格式也就是单个MP4或FLV文件对下载器来说反而更好处理但很多新视频已经不支持老格式了所以还是优先走DASH。拿到data之后判断里面有没有dash字段。DASH结构长这样dash data.get(dash) if dash: video_list dash.get(video, []) audio_list dash.get(audio, []) video_list.sort(keylambda x: x[id], reverseTrue) audio_list.sort(keylambda x: x[id], reverseTrue) video_url video_list[0][baseUrl] audio_url audio_list[0][baseUrl]video_list里的每个元素代表一个清晰度id越大清晰度越高。常见的id映射关系如下id含义1278K超高清126杜比视界125HDR真彩1204K超清1161080P601121080P801080P64720P32480P16360P默认会选最高分辨率也就是video_list排序后的第一个。如果你的账号没有对应的清晰度权限接口返回的列表里就不会出现那个id所以不需要担心下面选一个你没权限的。audio_list同理id越大代表码率越高但注意部分音频如杜比全景声需要额外权限这里同样以接口实际返回为准。还有一点baseUrl有时会被服务器拒绝Return Code 403。遇到这种情况可以尝试backupUrl备用地址B站一般会提供一到两个备用节点。取备用地址的逻辑video_url video_list[0][baseUrl] backup_url video_list[0].get(backupUrl) if backup_url: video_url backup_url[0]下载视频流时Referer头是必须带的而且必须是https://www.bilibili.com否则服务器会认为这是盗链请求直接拒绝。这一点在下载环节尤其重要。3.3 下载m4s流并用ffmpeg合并成MP4拿到音视频流地址后剩下的就是把文件下载到本地。这里用requests的stream模式分块写入避免一次性加载大文件到内存。def download_file(url, filepath, cookies): headers get_headers() resp requests.get(url, headersheaders, cookiescookies, streamTrue, timeout30) resp.raise_for_status() total_size int(resp.headers.get(content-length, 0)) downloaded 0 with open(filepath, wb) as f: for chunk in resp.iter_content(chunk_size1024 * 1024): if not chunk: continue f.write(chunk) downloaded len(chunk) print(f\r已下载 {downloaded} / {total_size} bytes, end) print() return filepath关于下载线程数我实测下来单线程就够用。B站单个视频流的文件本来就不大一个5分钟的视频视频轨大概30~60MB音频轨大概10~20MB单线程满速下载也就十几秒。你不要一上来就开十几个线程去并发拉流那样会让请求频率瞬间升高非常容易被风控封IP或者封Cookie。真要提速最多用两个线程分别下视频轨和音频轨。下载完成后本地会有两个文件video.m4s和audio.m4s。注意这个.m4s扩展名是B站自己起的它实际是ISO BMFF格式ffmpeg可以直接识别。合并命令ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4-c copy的意思是直接复制流不重新编码所以速度很快也不会损失画质。有人说要加-bsf:a aac_adtstoasc实测不需要B站返回的音频流本身就带ADTS头直接拷贝就能正常播放加了反而可能出错。合并过程可以用subprocess调用import subprocess def merge_media(video_path, audio_path, output_path): cmd [ ffmpeg, -i, video_path, -i, audio_path, -c, copy, -y, output_path ] result subprocess.run(cmd, capture_outputTrue, textTrue) if result.returncode ! 0: raise RuntimeError(fffmpeg合并失败: {result.stderr[-500:]}) print(f合并完成: {output_path})-y参数表示如果输出文件已存在就覆盖避免交互式询问导致脚本卡死。3.4 组合成完整脚本一运行就自动出片把上面几个函数串起来就是一个完整可运行的下载器。def main(): cookies load_cookie_from_file(COOKIE_FILE) bvid input(请输入BV号: ).strip() info get_video_info(bvid, cookies) print(f视频标题: {info[title]}) play_data get_play_url(info[bvid], info[cid], cookies) dash play_data.get(dash) if not dash: raise RuntimeError(该视频不支持DASH格式暂无法下载) video_list sorted(dash[video], keylambda x: x[id], reverseTrue) audio_list sorted(dash[audio], keylambda x: x[id], reverseTrue) video_url video_list[0][baseUrl] audio_url audio_list[0][baseUrl] video_file f{info[title]}.video.m4s audio_file f{info[title]}.audio.m4s output_file f{info[title]}.mp4 print(开始下载视频轨...) download_file(video_url, video_file, cookies) print(开始下载音频轨...) download_file(audio_url, audio_file, cookies) print(开始合并...) merge_media(video_file, audio_file, output_file) os.remove(video_file) os.remove(audio_file) print(下载完成) if __name__ __main__: main()运行流程就是输入BV号脚本自动解析标题和cid拉取播放地址下载音视频轨ffmpeg合并最后清理临时文件。整个流程大概就是一杯咖啡的时间。4. 实战中遇到的坑状态码、防盗链与Cookie失效写这个项目的过程中我踩了不少坑也帮群友排查过一些报错。把常见的集中整理一下方便你对照排查。4.1 403 / 412 / 权限不足怎么排查这应该是遇到最多的问题表现形式各不相同但原因基本在三类。第一类是Cookie无效或者过期。SESSDATA过期是家常便饭尤其是有段时间没登录B站会自动踢掉旧的会话。排查方法很简单浏览器重新登录一次再复制一份新Cookie换到cookie.txt里问题通常就解决了。第二类是请求头不完整。主要是Referer和User-Agent很多人在解析阶段没问题一到下载就403十有八九是请求视频流的时候没带Referer。加一行headers就解决。还有UA要用浏览器UA别用默认的python-requests。第三类是权限本身不够。比如你只有普通登录没有给UP主充电或者充电已过期又或者视频设置了“仅限充电且满N月”的解锁条件。这种情况接口返回的错误码通常是-403或-104提示信息也比较直白。我的建议是遇到这种错误就干脆放弃下载别去研究绕过方案没必要。返回码含义常见场景0成功一切正常-101未登录Cookie为空或SESSDATA已失效-111CSRF校验失败缺少bili_jct或过期-403访问权限不足未购买充电/充电过期/需要大会员-404资源不存在BV号错误或视频被删除-412请求被拦截请求频率过高或IP风控4.2 下载成功但无法播放的常见原因有一种情况很折磨人日志显示全部下载成功合并也完成但生成的MP4打开后黑屏、没声音或者提示文件损坏。我排查下来最常见的有三个原因。第一个原因是只下载了视频轨没有下载音频轨或者音频轨下载失败后没有正确重试导致合并时音轨缺失或文件不完整。解决办法是下载后先检查文件大小如果视频轨只有几百KB基本就是下载失败了重新下载。第二个原因是合并时用了错误的参数。有些教程让加-bsf:a aac_adtstoasc但实测对B站的音频流并不需要。我遇到过加了反而导致音频文件损坏的情况去掉这个参数重新跑一次就好了。如果你追求原汁原味用-c copy就对了。第三个原因是你本地播放器不支持某些编码格式。B站的高码率视频轨可能是HEVCH.265编码Windows自带的播放器和老版本PotPlayer默认可能不支持。碰到这种情况优先用VLC或者MPC-BE这类播放器尝试播放也可以直接用ffprobe查看视频轨编码格式确认一下。4.3 B站风控与访问频率的控制经验只要涉及登录Cookie就绕不开风控的话题。我在测试的时候遇到过两次412一次是短时间内反复调用playurl接口另一次是同时跑了多个下载脚本。B站对登录态下的异常行为是有监控的尤其是短时间内高频请求很容易触发临时封禁。控制手段其实很简单在关键请求之间加sleep比如两次请求之间间隔1~2秒time.sleep(1.5)下载的时候单线程稳扎稳打别用多线程并发拉同一个接口。实测单线程下一个视频从解析到下载完成请求次数也就四五次完全在正常范围内基本不会触发风控。另外有一点要特别注意不要把Cookie硬编码在代码里再丢到GitHub上。哪怕你仓库是私有的也强烈不建议。之前有人就是这样把账号Cookie泄露了被拿去刷奇怪的弹幕。Cookie从本地文件读取或者从环境变量读取都是为了安全。代码里我已经改成从cookie.txt读取自己用的时候也要小心保管这个文件。5. 必须强调的边界与后续扩展工具做出来了能跑通但有些话还是得说在前面。这个项目从定位上就是一个学习型项目不是生产工具。5.1 仅供学习交流这几个“不要”要记牢第一不要下载你没有权限访问的视频。所谓“没有权限”包括但不限于不是你账号购买的充电视频、充电已过期的视频、需要大会员但你只登录了普通账号的视频。脚本本身也不应该被用来“突破”任何限制接口返回什么错误就接受什么错误。第二不要二次传播下载的内容。下载到本地自用作为离线备份这个目的很正当。但是把下载后的视频传到其他平台、打包分享给朋友、甚至用于盈利这些都属于对UP主权益的侵害。B站的充电收入是很多创作型UP主的重要收入来源下载器如果变成白嫖工具伤害的是整个平台生态。第三不要把它做成在线解析网站或批量抓取系统。我在网上见过有人把类似脚本包装成“充电视频在线解析网站”让别人输入链接然后帮人下载。这不仅是Cookie泄露的高风险行为也明显越过了“学习交流”的边界。你自己的账号自己用最多帮身边懂技术的人搭个环境就已经够了。第四不要在公共服务器上挂机下载。服务器IP往往是数据中心段B站的风控策略对这类IP更严格很容易出现Cookie被风控甚至账号被限制的情况。本机跑一跑学习原理这是最稳妥的用法。5.2 接口维护风险与值得关注的开源方向B站的接口一直在演进这项目里的playurl接口属于相对稳定的老接口但最近B站正在逐步推进wbi签名机制未来playurl接口有可能也强制要求带签名参数。Wbi签名本质是把参数按规则排序、拼接、加上秘钥做MD5再把结果拼进请求里。网上有不少公开分析和开源实现如果你发现接口返回“缺少wbi签名”之类的提示可以往这个方向去研究。不过说实话如果你的核心需求只是稳定下载B站各类视频BBDown这个开源项目维护得比个人脚本好得多支持充电视频、字幕、弹幕、多P批量下载命令行体验也舒服。我这个脚本的价值更多在于让你彻底看明白“下载一个B站视频”这件事到底经过了哪些环节登录态、防盗链、接口解析、流合并。搞懂这些以后去看任何视频站下载器都是一通百通。如果还想继续扩展有两个方向可以考虑一是增加多P视频的批量下载遍历pages数组循环执行二是增加断点续传利用HTTP的Range头从指定字节继续下载。断点续传对下载大文件意义很大但对充电视频这种几十兆的流收益其实有限做出来更像练手。我个人后续想做的是加一个简单的弹幕下载功能把视频和弹幕一起存档这样充电视频下架之后至少还留得住当时一起看视频的弹幕氛围。最后分享一个我自己在实际使用中的经验Cookie文件我从来不放项目目录里而是放在项目外的一个私有目录代码里通过相对路径或配置文件引用。这样就算代码分享给朋友也不会把Cookie带出去。工具这东西做得越顺手越要记得保护好自己的账号和隐私。
网站建设高端定制企业官网