用浏览器控制台JS写自动化脚本:批量取消关注抖音账号实战指南
2026/9/19 11:58:30 网站建设 项目流程

前几天想清理一下关注列表,手动点了二三十个就受不了了:进主页、点关注列表、找账号、点“已关注”、弹窗再确认,一套动作重复几百次,手指和眼睛都遭罪。当时我第一反应就是“这活不该人干”。作为一个写过几年JS的人,我直接打开了电脑浏览器的开发者工具,在控制台里写了一段脚本模拟点击,十分钟不到就把批量取消关注这个需求搞定了。核心代码逻辑也就几行JS,跑起来非常稳。

这篇文章会把思路、代码逐行解释、完整操作步骤和常见问题全写出来。不管你是完全没有编程基础的新手,还是写过一点前端的老手,都能照着复现。先说清楚前提:只适合在你自己登录的抖音网页版上操作,本质上是替你自己点按钮,不涉及任何账号密码,也不碰第三方工具。

1. 别急着复制,先搞懂这个方案为什么可行

1.1 为什么不用第三方工具,而是直接在控制台跑JS

我一开始也想过去找现成的“批量取关工具”,搜了一圈后发现市面上所谓的神器大体分三类:一类是让你授权登录的第三方网页,风险最大,你等于把账号控制权交给了一个未知服务商;一类是手机端的自动点击器,需要开启无障碍服务,配置繁琐,而且App每次更新布局一变,坐标就全部失效;还有一类直接是收费的,功能也不见得稳定。

自己用JS在浏览器控制台操作是完全不同的思路:代码只在你自己浏览器里运行,不请求任何第三方服务器,不需要提交密码验证码,本质上就是你手动点击的“机械化版本”。只要抖音网页版还在用DOM渲染关注列表,这段脚本就能用。况且浏览器本身就是前端开发者的调试环境,打开控制台写JS,这是看家本领,完全可控。

方案实现方式稳定性风险
第三方批量工具授权登录,模拟接口请求依赖服务端维护账号泄露风险高
手机自动点击器按坐标点击App改版即失效无障碍权限麻烦
浏览器控制台JS直接操作页面DOM页面结构不变就可用无额外授权,风险可控

1.2 方案有边界,先做好预期管理

这个方案不是“无脑黑科技”,有几个前提条件:你需要一台电脑,浏览器能打开抖音网页版,并且账号已经扫码登录。手机浏览器理论上也能开控制台,但操作极不方便,后面会细说。

风险方面要实话实说:短时间高频点击,平台确实有一定概率弹出验证码或者临时限制关注/取消关注功能。这不是“破解”也没有绕什么加密,只是行为特征异常而已。所以我不建议一次性跑几百个,而是把脚本拆成小批量,隔一段时间跑一轮,分几天清理完。实测下来,一次跑五六十个、间隔拉长,问题不大;一上来就想清空几千个,大概率会被风控拦下,这种教训我见过不少。

1.3 关键点:你要点的按钮不是“关注”,而是“已关注”

很多人写脚本最容易翻车的地方,就是选择器匹配错对象。抖音的关注列表里,每个账号右侧的按钮,未关注状态显示“关注”,已关注状态显示“已关注”。你要取消关注,应该点击的是显示“已关注”的按钮,点下去之后才会弹出确认框。

如果脚本误匹配了“关注”两个字的按钮,那就适得其反,等于在帮你批量关注陌生人。所以代码里不能写includes('关注'),而要精确匹配“已关注”。至于为什么用文本匹配而不是CSS类名,是因为抖音网页端的前端代码带哈希后缀类名,类似Wp8f2K这种,每次发版都可能变,靠类名不靠谱。反而按钮上显示的文字相对稳定,匹配文本最省心。

再补充一个小细节:代码里我加了childElementCount === 0这个条件,意思是只找没有子元素的“叶子节点”。因为“已关注”这三个字可能同时出现在一个父容器里,父容器包含按钮以及别的子元素,如果直接点父容器,可能点错或触发多次事件。取叶子节点是最安全的操作粒度。

2. 核心JS代码逐行拆解:5行代码到底做了什么

2.1 先看核心5行版

为了让你对整体结构有概念,我先把最精简的版本贴出来。它依赖一个前提:你已经打开了抖音网页版的关注列表,并且页面能看到“已关注”按钮。

