ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

B站API参数详解:从bvid/cid到wbi签名与m4s合并

B站API参数详解:从bvid/cid到wbi签名与m4s合并 说实话B站可能是国内被逆向分析最多的视频站之一哪怕你没专门搞过爬虫也一定见过github上那些“B站视频下载器”“B站弹幕抓取工具”。这些工具本质上都是在调B站内部的API接口而搞懂这些接口的参数体系才是真正让工具“好用”的分水岭。这篇小教程我打算换个讲法不直接甩接口列表而是从参数本身入手把B站API里最常见的那几个参数值、它们在请求里到底起什么作用、哪些地方容易踩坑一次讲清楚。适合刚接触API调用的人也给以后想自己写查成分工具、刷数据脚本、m4s合并小软件的同学留一份能直接参考的笔记。1. 先搞懂B站的核心参数体系1.1 从av号到BV号bvid、aid、cid各自管什么B站早期视频只有一个av号也就是aid全称是archive id可以理解成视频在数据库里的自增主键。后来B站出于防爬和数据混淆的考虑把对外展示的标识改成了BV号也就是bvid形如BV1xx411c7mD。BV号可以通过算法和aid互相转换而且B站官方甚至开源过转换逻辑所以现在很多接口里两个参数都能用。但真正的播放地址、弹幕归属、评论列表全部依赖一个更底层的参数cid全称是content id。cid对应的是视频分P的“分P唯一标识”。同样是BV1xx411c7mD如果视频有10P那么每一P都有一个独立的cid。比如你请求视频信息时返回的页面结构里就会包含pages数组数组里的每个元素都有cid和page字段。这里特别容易搞混的是aid是全视频共用cid才是分P独立很多新手在写多P下载脚本时只传了aid没传cid结果拿了半天都是同一个视频的第一P。我用一个实际例子说明。假设你请求了https://api.bilibili.com/x/web-interface/view?bvidBV1xx411c7mD响应里会有类似这样的结构{ code: 0, data: { bvid: BV1xx411c7mD, aid: 170001, cid: 27201639, pages: [ {page: 1, cid: 27201639, part: P1 标题}, {page: 2, cid: 27201640, part: P2 标题} ] } }这里的顶层cid其实表示的是默认分P也就是P1的cid如果你要下载第2P必须用pages[1].cid去请求播放地址。这个细节我在接第三方下载工具时见过太多次了默认拿顶层cid最后合并出来的视频永远是第一P。1.2 用户侧参数uid、mid、buvid、cookie用户相关的参数也不复杂但要分清哪些是外部可见的哪些是内部关联的。uid就是用户IDB站API很多的用户空间、粉丝列表、动态接口都用uid作为入参也有部分老接口叫mid其实指的是同一个东西。比如用户空间信息接口https://api.bilibili.com/x/space/wbi/acc/info?midxxx这里的mid就是uid。buvid是B站给浏览器客户端分配的一个匿名设备标识全称是browser unique id。它的特点是不登录也有只要你访问过B站本地cookie里基本都会存一个buvid3或buvid4。很多接口在未登录状态下请求会要求带buvid否则直接拒绝或者返回风控错误码。我的经验是新环境第一次调用B站接口先访问一次https://www.bilibili.com/拿到cookie里的buvid再拿这个buvid去请求API成功率会大幅提高。cookie本身则是登录态的凭证B站的很多敏感接口比如充电视频、追番、收藏、投币都必须带SESSDATA这个cookie字段。SESSDATA是B站登录后的核心会话凭证有效期通常是半年到一年过期后接口会返回-101错误码。另外还有bili_jct这个cookie也就是csrf token作用是用来校验POST请求的合法性。凡是要写操作的地方比如投币、点赞、评论除了带cookie之外还必须带csrf参数值就取bili_jct。这个设计比较无语但你可以理解为B站对“读接口”和“写接口”是两套鉴权体系。1.3 请求级参数wbi签名、user-agent、referer如果只是拿公开数据光带参数可能就够了但B站从2022年下半年开始大面积推广wbi签名机制。简单说B站要求一部分接口的请求参数里必须额外带上w_rid和wts两个字段wts是当前的Unix时间戳w_rid是通过一组固定的字符表对参数排序加盐后算出来的MD5值。这个机制的目的就是为了筛掉一批完全不看反爬的爬虫脚本。user-agent和referer也特别重要。B站很多接口会校验请求来源播放地址接口、弹幕接口如果referer不是https://www.bilibili.com/很容易返回-403或者-404。UA方面如果你用python的requests默认UA去请求很大概率会被风控命中因为B站的WAF会对非主流UA做拦截。我之前见过一个很典型的案例同样的参数用浏览器访问一切正常换成Python请求就报-412最后排查下来就是UA的问题。换成一个完整的浏览器UA问题立刻消失。2. 常用API接口与参数对照2.1 视频信息接口x/web-interface/view这个接口是所有视频信息的基础入口是https://api.bilibili.com/x/web-interface/view入参是bvid或aid。返回的数据非常丰富包括标题、简介、封面、UP主信息、播放数、点赞数、投币数、分享数、分P列表、标签、发布时间、审核状态等。我日常调试时最常用的几个响应字段有这些字段含义data.bvid/data.aid视频标识双保险data.cid默认P的ciddata.pages多P列表含每P的cid、标题、时长data.owner.midUP主uiddata.stat.view播放量data.desc视频简介data.pubdate发布时间Unix时间戳这个接口有一个很隐蔽的坑它只在视频公开可见时返回正常的code0。遇到充电专属视频、私享视频、被删除视频时返回的code可能还是0但data内部会出现某些字段缺失或者code直接变成其他值比如-404表示视频不存在。写工具时不要只判断最外层的code还要判断data里有没有cid。如果data存在但cid没有那基本可以断定这是个不可播放的特殊状态视频。2.2 播放地址接口x/player/playurl 与m4s播放地址接口是下载相关功能的核心入口是https://api.bilibili.com/x/player/playurl需要三个参数bvid、cid、qn。其中qn表示清晰度例如16是360P、32是480P、64是720P、80是1080P、120是4K。默认不传qn时返回的是最低清晰度的流。还有个参数叫fnval它决定返回的视频封装格式fnval1是DASHfnval16是DASH1080P及以上需要的格式。当前B站主推的格式是DASH也就是视频和音频分开返回视频轨道和音频轨道各给一个URL。这两个URL对应的文件就是大家常说的m4s文件。这里展开讲一下m4s。m4s本质上就是不带文件头信息的MP4片段视频轨一般是video.m4s音频轨一般是audio.m4s。因为音视频分离播放器需要把两者同时加载再合成所以浏览器里才会出现“同时下载两个文件”的错觉。下载到本地后可以用ffmpeg直接合并ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4B站返回的DASH流里data.dash.video是一个数组每个元素对应一种清晰度包含baseUrl、base_url、codecs、bandwidth等字段。通常取video[0]代表最高画质但也存在最高画质只有视频轨没有音频轨的情况。下一节实战部分我会演示如何选择正确的轨道。2.3 弹幕与评论一次请求拿全子弹幕池B站弹幕有几种协议老版的是XML协议新版是protobuf协议。老接口https://api.bilibili.com/x/v1/dm/list.so?oid{cid}返回的就是XML格式支持分段拉取每段大概6分钟的历史弹幕。新接口则返回protobuf需要自己写proto解析。弹幕接口虽然好调但有个特殊的地方弹幕池的oid参数用的就是视频分P的cid。不管是老XML接口还是新的protobuf接口你传的必须是cid而不是aid或bvid。很多人在调弹幕时报空数据十有八九是把oid传成了aid。评论接口相对直观https://api.bilibili.com/x/v2/reply/main需要传入type1表示视频评论oid传cid再加mode参数控制排序mode3是按热度mode2是按时间。next参数是翻页游标首页为0。2.4 搜索与用户空间查成分工具的原理最近“查成分”很火其实这类工具的本质就是抓取用户的历史动态和投稿再根据关键词做统计。用户投稿接口是https://api.bilibili.com/x/space/wbi/arc/search入参是mid、pn页码、ps每页数量、order排序方式。orderpubdate是按发布时间orderclick是按播放量。这里需要注意用户投稿接口现在也强制走wbi签名直接裸请求会被风控。搜索接口则是B站数据获取里另一个高优先级接口入口是https://api.bilibili.com/x/web-interface/wbi/search/type需要keyword、search_type、page三个参数。search_typevideo是搜视频search_typebili_user是搜用户。搜索接口同样受wbi保护而且搜索关键词携带有比较严格的风控策略如果你在几秒内连续搜索同一个词大概率会触发-412。在网上能看到的一些“B站视频关键词采集工具”核心也就是循环调用这个搜索接口再配合视频信息接口补充数据。3. 动手写一个B站视频信息查询小工具3.1 环境准备与请求头构造这里我用Python演示主要依赖requests和json不需要额外装太重的东西。先构造一个通用的请求头import requests headers { 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 } session requests.Session() session.headers.update(headers)这个请求头是基础上面提过Referer和UA两个字段能解决80%的请求失败问题。初次使用时最好先访问一次B站首页让session拿到buvid cookiesession.get(https://www.bilibili.com/)如果你不先访问首页直接去请求view接口多半也能通但如果连续高频请求就可能被风控。先访问首页拿buvid相当于先给自己混了个“游客身份”。3.2 请求view接口并解析关键字段接下来我们请求视频信息接口。以BV号BV1xx411c7mD为例def get_video_info(bvid): url https://api.bilibili.com/x/web-interface/view params {bvid: bvid} resp session.get(url, paramsparams) data resp.json() if data[code] ! 0: raise Exception(f接口错误: {data[code]} {data[message]}) info data[data] return { bvid: info[bvid], aid: info[aid], cid: info[cid], title: info[title], desc: info[desc], owner: info[owner][name], uid: info[owner][mid], pages: [ {page: p[page], part: p[part], cid: p[cid]} for p in info[pages] ], view: info[stat][view], like: info[stat][like], danmaku: info[stat][danmaku], } if __name__ __main__: print(get_video_info(BV1xx411c7mD))这里有个细节值得注意session.get返回的响应最好用resp.json()直接解析不要用resp.text再手动json.loads因为接口返回的编码有时会有问题直接用.json()可以避免乱码。如果返回内容是{code:-412,message:请求被拦截}这种多半就是风控需要检查UA和buvid。3.3 用playurl拿到m4s音视频流并用ffmpeg合并拿到cid之后就能请求播放地址了。这里以请求1080P的DASH流为例def get_playurl(bvid, cid, qn80): url https://api.bilibili.com/x/player/playurl params { bvid: bvid, cid: cid, qn: qn, fnval: 16, fourk: 1, } resp session.get(url, paramsparams) data resp.json() if data[code] ! 0: raise Exception(f接口错误: {data[code]} {data[message]}) dash data[data][dash] video_item dash[video][0] audio_item dash[audio][0] return { video_url: video_item[baseUrl], audio_url: audio_item[baseUrl], video_codecs: video_item[codecs], audio_codecs: audio_item[codecs], bandwidth: video_item[bandwidth], } urls get_playurl(BV1xx411c7mD, 27201639, qn80) print(urls)接着把两个流下载下来然后合并。下载时注意要带上Referer请求头否则B站的CDN会拒绝返回403 Forbidden。下载代码很简单def download(url, filename): resp session.get(url, headers{Referer: https://www.bilibili.com/}) with open(filename, wb) as f: f.write(resp.content) download(urls[video_url], video.m4s) download(urls[audio_url], audio.m4s)合并就交给ffmpegffmpeg -i video.m4s -i audio.m4s -c copy output.mp4整个过程看起来简单但真正跑起来你可能会遇到两个问题。第一某些视频的dash返回里video数组可能不止一个元素选择时不能只看下标要看id字段id80是1080Pid64是720Pid32是480P。第二音频轨的codecs可能是mp4a.40.2视频轨可能是avc1.640032或hev1.1.6.L120.90合并时不影响但如果你要转格式得注意解码器。下载大文件时建议用streamTrue分块写盘不要一次性resp.content不然内存吃紧视频稍微长一点就飘红。4. wbi签名机制的实现细节4.1 wbi签名到底是什么刚才提到wbi签名这里展开细讲。B站从2022年开始对一批接口做了升级要求在业务参数之外额外带上两个参数wts和w_rid。wts就是当前请求的Unix时间戳w_rid是拼接了密钥之后算出来的MD5。这个密钥不是固定的而是由一个固定字符表包含所有大小写字母和数字顺序打乱过和当前时间推导出来的。为了拿到密钥B站前端在https://api.bilibili.com/x/web-interface/nav接口的响应里会返回一个wbi_img对象里面包含img_url和sub_url两个图片地址。把这两个图片的文件名不含扩展名拼接起来就能得到32字节的原始密钥。但这串密钥还需要经过一次字符重排才能作为真正的签名密钥。这个字符重排表可以从B站源码里拿到。4.2 纯Python实现wbi签名可以直接参考我整理好的这段实现核心点在于混排表和MD5拼接import time import hashlib import urllib.parse from functools import reduce MIXIN_KEY_ENC_TAB [ 46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49, 33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36, 20, 34, 44, 52 ] def get_mixin_key(orig): return reduce(lambda s, i: s orig[i], MIXIN_KEY_ENC_TAB, )[:32] def enc_wbi(params, img_key, sub_key): mixin_key get_mixin_key(img_key sub_key) curr_time round(time.time()) params[wts] curr_time params dict(sorted(params.items())) params { k: .join(filter(lambda chr: chr not in !()*, str(v))) for k, v in params.items() } query urllib.parse.urlencode(params) wbi_sign hashlib.md5((query mixin_key).encode()).hexdigest() params[w_rid] wbi_sign return params使用时先从nav接口拿img_key和sub_key再调用enc_wbi把返回的参数拼进请求里。要注意的是排序之前的参数必须不含w_rid而且过滤特殊字符那一步不能省略否则签名算出来对不上。4.3 签名失效的典型表现与解决wbi签名失效最常见的报错是-403或-404返回内容里一般会有非法访问或请求被拦截的描述。失效原因大概率有三个一是拿到了过期的nav接口缓存密钥已经轮换二是本地时间与服务器时间偏差过大wts对不上三是排序时漏了某个参数导致B站服务端校验时拼接顺序不一致。解决办法是按流程重新请求nav接口拿最新密钥注意在正式请求前用time.time()校准一下本地时间。如果服务器时间偏差超过几十秒建议直接用NTP同步或者用一个可信任的HTTP接口返回的时间作为基准。5. 常见报错与排查思路5.1 高频错误码速查表B站API的返回码其实很固定我把实际开发里比较常碰到的整理成表省得大家每次去查文档。错误码含义常见场景-101未登录未带cookie或SESSDATA失效-111csrf校验失败POST请求没有传csrfcsrf与cookie不符-403访问权限不足风控拦截或需要更高权限如充电视频-404资源不存在视频被删除、稿件不可见、接口路径错误、wbi签名错误-412请求被拦截频率过高、UA异常、缺少buvid-799请求过于频繁短时间请求次数超过阈值-352风控校验失败需要滑块验证或升级为登录用户0请求成功正常5.2 充电视频与会员专享内容的权限边界在热词里出现了一堆“b站充电视频解析、提取网站”这里必须提醒一句充电视频本质上是付费内容B站设了权限校验接口层面不会因为你带一个普通cookie就能拿到播放地址。充电视频的播放地址接口一般会在参数里额外带一个ep_id或者season_id并且由单独的付费接口返回带durl的地址。普通用户去请求只会拿到-403或者code ! 0。网上那些解析工具大概率是接到了UP主本人的充电专属接口权限或者用测试账号把已购买的视频缓存下来再分享这存在版权风险我不建议碰。会员专享内容则类似番剧和电影用的是另一套接口体系入口是https://api.bilibili.com/pgc/player/web/playurl参数里有ep_id同时还会校验大会员状态。如果你需要开发这类功能先把普通视频的这套逻辑跑通再考虑会员内容权限边界要拎清。5.3 应对频率限制与风控的实操策略风控是所有人都躲不过去的特别是搜索、用户空间这类接口。我踩过几次坑之后总结出几个土办法第一请求间隔至少留0.5到1秒不要用并发压测的方式去刷第二有条件的话用IP池但要注意B站对同一IP的阈值非常敏感超过阈值直接-412第三尽量模拟真实浏览器行为访问API前先请求一次页面在页面里带上必要的cookie第四请求失败时不要立即重试先等30秒以上。另外B站现在还会对“无buvid但高频请求”的用户做优先级降级也就是同样一个接口带buvid的请求可能正常返回不带buvid的可能直接返回-352。这也是为什么我强调要先访问首页。反正记住一条原则用最像浏览器的方式去请求成功率一定最高。6. 一些小众但实用的参数玩法6.1 网页端快捷键和播放器参数B站网页端的播放器虽然看起来只是普通HTML5播放器但URL上也可以塞很多控制参数。比如在视频页URL后面加?t75可以直接从75秒开始播放。加?p2可以定位到第2P这个对做站内搜索直达非常有用。如果你习惯用快捷键网页端其实内置了几个常用的方向键是快进快退M是静音F是全屏空格是播放暂停。B站网页版修改快捷键的方法也简单先按Shift加问号弹出快捷键面板部分播放器的快捷键可以在浏览器的devtools里改但本质上都是改页面JS的键位映射不建议新手折腾。6.2 倍速播放与弹幕密度参数倍速播放其实不是一个专门的API参数而是前端播放器的能力。B站网页端支持0.5到2倍速在播放器右下角设置里调也可以按[和]加减速。对下载下来的视频做倍速处理还是要靠ffmpeg的setpts和atempo过滤器具体命令不复杂ffmpeg -i input.mp4 -filter:v setpts0.5*PTS -filter:a atempo2.0 output.mp4弹幕密度方面B站的新版播放器支持调节弹幕显示范围但这个设置存在播放器本地不走API。如果你想抓取最大密度的弹幕数据更好的方式是通过protobuf接口的segment参数分段拉取每段拉1分钟再合并去重基本能覆盖全弹幕池。6.3 给数据接口加参数的通用思路最后讲一个通用经验不管你是用B站的API还是以后去调其他平台的API加参数之前一定要先理清楚参数属于哪个层次。B站的参数大致分四类资源标识类bvid、aid、cid、用户标识类uid、mid、buvid、鉴权类cookie、csrf、wbi签名、业务控制类pn、ps、qn、order。调试时先确认资源标识对没对上再看鉴权和频率最后才去调业务控制参数。这个排查顺序能帮你少走很多弯路。就我个人经验来说B站API最折磨人的不是接口文档少而是参数边界条件特别多。同样一个接口可能因为少了referer、少了buvid、多了空格符返回结果就会天差地别。如果你也卡在某个报错上很久不妨把请求参数、请求头全部打印出来和浏览器devtools里实际发出的请求做个diff基本都能找到问题。毕竟B站在前端已经把正确请求都展示在你面前了照着抄总不会错。
返回列表