Loading
CamelliaV の BLOG
0%
INITIALIZING
DOC_ID // 3e0ca1ONLINE

[临时AI稿][2026.9.19] B站收藏夹与稍后再看:纯 API 读取方案

2026-9-19
UPDATED: 2026-9-24
技术分享
READ 5 MIN/COUNT 1797
#实用#开发
CamelliaV の BLOG

一句话结论

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 下发给你。
两个陷阱是叠加的,缺一不可:
  1. 必须带 Referer: https://www.bilibili.com。不带的话,服务器返回一个没有任何 Set-Cookie 头的空 302。从客户端看,请求「成功」了,但响应体里什么都没有。
  1. 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 那两个叠加陷阱,它们伪装成网络问题的样子。
NAVIGATION // Related Articles
Loading...
© 2024-2026 CamelliaV