(async () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); // 1 setInterval(() => window.scrollTo(0, document.body.scrollHeight), 600); // 2 for (let i = 0; i < 50; i++) { // 3 [...document.querySelectorAll('div,span,button')].filter(e => e.textContent.trim() === '已关注' && e.childElementCount === 0).forEach(e => e.click()); // 4 await sleep(1500); // 5 } })();

严格算下来是5条主逻辑语句。这段代码不处理弹窗,也不打印进度,但能让你看到批量取消关注是怎么跑起来的。想拿来直接实战,建议用后面第三节的增强版,更稳妥。

2.2 每行代码都在干什么

第一行的sleep,是一个把setTimeout包装成 Promise 的延时函数。我把它抽出来,是为了后面写await sleep(1500)时更简洁。JS里处理这种异步等待,最顺手的方式就是 Promise 加 async/await。

第二行的setInterval是自动滚动。关注列表是无限滚动加载的,不滚动就不会加载更多数据。我每隔600毫秒把页面滚到底一次,模拟人手不断往下翻的效果。

第三行是循环次数。我写了50次,配合后面的延时,大概能跑一分多钟。实际操作时这个数字你可以自己改,想多清理就调大,想保守就调小。

第四行是整个脚本的灵魂:先通过document.querySelectorAll('div,span,button')把页面上所有候选元素找出来,再用filter过滤出文本内容恰好是“已关注”的叶子节点,最后用forEach逐个执行click()。这里用到了展开运算符[...]把 NodeList 转成数组,不然没法直接用filter方法。

第五行的await sleep(1500)是策略性的停顿。点击之后页面需要时间响应,弹窗需要时间渲染,列表需要时间加载,同时这个间隔也让操作节奏更接近真人,而不是机器人的瞬间连点。

2.3 为什么不能用同步阻塞代替异步等待

有朋友可能会问:直接写个setTimeout不就行了,为什么要套 Promise?甚至有人会想用while循环加空转来拖时间。这里我得说清楚:JS的setTimeout是异步的,而同步的while空转会直接卡死浏览器主线程。一旦主线程被占住,页面就没法渲染、没法滚动、没法弹出确认框,整个脚本会“假死”。

await sleep(...)的作用是告诉浏览器“我先挂起,计时器到了再叫我”,挂起期间浏览器可以自由处理用户界面、渲染页面、加载数据。这正是 JS 事件循环的经典应用场景:宏任务、微任务、定时器回调各自排队执行。你不需要把事件循环背得滚瓜烂熟,但至少得理解“异步等待才能把控制权还回去”这个道理,不然脚本就废了。

3. 完整实操:从打开控制台到跑完一轮

3.1 准备阶段:登录网页版抖音并进入关注列表

打开电脑浏览器,访问抖音网页版,首次使用需要扫码登录。登录后进入你自己的主页,找到“关注”入口点进去。正常情况下会看到一串你关注过的账号,每个账号右侧都有一个“已关注”按钮。

这里提醒一句:有的浏览器可能默认桌面版页面和手机版页面布局不一样,但只要是网页版,DOM结构就还在,脚本就能生效。我建议用 Chrome 或者 Edge,Ctrl+Shift+I 打开开发者工具比较顺手。

3.2 打开开发者工具和控制台

Windows/Linux 按F12或者Ctrl+Shift+I,Mac 按Cmd+Opt+I。打开后默认在 Elements 标签,需要切换到 Console(控制台)面板。Console 就是你输入 JS 代码的地方,看到最下面有个闪烁的光标提示符就对了。

粘贴代码之前,建议先在控制台执行一行探测命令,看看页面上的“已关注”按钮到底能不能被匹配到:

[...document.querySelectorAll('div,span,button')].filter(e => e.textContent.trim() === '已关注' && e.childElementCount === 0).length

如果输出结果是数字且大于0,说明选择器匹配成功;如果是0,说明页面按钮文字不一样,你先去页面上肉眼看一下按钮显示的是不是“已关注”,再决定要不要把匹配条件放宽成“包含已关注”。

3.3 增强版脚本:带进度、处理弹窗、防重复

核心5行版最大的短板是不处理取消关注后的确认弹窗,而且同一轮可能重复点到同一个按钮。实用版我加了几个能力:自动识别并点击确认弹窗里的“确定”按钮、通过 Set 记录已经处理过的节点、每轮只处理前3个按钮降低并发风险、控制台打印进度日志。

