一句话结论
B 站官方的 Web API 完全够用:收藏夹、稍后再看、视频搜索,全是普通 HTTP 请求。唯一的门槛是登录态——手机 APP 扫一次二维码,把 cookie 存下来,之后所有读取都是纯 API 调用,不需要任何浏览器自动化。
背景
起初的需求很简单:想读自己的 B 站收藏夹和稍后再看列表,但不想为这个开浏览器、装 Selenium。B 站的 Web API 本身是公开的,
curl 就能打,但几乎所有读操作都要登录态(SESSDATA),这一步卡住了大多数人。本文记录的是把这条路彻底打通的过程。核心发现:二维码登录 API 的回调链有个反直觉的设定,踩中之后会表现为「扫码成功、但 cookie 死活拿不到」,非常像网络问题,实际不是。
接口清单(全部为
GET,带 cookie 即可):功能 | 接口 |
生成登录二维码 | passport.bilibili.com/x/passport-login/web/qrcode/generate |
轮询扫码状态 | passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=… |
收藏夹列表 | api.bilibili.com/x/v3/fav/folder/created/list |
收藏夹内容 | api.bilibili.com/x/v3/fav/resource/list?media_id=…&order=mtime |
稍后再看 | api.bilibili.com/x/v2/history/toview/web |
视频搜索 | api.bilibili.com/x/web-interface/search/type?search_type=video&keyword=… |
扫码登录的正确姿势
整条登录链路拆成三步,每一步都有各自的坑。
第一步:生成二维码并轮询
/generate 返回一个 qrcode_key 和一个二维码图片链接。APP 扫码确认后,/poll 接口会返回登录成功。第一个坑在状态码层级:
/poll 的外层 code 字段恒为 0(表示 HTTP 层 OK),真正的扫码状态在 data.code 里:data.code | 含义 |
86101 | 未扫码 |
86090 | 已扫码,等待 APP 确认 |
86038 | 二维码已过期 |
0 | 确认成功, data.url 为登录回调 |
只看外层
code 会得到「0.4 秒就登录成功」的假象——实际上 cookie 是空的。这个字段层级混淆是第一类翻车点。第二步:crossDomain 中转(真正的难点)
扫码成功后
data.url 长这样:它不是一个直接带 cookie 的 URL,而是一个中转端点——访问它,服务器才在响应里把
SESSDATA、bili_jct、DedeUserID 等 cookie 下发给你。两个陷阱是叠加的,缺一不可:
- 必须带
Referer: https://www.bilibili.com。不带的话,服务器返回一个没有任何Set-Cookie头的空 302。从客户端看,请求「成功」了,但响应体里什么都没有。
urllib默认自动跟随 302 重定向,中间跳的Set-Cookie会被静默丢弃。SESSDATA恰恰就在第一跳 302 的响应头里。
这两个坑合起来,症状是:扫码成功 → 访问回调 → cookie 为空 → 报错「登录回调缺少 SESSDATA」。因为请求在网络层完全正常(状态码 302、有 Location),第一反应会怀疑是代理或 TUN 在剥头——这个误判我实打实经历过,还顺带查了一圈分流配置,全是白费。
正确做法:禁用自动重定向,逐跳手动解析响应头:
另外,
ticket 是一次性的:只要请求过一次 crossDomain(哪怕提取失败),这个 ticket 就消耗了。调试时不能拿同一个 ticket 反复试,必须重新扫码。这一点会显著拖慢 debug 进度,值得提前知道。第三步:持久化
拿到的 cookie 写成 JSON 存本地(建议
0600 权限)。有效期约 6 个月(服务端 Expires 头实测到 2027-03-18),不是一个月。poll 响应里还带一个 refresh_token,一并存下,到期前可以续期。读取:收藏夹与稍后再看
有了 cookie,读取部分非常直白。注意收藏夹内容接口的
order 参数:order=mtime按收藏时间倒序(最近收藏在前)
order=view按播放量
order=pubtime按视频发布时间
分页是
pn/ps,想拿「最近收藏的 20 条」就是 pn=1&ps=20&order=mtime,一页拿完。一个容易误解的地方:收藏夹列表里的
media_count 有时返回 None(尤其是超大收藏夹),但这不影响按页取数据——分页本身是靠 pn 递增 + 空页判断终止的,不需要总数。稍后再看接口一次返回全部列表,不分页。每条带
progress(已看秒数,-1 表示已看完)和 duration(总时长),可以直接算观看进度。搜索接口返回的标题里带
<em class="keyword">…</em> 高亮标签,展示前记得剥掉。一个工具脚本
最终形态是一个单文件脚本,放在本地直接用:
所有读取命令都支持
--json 拿原始 API 响应,方便二次处理。二维码在终端里用 qrencode -t ANSIUTF8 直接渲染,不用装 Python 二维码库。另外建议给 HTTP 层加一层指数退避重试。不是为了接口限流,而是国内网络偶发
SSL EOF(尤其是 TUN 环境下),一次抖动就把整条登录流程打断,体验很差。踩坑记录汇总
症状 | 表面原因 | 真实原因 |
0.4 秒"登录成功"但 cookie 为空 | 看起来像登录秒过 | 状态码在 data.code 不在外层 code |
扫码成功后拿不到 SESSDATA | 怀疑代理/TUN 剥头 | crossDomain 要求 Referer,且 urllib 自动跟随 302 丢弃了中间跳的 Set-Cookie |
同一个 ticket 反复试都不行 | 代码写错了 | ticket 一次性,请求过即消耗 |
裸 urllib 请求被 412 | 接口要鉴权 | 必须带 User-Agent,否则撞 WAF |
收藏夹列表接口 404 | 接口不存在 | 旧版 /folder/list 下线,正确路径是 /folder/created/list |
轮询中流程整体崩溃 | 接口挂了 | 网络间歇 SSL EOF,加重试即可 |
一句话总结
B 站读取类需求不需要浏览器自动化:二维码登录一次拿 cookie(注意
Referer + 手动跟跳转),之后全是 GET 请求。最大的时间杀手是 crossDomain 那两个叠加陷阱,它们伪装成网络问题的样子。