B站批量取消关注实战:从网页 API 风控到 Tampermonkey 自动续跑
文章目录16 节
一、先说结果和边界
这次我准备清理自己 B 站账号中的全部关注,开始时共有 1192 个,涉及默认分组、特别关注、自建分组和互关账号。
我最初想直接复用浏览器登录状态调用网页 API。读取关注列表和逐个取消关注都能成功,但连续执行一段时间后,请求开始返回 -352。继续增加重试或缩短间隔并不能解决问题,只会让行为更像异常流量。
最后我改为复用 B 站桌面关注页自己的“已关注”控件:每页处理最多 24 个,处理完刷新页面,再从 localStorage 恢复任务。遇到绑定手机号验证时,脚本暂停,等我在网页中手动完成验证后继续。
我把最终脚本发布到了 GitHub:
- 项目地址:MrXJG/bilibili-auto-unfollow-all
- Tampermonkey 直接安装:bilibili-auto-unfollow-all.user.js
- 初始版本提交:f62569b
- 许可证:MIT
需要特别说明的是:这次记录的最后验证状态并不是“1192 个已经全部清零”。我确认成功取消了 297 个,实时剩余关注数为 895,随后任务暂停,后续由我自行决定是否继续。因此本文记录的是已经验证过的自动化路径,而不是一个虚构的“全部完成”结果。
公开内容不会展示账号 MID、昵称、Cookie、CSRF、绑定手机号、关注对象或备份文件。本文涉及的自动化只用于管理我自己的账号,也没有绕过验证码、手机号验证或浏览器安全机制。
二、清理前先做盘点,而不是直接开循环
取消关注会改变账号关系数据,而且很多信息无法通过一次“撤销”完整恢复。我先读取了关注列表并保存私有备份,再统计本次操作的实际范围。
| 项目 | 清理前统计 |
|---|---|
| 关注总数 | 1192 |
| 按 24 个每页计算的页面数 | 50 |
| 默认分组中的关注 | 1114 |
| 带自建分组标签的条目 | 78 |
| 特别关注 | 1 |
| 互关 | 2 |
| 名称中包含“账号已注销”的条目 | 28 |
这里的“自建分组 78 条”是带分组标签的关注条目数量,不代表有 78 个不同分组。互关也不是一种可以单独排除的普通前端分类;如果目标是清理全部关注,这 2 个互关关系同样会被解除。
风险等级:DANGER(会批量解除账号关系,不能完整自动回滚)
操作目标: 当前已登录 B 站账号的全部关注,包括默认分组、特别关注、自建分组和互关账号。 执行前检查: 确认浏览器登录的是目标账号,记录当前关注总数,并在本地备份关注列表。备份中包含个人社交关系数据,不能上传到公开仓库或粘贴到公共日志。 不可恢复点: 取消后,原关注时间、特别关注状态、自建分组归属和互关关系不会随重新关注自动恢复。即使保留 MID 列表,也不能保证完整还原这些历史状态。 停止条件: 出现手机号验证、验证码、按钮状态没有变化、剩余数量异常、页面结构变化或登录账号不符时应立即停止。不要在多个标签页中同时运行。 完成验证: 以 B 站实时关注总数和页面状态为准,不能只看脚本的本地计数。只有实时总数为 0,才能称为全部清理完成。
三、第一条路线:直接调用网页关系 API
1. 读取关注列表
B 站桌面关注页会请求下面这个接口读取关注数据:
GET https://api.bilibili.com/x/relation/followings
请求中包含目标账号、页码、每页数量和网页来源等参数。浏览器已经登录时,带上当前站点凭据即可读取自己的关注列表。
我用它完成了两个动作:
- 分页拉取 1192 条关注记录,建立操作前清单。
- 在自动化运行期间读取实时
total,避免只依赖页面中可能滞后的数字。
这里得到的列表包含 MID、昵称、分组、特别关注、关系属性和关注时间等个人数据。公开文章只保留聚合统计,不提供备份原文。
2. 单个取消关注
网页关系修改接口是:
POST https://api.bilibili.com/x/relation/modify
本次抓到的请求语义中,act=2 对应取消关注。请求还需要目标账号标识和当前会话的 CSRF 值。
下面只是我在浏览器中观察到的字段结构,用于说明请求含义,不是可以直接执行的命令:
fid=<目标账号 MID>
act=2
re_src=<网页来源>
csrf=<当前会话 CSRF,已隐藏>单次请求能够正常取消关注,这证明接口和登录状态本身没有问题。但“单次成功”并不等于可以用一个高速循环无限执行。
3. 为什么没有使用批量关系接口
我还查到了关系批量修改接口:
POST https://api.bilibili.com/x/relation/batch/modify
归档接口说明中,这个接口支持的操作类型只有关注和悄悄关注,对应 act=1、act=5,并不包含取消关注的 act=2。也就是说,不能因为接口名称里有 batch,就假设它支持批量取消关注。
这一步很关键:如果没有先确认操作枚举,直接把大量 MID 塞进一个未支持取消动作的接口,轻则返回参数错误,重则可能造成与预期不同的关系变更。
四、直接 API 循环为什么会撞上 -352
直接请求在前面一段能够成功,随后开始出现下面的返回码:
code: -352我当时仍然处于正常登录状态,读取接口也能继续使用,因此不能简单地把它解释成“Cookie 失效”或“CSRF 过期”。结合连续关系修改的行为,更合理的处理方式是把它视为平台风控信号:停止当前请求链,而不是继续堆叠重试。
这次没有尝试以下做法:
- 没有伪造或批量更换账号环境。
- 没有绕过验证码或手机号验证。
- 没有从浏览器外导出 Cookie 交给第三方服务。
- 没有通过并发、零延迟或无限重试强行冲击接口。
直接 API 路线的问题,不只是少了一个请求头。B 站原生页面发出的关系修改请求还带有页面来源、设备和前端运行上下文,并且操作节奏来自正常的网页交互。单独复制最小请求,在低频时可能有效,但不能据此推导出稳定的大批量执行能力。
在出现 -352 后,我停止了原始 API 自动化,转而使用网页原生控件。这个变化不是为了“隐藏自动化”,而是为了让每次操作继续走 B 站已经提供给当前用户的正常前端链路,并在平台要求验证时停下来交还给人工。
五、第二条路线:驱动网页原生“已关注”控件
B 站个人空间关注页每页最多展示 24 个关注对象。新的自动化不再自行拼装取消关注 POST 请求,而是定位当前页中可见的“已关注”控件并逐个点击。
整体状态机如下:
flowchart TD
A["用户点击开始"] --> B["等待关注页加载"]
B --> C{"出现手机号验证?"}
C -- "是" --> D["暂停并等待用户手动完成"]
D --> B
C -- "否" --> E["点击当前页第一个已关注控件"]
E --> F{"按钮数量减少 1?"}
F -- "否" --> G["停止任务,避免继续误操作"]
F -- "是" --> H["累计成功数并随机等待"]
H --> I{"本页已处理 24 个?"}
I -- "否" --> C
I -- "是" --> J["读取实时剩余关注数"]
J --> K{"剩余为 0?"}
K -- "是" --> L["停止并提示完成"]
K -- "否" --> M["刷新当前关注页"]
M --> B1. 每次只处理当前页
脚本把单轮上限设为 24,与关注页的页面容量一致。每成功取消一个关注后,它会检查页面中“已关注”按钮的数量是否减少 1。
如果数量没有按预期变化,脚本最多补点一次;仍然没有变化就停止,而不是跳过当前对象继续操作。这是一个故障关闭策略:宁可中断,也不在页面结构未知时继续盲点。
2. 使用随机间隔,不做零延迟连点
当前版本在确认一次成功后随机等待 600~900 毫秒。这个延迟不是绕过风控的保证,也不意味着平台认可这样的批量操作;它只是避免脚本以零间隔持续触发页面控件。
不建议把间隔改成 0,也不建议同时打开多个关注页并行运行。平台规则、页面实现和风控阈值都可能变化,脚本无法承诺任何账号环境都不会触发验证或限制。
3. 用 localStorage 保存最小运行状态
页面刷新会销毁当前 JavaScript 上下文,所以脚本只在当前浏览器的 localStorage 中保存三个核心状态:
- 是否仍处于运行状态;
- 本次已确认成功的数量;
- 本次任务的开始时间。
刷新后,如果检测到任务仍在运行,脚本会自动继续。它不会把 Cookie、关注列表或手机号保存到 localStorage,也不会把数据上传到外部服务器。
这里的累计成功数只是脚本确认按钮状态变化的次数。最终结果仍要通过关注列表接口返回的实时总数核对。
4. 每页完成后刷新并续跑
处理完一页后,脚本读取实时关注总数:
- 如果总数为 0,清除运行状态并结束。
- 如果仍有关注,等待 2 秒后刷新当前页面。
- 如果实时总数暂时读取失败,仍可刷新,但下一页会重新等待页面就绪。
这种设计解决了最初手动脚本的一个实际问题:如果只在当前 DOM 上循环,处理完一页就没有新的按钮,任务不会自己加载下一批。刷新加持久化状态,才构成真正的自动续跑。
六、手机号验证必须由用户自己完成
自动化运行一段时间后,B 站页面弹出了“请输入您绑定的手机号码”。这不是普通的确认菜单,而是平台要求当前账号完成额外验证。
脚本检测到这个可见提示后会:
- 停止继续点击关注按钮。
- 在右下角面板提示需要手动验证。
- 等待验证窗口消失。
- 验证完成后再恢复原来的页面处理流程。
我在浏览器中手动完成了这次验证。脚本没有读取、填写或提交手机号,也没有尝试识别验证码或绕过验证流程。
如果验证反复出现、无法完成或账号状态异常,应点击“停止”并结束任务。自动化工具不应该把平台明确要求人工确认的步骤变成静默绕过。
七、如何安装和使用最终脚本
风险等级:DANGER(启动后会持续取消当前账号的全部关注)
适用范围: 仅用于管理自己有权操作的 B 站账号。脚本匹配桌面版个人空间关注页,目标范围是当前登录账号页面中的全部关注。 执行前检查: 安装前查看 GitHub 源码,确认浏览器当前账号和关注总数,备份关注列表,并关闭其他 B 站关注页标签。 不可恢复点: 特别关注、自建分组、原关注时间和互关状态无法靠脚本完整恢复。GitHub 上的脚本和本地备份都不等于一键回滚工具。 停止方法: 随时点击右下角面板中的“停止”。遇到验证、页面结构变化、按钮状态不更新或剩余数量异常时必须停止。 完成验证: 观察右下角状态只是过程检查;最终以 B 站页面和实时接口显示的关注总数为准。
使用步骤:
- 在浏览器中安装 Tampermonkey。
- 打开 Raw 用户脚本安装链接,在 Tampermonkey 页面确认安装。
- 登录 B 站,打开自己的“个人空间 → 关注”页面。
- 确认右下角出现“B站全部取消关注”面板。
- 点击“开始”,再次确认操作范围。
- 保持这个页面打开;遇到手机号验证时,在网页中手动完成。
- 需要中断时点击“停止”,不要直接开启第二个标签页继续跑。
发布版本的用户脚本 SHA-256 为:
e1fae370dfad0d8dc94f46d7dee5a5d2320aa7ba565c24d254aea82532769200这个校验值用于核对我在 2026 年 8 月 8 日发布的初始脚本内容。以后 GitHub 脚本升级时,文件内容和哈希值会随之变化,应以对应版本的源码为准。
八、本次实测结果
本次任务的证据链如下:
| 阶段 | 已验证结果 |
|---|---|
| 清理前备份 | 私有备份记录 1192 个关注,未上传到 GitHub 或博客资源目录 |
| 直接 API 尝试 | 单个取消关注能够成功,连续执行后出现 -352 |
| 批量接口核对 | batch/modify 没有提供取消关注所需的 act=2 |
| 网页原生控件 | 能够按当前页逐个取消,并确认按钮数量变化 |
| 自动续跑 | 每页最多 24 个,刷新后从 localStorage 恢复 |
| 平台验证 | 出现绑定手机号验证后暂停,由我手动完成,再继续运行 |
| 最后确认进度 | 已成功取消 297 个,实时剩余 895 个 |
| 全部清零 | 未验证;任务随后暂停,不能写成已经完成 |
这组数字正好满足 1192 - 297 = 895,说明最后一次本地成功计数与实时关注总数一致。但它只能证明截至最后一次核对时的数据闭合,不能证明之后账号状态没有变化。
九、这次方案真正解决了什么
回头看,这次最重要的不是找到一个“更隐蔽”的接口,而是把任务拆成了几个可观察、可停止的状态:
- 用只读接口盘点和核对实时总数。
- 用原生网页控件执行每一次关系变更。
- 用按钮数量变化确认单次操作是否生效。
- 用
localStorage跨刷新保存最小状态。 - 用人工验证作为不可自动跨越的边界。
- 遇到异常时停止,而不是无限重试。
直接 API 更短,但在连续关系修改场景下很快碰到 -352;网页原生请求链更接近正常前端操作,却仍然可能触发手机号验证。两条路线都说明:已有 Cookie 只代表浏览器当前登录,不代表平台会允许无限量、无验证地执行高频关系变更。
最终脚本适合在明确知道后果、已备份并愿意观察运行状态的前提下使用。它不是 B 站官方功能,也不是绕过平台风控的工具。页面结构或平台规则变化后,最安全的默认动作始终是停止并重新检查,而不是继续让旧脚本盲目运行。