(async () => { const sleep = ms => new Promise(r => setTimeout(r, ms)); const done = new Set(); let total = 0; for (let i = 0; i < 60; i++) { const btns = [...document.querySelectorAll('div,span,button')] .filter(e => e.textContent.trim() === '已关注' && e.childElementCount === 0) .filter(e => !done.has(e)); for (const btn of btns.slice(0, 3)) { done.add(btn); btn.click(); total++; await sleep(600); const modal = document.querySelector('[class*="modal"], [class*="dialog"], [class*="popup"]'); if (modal) { const okBtn = [...modal.querySelectorAll('button')].find(b => /确\s*定|确认|不再关注/.test(b.textContent)); if (okBtn) okBtn.click(); } await sleep(600); } window.scrollTo(0, document.body.scrollHeight); console.log(`第 ${i + 1} 轮完成,累计处理 ${total} 个账号`); await sleep(1500); } console.log('全部结束,累计处理 ' + total + ' 个账号'); })();

这里解释几个新增点。Set的作用是记录已经操作过的DOM节点,因为有些按钮点击后如果页面没有立刻刷新,它可能还在列表里,下一轮查询又会被匹配到,会造成重复点击。slice(0, 3)是每轮最多处理3个,配合两次sleep,节奏比较接近真人。弹窗处理是通过正则匹配“确定”“确认”“不再关注”这类关键词,在弹窗里找到对应的按钮并点击。这套逻辑我做出来之后,在抖音网页版上实测过,能自动连续处理,不需要中途人工介入。

3.4 运行过程与结果判断

把增强版代码粘贴到 Console,回车执行。你马上会看到页面开始自动滚动,控制台不断打印“第几轮完成,累计处理多少个账号”,同时账号右侧的“已关注”按钮会陆续变成“关注”,这就是取关成功的信号。

跑完一轮之后,回到关注列表顶部,刷新页面再看一眼关注数量,确认数字已经下降。如果脚本中途没有任何日志输出,大概率是匹配不到按钮,按前面说的探测命令先排查一下。

3.5 手机浏览器能不能跑这段脚本

理论上可以。手机上的 Safari 或者 Chrome 也内置了开发者工具,但步骤非常繁琐:iPhone 需要数据线连接 Mac,在 Safari 的“开发”菜单里远程调试;Android 手机用 Chrome 的远程调试功能也至少要一条数据线。折腾半天,不如直接打开电脑弄。

至于抖音 App 内部,早年的版本还能通过一些方式注入调试环境,现在普通用户找不到入口了,而且 App 原生界面和网页版的 DOM 完全不同,这段代码在 App 里无法工作。

4. 常见问题与排查技巧实录

4.1 报错与解决速查表

我自己在写这段脚本的过程中,以及帮朋友远程看过几次,碰到的典型问题大概就是下面这么几类。

现象可能原因解决办法
提示Uncaught SyntaxError复制代码时带进了行号或特殊字符重新复制干净代码,确保 Console 中无多余符号
没有任何反应页面没有匹配到“已关注”按钮先跑探测命令,检查按钮实际文本
点击了但关注数没减少弹窗没被自动确认使用增强版脚本,或手动处理弹窗
跑一会儿就停下页面滚动到底没有新数据加载检查是否已经清理完,或者被要求输入验证码
btn.click is not a function选择器匹配到一个非元素节点确认querySelectorAll返回的是元素列表

4.2 按钮文本不是“已关注”怎么办

抖音不同时期、不同设备型号下,按钮文字可能是“正在关注”“已关注 TA”或者前后带空格。我遇到过用=== '已关注'匹配不到的情况,这时候可以改用includes('已关注')或者正则表达式:

.filter(e => e.textContent.trim().includes('已关注') && e.childElementCount === 0)

也可以先输出一些调试信息,看看页面上到底有什么文本:

[...document.querySelectorAll('div,span,button')].filter(e => e.childElementCount === 0).map(e => e.textContent.trim()).filter(t => t.includes('关注')).slice(0, 20)

这段代码会打印当前页面里前20个包含“关注”文本的叶子节点文字内容,你能直观看到按钮文本的真实格式。

