尧图精选

用PHP解析B站视频下载地址:从BV号到高清播放地址的完整实现

🕒 发布时间:2026/9/20 12:13:39 📁 来源:尧图网络
前几天有个朋友问我能不能把一个B站学习视频下载到本地他想在地铁上反复看。我第一反应是丢给他一个在线解析网站但转念一想那些网站要么弹广告、要么第二天就失效与其来回折腾不如用PHP写个解析脚本几分钟就能拿到高清播放地址。今天就把这个思路和代码完整放出来希望能帮到和那位朋友一样困在“B站视频怎么下载”这个问题上的人。如果你懂一点PHP、会用命令行甚至只是想了解视频网站接口是怎么工作的下面这5分钟的内容应该能让你少走很多弯路。这篇文章的主角是一个免费的PHP小工具。它做的事情很简单输入一个B站BV号输出一串或者多串可以直接播放的视频地址。核心代码不到60行不依赖任何框架也不需要数据库只要你的机器能跑PHP就行。与其说它是一个“破解工具”不如说它是一份“接口调用练习”因为B站网页端本身就是这样拿视频地址的我们只是把播放器背后发起的请求用PHP模拟了一遍。1. 为什么我不推荐“在线解析网站”而是自己写脚本1.1 在线解析网站的三大痛点先说说我为什么不建议你用那些所谓“免费B站解析下载网站”。第一个痛点是广告和诱导。这类站点为了赚流量费页面上通常堆满了假播放按钮、下载器推广和弹窗。你只是想下载一个视频结果可能装了一堆流氓插件或者下载下来是个exe而不是mp4。关于这一点我在帮朋友排查电脑问题时见过太多次。第二个痛点是清晰度被锁。免费的在线解析往往只能拿到480P或者720P想要1080P就引导你开会员或者关注公众号、付费买卡密。问题是B站很多视频本身就是公开的、你登录账号就能看1080P这些网站只是把你的清晰度降级再卖给你属于两头赚。第三个痛点是失效快。B站接口一调整、风控一收紧这些站点很短时间内就会集体扑街。你今天收藏的解析网站下周可能就变成“本站已关停请加群获取新地址”。这种依赖关系让你永远在追逐一个不稳定的服务。1.2 自己写脚本的本质拿到播放地址别看“解析”这个词听起来很玄它背后的逻辑其实很简单视频网页能正常播放就说明播放地址一定存在而且能被浏览器获取到。所谓解析就是去找到那个地址把它从接口返回的数据里取出来给它加上必要的请求头让你能在任意播放器里直接访问。整个过程跟“破解”完全不沾边更像是在模仿浏览器去和B站服务器“正常对话”。我见过不少人对“接口”有心理壁垒觉得那是后端工程师研究的东西。其实你用一个PHP文件加几行cURL代码就能把“接口”这件事体验一遍。这也是我推荐大家自己写而不是用别人工具的原因一旦你理解了接口调用、JSON解析、HTTP头设置这几个概念以后不管是下载其他网站的视频还是对接各种开放平台API都会变得很自然。1.3 这篇文章适合谁老实说如果你完全不会PHP只是想要一个现成工具那这个脚本也能直接用运行一下、复制地址出来就行。但如果你希望从这个例子里学到东西那就需要具备一点点基础会打开命令行知道php xxx.php这种运行方式能看懂基本的PHP语法就行不需要会面向对象对“JSON”这个东西有印象知道它是接口返回的数据格式满足这几点你跟着下面内容走一遍应该就能把这套逻辑吃透。2. B站视频地址的获取逻辑BV号、cid、playurl是怎么串起来的2.1 从BV号到aidview接口B站早期的视频ID是纯数字的也就是av号比如av170001。后来为了防爬和去重B站换成了现在这种BV开头的字符串比如BV1GJ411x7h7。但是B站内部很多老接口依然只看数字ID也就是aid。所以我们做的第一件事就是把用户输入的BV号转换成接口能识别的aid和cid。转换所用的接口是https://api.bilibili.com/x/web-interface/view?bvidBVxxxxxxxx这个接口返回的是视频的详细信息包括标题、封面、简介、分区以及一个非常重要的字段cid。cid是分P的唯一标识B站一个视频可以有多个分P也就是你看到的视频列表每一P对应一个不同的cid。所以严格来说获取播放地址时BV号并不是最终参数cid才是。举个例子你请求view接口后返回的JSON结构大概长这样简化过{ code: 0, data: { aid: 123456789, bvid: BV1GJ411x7h7, cid: 234567890, title: 示例视频标题, pages: [ { page: 1, cid: 234567890, part: P1标题 }, { page: 2, cid: 234567891, part: P2标题 } ] } }大部分视频只有一P所以data.cid就是在这个页面下默认播放的那一P的ID。如果你需要处理多Pdata.pages数组里有全部信息。这部分后面进阶会讲到。2.2 从avid cid到播放地址playurl接口拿到aid和cid之后第二步就是请求真正的播放地址接口https://api.bilibili.com/x/player/playurl?avid123456789cid234567890qn80fnval0这个接口会返回一串播放信息。其中qn是清晰度参数fnval是格式参数。如果fnval0返回的是比较传统的DURL格式视频通常是一个或多个分段地址可以直接下载如果fnval16返回的是DASH格式视频轨和音频轨分离后面需要额外合并。在DURL格式下返回的数据核心是data.durl数组{ code: 0, data: { timelength: 1830000, durl: [ { url: https://upos-sz-mirror.../xxx.flv?xxx, order: 1 } ] } }我们需要的就是durl[0].url这个字段。2.3 防盗链和RefererB站为什么拦住外站请求拿到播放地址之后如果你直接复制到浏览器地址栏打开大概率会看到403错误或者下载下来的是一堆乱码。这不是地址错了而是B站的CDN会校验HTTP请求头里的Referer字段。它的逻辑是只有来自https://www.bilibili.com/页面的播放请求才会被允许其他来源一概拦截。这就是我们常说的“防盗链”。浏览器在播放B站视频时会自动带上Referer所以一切正常而你直接复制地址到新标签页打开时Referer就变了于是被拒绝。很多新手写爬虫、写解析脚本失败就失败在这一步明明接口返回了地址却下载不下来。解决方式也很简单在cURL请求里面手动设置一下头信息即可。这也是我下面代码里重点强调的部分。3. 5分钟跑通完整的PHP解析脚本与逐段拆解3.1 环境准备第一步确认你的环境满足要求PHP 7.4 或更高版本命令行输入php -v能看到版本号PHP已启用cURL扩展命令行输入php -m | grep curl能看到curl如果你用的是Windows最简单的方式是装一个PHPStudy或者Laragon里面自带了PHP和curl扩展。如果你用的是Mac或者Linux系统自带的PHP通常也够用。整个过程不需要安装Composer、不需要引入任何第三方库因为底层HTTP请求我们用cURL完成JSON解析用PHP自带的json_decode完全够用。3.2 第一段代码请求view接口拿到cid我习惯先封装一个通用的request()函数把公共的请求头统一设置好。这样可以避免在多个地方重复写User-Agent和Referer。?php function request(string $url): array { $ch curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 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/ ]); $result curl_exec($ch); if (curl_errno($ch)) { throw new RuntimeException(cURL错误: . curl_error($ch)); } curl_close($ch); return json_decode($result, true); }CURLOPT_SSL_VERIFYPEER设为false是为了避免本地缺少CA证书导致的SSL验证失败。这个设置在生产环境不建议长期开启但本地跑脚本问题不大。然后写一个getPlayUrl()方法第一步先请求view接口取cid和aidfunction getPlayUrl(string $bvid): array { $viewUrl https://api.bilibili.com/x/web-interface/view?bvid{$bvid}; $viewData request($viewUrl); if (!isset($viewData[code]) || $viewData[code] ! 0) { throw new RuntimeException(获取视频信息失败: . ($viewData[message] ?? 未知错误)); } $aid $viewData[data][aid]; $cid $viewData[data][cid]; echo 标题: . $viewData[data][title] . PHP_EOL; // 下一步请求播放地址 }这里先校验返回状态code只要不为0就说明BV号有问题或者视频不存在直接抛异常结束。接下来才进入真正的playurl请求。3.3 第二段代码请求playurl拿到高清播放地址接着上面的函数继续写$playApi https://api.bilibili.com/x/player/playurl?avid{$aid}cid{$cid}qn80fnval0; $playData request($playApi); if (!isset($playData[code]) || $playData[code] ! 0) { throw new RuntimeException(获取播放地址失败: . ($playData[message] ?? 未知错误)); } return $playData[data]; }这里的qn80表示请求1080P如果当前账号或视频没有这个清晰度接口会自动降级不会直接报错。至于fnval0表示使用DURL格式得到的是可以直接播放的FLV或MP4地址对新手最友好。3.4 完整源码把上面几段拼起来再加一个命令行入口就是完整的工具代码不到60行?php function request(string $url): array { $ch curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_TIMEOUT, 10); curl_setopt($ch, CURLOPT_HTTPHEADER, [ 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/ ]); $result curl_exec($ch); if (curl_errno($ch)) { throw new RuntimeException(cURL错误: . curl_error($ch)); } curl_close($ch); return json_decode($result, true); } function getPlayUrl(string $bvid): array { $viewData request(https://api.bilibili.com/x/web-interface/view?bvid{$bvid}); if (!isset($viewData[code]) || $viewData[code] ! 0) { throw new RuntimeException(获取视频信息失败: . ($viewData[message] ?? 未知错误)); } $aid $viewData[data][aid]; $cid $viewData[data][cid]; echo 标题: . $viewData[data][title] . PHP_EOL; $playData request(https://api.bilibili.com/x/player/playurl?avid{$aid}cid{$cid}qn80fnval0); if (!isset($playData[code]) || $playData[code] ! 0) { throw new RuntimeException(获取播放地址失败: . ($playData[message] ?? 未知错误)); } return $playData[data]; } $bvid $argv[1] ?? ; if ($bvid ) { die(用法: php bili_parser.php BVxxxxxxxx\n); } try { $data getPlayUrl($bvid); if (isset($data[durl]) $data[durl]) { foreach ($data[durl] as $index $part) { echo 分片 . ($index 1) . 地址: . $part[url] . PHP_EOL; } } else { echo 未解析到播放地址。 . PHP_EOL; } } catch (RuntimeException $e) { echo 解析失败: . $e-getMessage() . PHP_EOL; }把这段代码保存为bili_parser.php。3.5 实际运行效果在命令行输入php bili_parser.php BV1GJ411x7h7会先打印视频标题然后输出一个或多个分片地址。拿B站官方账号的一个视频测试输出类似这样标题: 【官方】B站 2019拜年祭 分片1地址: https://upos-sz-mirroralicn.bilivideo.com/upgcxcode/.../xxx.flv?eig...把这段地址复制到支持FLV的播放器里比如PotPlayer、VLC或者直接用ffplay打开就能正常播放。如果地址被断成好几行说明是从终端复制的换行符问题可以重定向到一个文本文件php bili_parser.php BV1GJ411x7h7 output.txt如果你只是想在浏览器里直接看也可以把这个PHP脚本放到本地Web服务器目录下用?bvid传参。不过命令行方式最简单也不会被PHP内置的浏览器输出格式干扰推荐优先用命令行。4. 我在实测中踩过的坑防盗链、多分片、清晰度被锁4.1 坑1Referer没写对返回403我第一次测试的时候Referer写的是Referer: https://www.bilibili.com注意少了一个结尾斜杠。结果部分视频能解析部分视频的播放地址拿到之后怎么都下载不了。后来我对比浏览器的请求头才发现B站CDN校验时对Referer的匹配比较严格把末尾斜杠加上去Referer: https://www.bilibili.com/问题立刻消失。别看这个区别不起眼在实际使用中可能就是一次请求成功、一次请求失败的差别。还有一点如果你改动了请求头大小写比如写成referer部分CDN也能识别但为了稳妥HTTP头里的固定名称建议按标准来大小写保持一致。4.2 坑2有些视频返回多个durl分片长视频和高码率视频会被B站切成多个分片在接口返回里就是durl数组里有多个元素。每个分片都有自己独立的URL、时长和大小。如果只取durl[0][url]你可能只下载了视频的前几分钟后面的全丢了。所以上面完整代码里我特意用了foreach循环把所有分片地址都打印出来。如果你要下载完整视频就得把每个分片都下载下来再按顺序合并。对于FLV格式合并可以直接用ffmpegffmpeg -i concat:part1.flv|part2.flv|part3.flv -c copy output.flv或者更清晰的做法先把每个分片下载到本地然后用ffmpeg -f concat -safe 0 -i filelist.txt -c copy output.flv其中filelist.txt里每行写一个file part1.flv。顺便说一句如果你看到一个视频接口返回了十几段分片不用担心是异常这是很正常的切片策略。B站的CDN会根据网络状况动态决定分片数量同一个视频在不同时间请求分片数都可能不一样。4.3 坑3没登录只能拿到480PB站对未登录用户的清晰度限制非常明显。未登录状态下即使你请求qn80接口也可能只给你返回一个480P的地址。这是因为B站要求视频清晰度必须结合账号权限来校验普通游客最多看480P登录用户通常能看720P或1080P更高清晰度比如1080P、4K、原画则要求大会员。如果你需要下载1080P一个办法是在请求头里带上你自己的登录Cookie。怎么拿Cookie浏览器登录B站后按F12打开开发者工具在Network里随便点一个请求复制请求头里的Cookie字段。然后在上面的request()函数里加一行Cookie: SESSDATA你的登录凭证; bili_jct你的csrf_token这里只使用你自己账号的Cookie相当于模拟你自己在网页端看视频属于正常账号行为不涉及破解任何东西。但要注意Cookie属于敏感信息千万不要把带着Cookie的脚本随便发给别人更不要把Cookie上传到公开代码仓库。否则别人能用你的账号发弹幕、评论甚至做一些更麻烦的操作。我实测发现携带了Cookie之后接口返回的清晰度才会真正按你账号的权限走。普通登录账号一般能拿到1080P大会员账号可以拿到1080P甚至更高。4.4 坑4请求过快返回-412B站接口有风控策略如果你高频刷接口返回的数据里code可能会变成-412提示“请求被拦截”或者“操作太频繁”。我一开始调试时用脚本循环测试了好几个BV号每个之间间隔不到1秒结果第4个就触发了风控需要等好几分钟才能继续用。解决办法很简单控制请求频率每次解析之间至少间隔2到3秒不要批量、高并发地跑这个脚本。同时建议把CURLOPT_TIMEOUT设小一点比如5到10秒避免一个接口卡住让脚本一直挂着。另外尽量不要在生产环境或者公司网络出口去频繁抓B站接口因为整个出口IP都可能被限。作为个人工具控制频率、合理使用一般不会出问题。5. 进阶玩法dash格式、音视频合并、把它变成通用小工具5.1 什么是dash格式为什么B站把一个视频拆成两半如果你把fnval从0改成16接口返回的数据结构会变得不一样{ code: 0, data: { dash: { video: [ { id: 80, baseUrl: https://upos-sz-mirror.../video.m4s?xxx } ], audio: [ { id: 30216, baseUrl: https://upos-sz-mirror.../audio.m4s?xxx } ] } } }dash对象下面有video和audio两个数组视频轨和音频轨是完全分开的两个文件。这就是DASH自适应流媒体协议它的优势是能根据用户网速动态切换清晰度让视频加载更流畅。但代价就是如果你直接下载视频轨文件打开后会没有声音。很多用视频下载工具下载B站视频的新手会遇到一个困扰明明下载下来的是.m4s文件用播放器打开却只有画面没有声音。这往往是因为他下载的是B站网页缓存里的视频轨而不是合并后的成品。用接口请求DASH格式时也存在同样的问题。所以如果你的目标是直接拿一个完整的视频文件用fnval0的DURL格式反而是最省事的。只有当你对清晰度有极致要求连DURL格式都不满足时才去处理DASH格式。5.2 用ffmpeg把两个m4s合并成mp4既然DASH格式把视频和音频分开了那拿到地址后的最后一步就是合并。合并工具我推荐ffmpeg几乎全平台通用而且处理这种纯复制流的合并非常快。下载好视频轨video.m4s和音频轨audio.m4s之后在同一目录执行ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4这里的-c copy意思是流复制也就是不重新编码只是把视频轨和音频轨封装到同一个MP4容器里。整个合并过程通常几秒就能完成而且画质没有任何损失。如果你是在命令行里只拿到了baseUrl也可以直接用PHP的file_put_contents或者cURL去下载这两个文件。不过要注意DASH的baseUrl同样需要带Referer请求头否则下载下来的依然是403。5.3 支持多P视频前面说过view接口返回的data.pages数组里包含了所有分P的信息。把它利用起来就能把工具从“单P解析”升级成“全P解析”。思路不复杂把解析播放地址的逻辑抽成一个resolveByCid($aid, $cid)函数然后循环遍历pages数组$pages $viewData[data][pages]; foreach ($pages as $page) { echo P . $page[page] . : . $page[part] . PHP_EOL; $playData resolveByCid($aid, $page[cid]); // 输出对应地址 }这样遇到“番剧、课程、合集”这类视频你一次性就能拿到所有分P的播放地址而不是只拿第一P。部分官方合集视频可能有几十上百P用这个工具配合下载工具的批量任务功能就能实现完整离线备份。不过还是那句话合集视频可能涉及UP主和平台版权个人离线观看、学习剪辑、做笔记都可以不要二次分发。5.4 写在最后关于版权与合规的提醒每次聊到视频解析、下载这类话题版权问题都绕不开。我的态度很明确技术是中性的但这个能力应该用在合理的地方。你完全可以用这个工具下载自己上传到B站的视频做本地备份这比从后台导出方便得多你也可以下载一些公有版权、允许二次创作的视频做剪辑素材你还能用它来学习B站接口的调用逻辑锻炼自己的PHP和网络编程能力。但我不建议用它去批量抓取他人付费内容更不建议把下载的视频搬运到其他平台。这不仅涉及版权风险也违背了创作者分享内容的初衷。B站的接口和风控策略一直在更新今天能用的一些参数、字段过几个月可能就会失效。如果你照着这篇文章操作时发现接口返回异常最可能的原因就是B站调整了策略。这时候不妨回头看看接口文档、浏览器Network面板里的真实请求用同样的思路重新适配就好这也是写代码的乐趣所在。最后再分享一个小技巧把上面代码里的request()函数单独保存成一个common.php以后你在其他项目里需要请求第三方接口时直接require进来就能复用。我后来写抖音、小红书、YouTube的解析脚本用的也都是同一个基础函数只是换了接口地址和参数。站在接口的角度看这些平台的结构其实大同小异。有了这次的PHP经验下次你再遇到任何“视频下载”需求应该都不会慌。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →