说实话,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?bvid=BV1xx411c7mD,响应里会有类似这样的结构:
{ "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就是用户ID,B站API很多的用户空间、粉丝列表、动态接口都用uid作为入参,也有部分老接口叫mid,其实指的是同一个东西。比如用户空间信息接口:https://api.bilibili.com/x/space/wbi/acc/info?mid=xxx,这里的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的cid |
data.pages | 多P列表,含每P的cid、标题、时长 |
data.owner.mid | UP主uid |
data.stat.view | 播放量 |
data.desc | 视频简介 |
data.pubdate | 发布时间,Unix时间戳 |
这个接口有一个很隐蔽的坑:它只在视频公开可见时返回正常的code=0。遇到充电专属视频、私享视频、被删除视频时,返回的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,它决定返回的视频封装格式,fnval=1是DASH,fnval=16是DASH+1080P及以上需要的格式。当前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需要传入type=1表示视频评论,oid传cid,再加mode参数控制排序,mode=3是按热度,mode=2是按时间。next参数是翻页游标,首页为0。
2.4 搜索与用户空间:查成分工具的原理
最近“查成分”很火,其实这类工具的本质就是抓取用户的历史动态和投稿,再根据关键词做统计。用户投稿接口是https://api.bilibili.com/x/space/wbi/arc/search,入参是mid、pn页码、ps每页数量、order排序方式。order=pubdate是按发布时间,order=click是按播放量。这里需要注意,用户投稿接口现在也强制走wbi签名,直接裸请求会被风控。
搜索接口则是B站数据获取里另一个高优先级接口,入口是https://api.bilibili.com/x/web-interface/wbi/search/type,需要keyword、search_type、page三个参数。search_type=video是搜视频,search_type=bili_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 cookie:
session.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, params=params) 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, qn=80): url = "https://api.bilibili.com/x/player/playurl" params = { "bvid": bvid, "cid": cid, "qn": qn, "fnval": 16, "fourk": 1, } resp = session.get(url, params=params) 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, qn=80) 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")合并就交给ffmpeg:
ffmpeg -i video.m4s -i audio.m4s -c copy output.mp4整个过程看起来简单,但真正跑起来你可能会遇到两个问题。第一,某些视频的dash返回里video数组可能不止一个元素,选择时不能只看下标,要看id字段,id=80是1080P,id=64是720P,id=32是480P。第二,音频轨的codecs可能是mp4a.40.2,视频轨可能是avc1.640032或hev1.1.6.L120.90,合并时不影响,但如果你要转格式,得注意解码器。下载大文件时建议用stream=True分块写盘,不要一次性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失效 |
-111 | csrf校验失败 | POST请求没有传csrf,csrf与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后面加?t=75,可以直接从75秒开始播放。加?p=2可以定位到第2P,这个对做站内搜索直达非常有用。如果你习惯用快捷键,网页端其实内置了几个常用的:方向键是快进快退,M是静音,F是全屏,空格是播放暂停。B站网页版修改快捷键的方法也简单,先按Shift加问号弹出快捷键面板,部分播放器的快捷键可以在浏览器的devtools里改,但本质上都是改页面JS的键位映射,不建议新手折腾。
6.2 倍速播放与弹幕密度参数
倍速播放其实不是一个专门的API参数,而是前端播放器的能力。B站网页端支持0.5到2倍速,在播放器右下角设置里调,也可以按[和]加减速。对下载下来的视频做倍速处理,还是要靠ffmpeg的setpts和atempo过滤器,具体命令不复杂:
ffmpeg -i input.mp4 -filter:v "setpts=0.5*PTS" -filter:a "atempo=2.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站在前端已经把正确请求都展示在你面前了,照着抄总不会错。