4.3 页面滚动加载不触发怎么办

无限滚动页面的加载依赖“滚动到接近底部”这个动作。如果脚本执行后页面根本不滚,检查一下是不是因为页面本身就短,没有更多内容可以加载;如果确认有内容但滚动不触发,把setInterval的间隔调小一点,或者把滚动位置改为document.scrollingElement.scrollTop

setInterval(() => document.scrollingElement.scrollTo(0, document.scrollingElement.scrollHeight), 600);

有些页面用window.scrollTo是有效的,有些需要document.scrollingElement,两个都试一下总能中一个。

4.4 取消关注弹窗没有自动确认

弹窗的 class 类型每个版本可能不一样,有的弹窗外层类名不包含 modal、dialog、popup 中的任何一个,导致增强版里的弹窗选择器失效。最简单的解决办法:跑脚本的时候人稍微盯一下屏幕,一旦发现弹窗没有被自动点击,手动点一下“确定”就行。好在脚本的循环还在继续,不至于卡死。

如果真的想做到全自动,可以把弹窗的查找范围扩大到所有body下的可见元素,查找包含“不再关注”文字的按钮,但这样误触风险更高,我一般不推荐。

4.5 被要求验证码或者操作频繁提示

这是最需要重视的情况。一旦页面弹出验证码或者提示“操作频繁”,立刻停止脚本,不要换代码继续跑,更不要重启脚本硬刚。验证码是平台的风险提示信号,你继续操作只会加重限制。正确的做法是:停半天或者一天,之后再小批量接着清理,单次数量控制在几十个以内,间隔时间拉到几分钟一次,基本不会再触发。

5. 这个脚本还能怎么扩展

5.1 导出关注列表,做数据统计

批量取关之前,我通常会先把关注列表导出来,看看自己到底关注了谁。这比想象中简单,既然已经能定位到“已关注”按钮,那顺着按钮往上层找账号名称和主页链接就行。

[...document.querySelectorAll('div,span,button')] .filter(e => e.textContent.trim() === '已关注' && e.childElementCount === 0) .map(e => e.closest('li, [class*="item"]')?.innerText?.split('\n')[0]) .filter(Boolean)

跑完这行代码,会把关注列表当前加载出来的账号名显示在控制台里。你还可以把结果复制到表格里,按自己的标准分分类:哪些是已经停更的,哪些是当时随手关注的,哪些是真正想继续看的。有了这份清单再决定取关谁,就不会手滑把喜欢的主播也清掉。

5.2 改成自动关注、回关粉丝

如果你需要的不是取消关注,而是批量关注,思路完全一样,只需要把匹配文本从“已关注”改成“关注”,并且注意不要匹配到“已关注”:

.filter(e => e.textContent.trim() === '关注' && e.childElementCount === 0)

这个需求常见于一些活跃主播想快速回关粉丝的场景。但我必须提醒:关注操作比取消关注更容易触发风控,批量加关注的风险更高,量一定要控制得保守,千万别一次性加几百个。

5.3 做一个安全兜底

脚本没有“撤销”功能,取消关注之后想恢复,只能重新搜索并点回“关注”,操作同样麻烦。所以我在跑正式脚本之前,会先记录一下当前关注总数,然后跑一个低轮次的测试版本,比如只处理5个账号,确认效果符合预期后再改成大批量。第一次使用的时候,强烈建议先挑几个不太重要的账号做实验,等熟悉了手感再决定是否清理全部。

6. 一点真实体会

我用这个脚本陆续清理了将近两百个账号,没有触发任何限制,秘诀只有一个:不贪。每天跑一次,一次只处理几十个,跑完就关页面,跟正常刷抖音的频率差不多,平台不会把你当成异常机器。

我个人最大的感触是,这类“浏览器自动化”真正解决的不是酷炫问题,而是把重复、枯燥、低价值的体力活从人身上剥离开。你只需要在开始时做一次判断——哪些人值得关注,之后那些机械的点按动作完全可以交给代码。你如果动手能力稍强,可以把这套思路迁移到其他需要重复点击的网页任务上,但记住一个底线:只操作你自己的账号,只做合法合规的事情。

最后再分享一个小技巧,跑脚本之前把电脑音量关掉,有些弹窗和提示音连续响起来是真的吵,这个细节用过的人应该都懂。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询