评论区账号成分检测器:用户脚本安装、指标解读与限流实践
前阵子有个朋友甩给我一张截图评论区里两个账号正吵得热闹其中一个昵称是用户一串随机数字头像是默认灰图他顺手点进主页翻了翻——零投稿、关注四百多人、粉丝个位数、动态全是转发抽奖。他问我这种东西有没有办法不点进去就能看出来每次都手动翻一遍太费劲了。能而且装好大概三分钟。这类东西在社区里被叫作成分检测器名字听着挺唬人实际上干的事很朴素把你在评论区看到的那个账号 ID 拽过来读一遍它公开可见的行为痕迹折算成几个能看懂的数字糊一张小面板在网页边上。它不碰私信不碰手机号不碰任何需要越权才能拿到的东西全靠任何人打开浏览器都能看到的那部分信息做统计。这篇就把安装、触发、参数调整和结果解读一次讲透顺带说说我自己踩过的坑——包括那种装完了但是没反应的经典问题以及看到一堆数据之后最容易犯的误判。适合两类人看一类是纯粹想省事的普通用户想在下结论之前多一个参考维度另一类是自己动手改脚本的我会把请求链路和几个关键参数拆开讲照着改就能用。技术门槛不高但有几个细节不注意就是白装。1. 先掰扯清楚成分检测到底在测什么1.1 它读的是公开行为痕迹不是隐私数据很多人一听检测账号就联想到扒信息这个误会得先澄清。评论区页面本身就是一个数据富矿你在网页上滚动加载评论的时候浏览器已经在后台拿到了一份结构化的 JSON里面每个楼层都带着一堆字段用户 ID、昵称、头像地址、等级、是否是大会员、是否资深会员、评论正文、发布时间、点赞数、回复数。这些东西只要你的浏览器能渲染出来就说明它是公开下发的。所以最轻量的实现方式根本不是额外去请求一堆接口而是先把白送的这份数据吃掉。我见过不少人一上来就写死循环去请求用户空间结果几十条评论就触发了限流其实首屏那份 JSON 里的信息已经够做一轮初筛了——等级、ID 号段、评论长度、是否带链接这几个维度组合起来就能筛掉一大半一眼可疑的账号只有筛出来的少数几个才值得追加请求。这一点是我认为最值得强调的设计取舍先用零成本数据做粗筛再用有成本请求做精查。反过来做脚本跑十个账号就会被平台拦下来还容易把账号状态搞脏。1.2 三类指标活跃、互动、内容把能拿到的东西归归类大概就是三个抽屉。活跃画像管的是这个号活了多久、什么时候在活动。核心是账号 ID 的号段早期注册的号位数少这个只能粗估别当精确值用、等级、最近一次动态的时间、评论集中在一天里的哪些时段。一个注册才几天、但一天二十四小时都在评论的账号和一个是六年前注册、只在晚上十点后冒泡的账号行为模式完全不是一回事。互动画像管的是它怎么跟别人打交道。关注数与粉丝数的比值、互关比例、评论长度的中位数、是自说自话还是真的在回复别人、被回复的比例。营销类账号的典型特征是关注数远大于粉丝数、评论极短、几乎不回复别人的追问。内容画像管的是它自己产出什么。投稿数量、原创与转载的比例、分区集中度、动态的类型分布。一个只投过三个视频但发了八百条动态的账号和一个投稿两百条但从不发动态的账号是两种完全不同的存在。这三类指标单独看都可能误导放一起看才有意义。这也是为什么面板上不该只给一个总分——总分是给懒人看的分项才是给判断用的。1.3 结果的边界能说明什么不能说明什么这里必须把话说死不然用着用着就容易走偏。检测结果能说明的是行为模式的相似度比如这个账号的行为特征和常见的批量注册号高度重合。它不能说明的是这个账号背后是谁、动机是什么、说的对不对。区别在哪一个人可能是刚注册的新用户可能是不爱发内容的潜水党可能是把账号借给家里人共用这些都会让指标看起来很像可疑账号。反过来一个运营了很久、投稿不少、粉丝几千的账号也可能在特定话题下做有组织的发言——它的画像非常健康但行为依然值得警惕。所以正确的用法是把它当筛选器而不是判决书。它帮你把一百条评论里的注意力集中到三五个值得多看一眼的账号上剩下的判断还是得你自己做。我见过有人拿着分数截图去挂人这属于典型的本末倒置也容易把工具本身搞臭。2. 装之前得先理清的三件事2.1 脚本宿主怎么选不是所有管理器都能跑这类工具绝大多数是以用户脚本的形式分发的需要一个宿主来托管。市面上常见的有三类差异比想象中大宿主类型跨域请求能力本地存储对新版页面的兼容上手难度老牌全能型提供专用跨域 API需要声明白名单容量较大支持对象存储好社区跟进快低装完即用轻量开源型同样提供跨域 API语义略有差异支持API 命名不同好中部分脚本要改一行国产集成型提供跨域 API还带脚本市场支持好最低但要注意脚本来源关键差异在跨域请求这一项。评论区脚本要去请求用户空间的数据这属于跨域普通fetch会被浏览器的同源策略拦下来。宿主提供的专用 API 就是为了解决这个问题的但它需要在脚本头部的元信息里用connect显式声明允许访问的域名少写一个域名请求就会静默失败——注意是静默控制台里可能只有一行不起眼的警告面板上什么都不显示特别容易让人以为脚本坏了。我个人的选择倾向是如果你只是用随便哪个都行如果你打算改脚本选那个 API 文档写得清楚的改起来省事。2.2 权限弹窗里那几行字值得逐条读安装脚本的时候宿主会弹一个确认框列出这个脚本要的权限。绝大多数人都是直接点确定我建议至少看一眼这几项GM_xmlhttpRequest跨域请求。这是核心能力没有它脚本基本废掉。GM_setValue/GM_getValue本地存储。用来缓存已经查过的账号避免重复请求。unsafeWindow直接访问页面上下文。这个权限要警惕正常情况下查公开数据用不到它如果脚本要了这个权限又说不清用途我一般就不装了。match脚本在哪些页面生效。正常应该只有评论区所在的那几个路径如果写了*://*/*这种全站通配说明它想在所有网站上跑没必要。还有个小细节脚本的版本号和更新地址。能自动更新的脚本会随着页面改版跟着修这个价值比想象中大——评论区前端平均几个月就会调整一次结构写死选择器的脚本过一阵子就抓不到元素了。2.3 账号状态与页面状态装之前先确认两件事能省掉后面一半的排查时间。第一你得是登录状态。没登录的时候评论区只下发一部分数据用户 ID 字段可能缺失脚本拿到空值自然什么都算不出来。这不是脚本的问题是数据源的问题。第二把那些净化类的扩展先关掉。有些工具会隐藏评论区元素、折叠长评论、移除头像这些操作会直接改变 DOM 结构脚本靠选择器找元素的逻辑就断了。症状很典型面板能出来但永远显示正在读取。还有一种是新旧版评论区的问题。社区前端做过一次比较大的改版新版把评论区封装成了自定义元素内部结构走 Shadow DOM老脚本用document.querySelector直接查是查不到的必须穿透 shadowRoot。如果你装的脚本比较老很可能就卡在这一步。3. 三分钟跑通从装宿主到第一次出结果3.1 第一步装好宿主并确认它活着装完扩展之后浏览器工具栏上应该出现一个图标。点开确认它显示的是已启用状态而不是灰色的。有的宿主在装完之后需要刷新一次已经打开的页面才会注入所以装完先把所有旧标签页关掉重开别在旧页面上试。顺便确认一下扩展没有被浏览器的扩展程序管理默认禁用。有些浏览器对来源不明的扩展会先禁用需要手动打开开关。这一步看着傻但确实是新手最常卡住的地方。3.2 第二步导入脚本并核对元信息从脚本的分发页点安装宿主会弹出安装界面上半部分是可读的代码。这时候别急着点确定往下滚一滚看两个地方一是match那一行确认它匹配的是你实际要用的页面路径不是别的。二是grant那几行确认跨域请求的权限在里面。这两项对了基本就成了一半。如果你的宿主不支持那个分发页的直接安装部分轻量型宿主是这样就得手动新建脚本、粘贴代码。粘贴的时候注意别把最外层的内容搞丢尤其是开头那段注释块宿主的元信息全靠它解析缺了name那一块脚本是装不上的。3.3 第三步在评论区触发面板打开任意一个视频的评论区往下滚动让评论加载出来。正常情况下三五秒内页面右下角或者评论区工具栏附近会冒出一个悬浮按钮。如果没冒出来按 F12 打开开发者工具切到 Console看有没有红色报错。最常见的三种xxx is not defined—— 宿主没注入或者脚本没装成功。Uncaught (in promise) TypeError: Cannot read properties of null—— 选择器没找到元素通常是页面改版或者被别的扩展改了结构。一堆 CORS 相关的警告 ——connect里没写对应域名。3.4 第四步首次运行必看的四个开关面板出来之后别急着点开始检测先看看设置里这几项请求间隔默认值一般在 800 到 1500 毫秒之间。低于 500 的建议直接改掉。缓存有效期默认 24 小时比较合理调太短等于每次都重新拉一遍。最大检测条数有个上限比较安全比如一次最多处理当前加载出来的前 50 条。是否自动检测这个一定要关。自动检测意味着你每次打开评论区它都在后台跑请求一天下来次数相当可观很容易被平台判定为异常流量。把这四项确认完再点开始。第一次跑建议就检测十条看看结果对不对别一上来就全量。4. 点下去之后它到底在干什么4.1 从 DOM 里把用户 ID 抠出来这一步是所有后续操作的前提也是最容易出问题的地方。千万不要用昵称当唯一标识。昵称可以随便改可以重名里面还可能有各种特殊字符、表情符号拿它做键去重会乱套。正确做法是找用户 ID它是个纯数字在页面上通常藏在评论楼层的链接里格式类似/space/1234567890用一个正则就能提出来const midPattern /space\.(\d)/; function extractMid(el) { const link el.querySelector(a[href*/space/]); if (!link) return null; const m link.getAttribute(href).match(midPattern); return m ? m[1] : null; }新版页面还有一种写法元素上直接带>function extractMid(el) { const direct el.dataset?.userId; if (direct /^\d$/.test(direct)) return direct; const link el.querySelector(a[href*/space/]); return link?.getAttribute(href).match(/space\.(\d)/)?.[1] ?? null; }评论区是懒加载的滚动才会新增节点所以不能用querySelectorAll一把梭得用MutationObserver盯着列表容器const observer new MutationObserver(muts { for (const m of muts) { for (const node of m.addedNodes) { if (node.nodeType ! 1) continue; const items node.matches?.([data-user-id], .reply-item) ? [node] : [...node.querySelectorAll?.([data-user-id], .reply-item) ?? []]; for (const it of items) { const mid extractMid(it); if (mid !seen.has(mid)) { seen.add(mid); queue.push(mid); } } } } }); observer.observe(listContainer, { childList: true, subtree: true });4.2 接口调用的顺序和限流设计拿到 ID 之后才轮到追加请求。合理的调用顺序是先便宜后昂贵先拿基础信息粉丝数、关注数、等级这一层几乎不耗配额再拿投稿列表这个稍重最后才考虑动态那个最重而且经常因为对方没开公开权限而返回空。限流的核心是串行 固定间隔 指数退避。别写并发并发是触发风控最快的方式。一个能用的队列大概长这样const CONFIG { interval: 1200, retry: 2, backoff: 3 }; async function runQueue(ids) { for (const id of ids) { const cached await getCache(id); if (cached) { render(id, cached); continue; } try { const data await fetchProfile(id); await setCache(id, data); render(id, data); } catch (e) { if (e.status 412 || e.code -352) { // 平台在提示访问过于频繁直接停手 toast(请求过于频繁已暂停请稍后再试); break; } // 其他错误按退避重试 await sleep(CONFIG.interval * CONFIG.backoff); } await sleep(CONFIG.interval Math.random() * 400); // 加随机抖动 } }那个随机抖动是经验之谈固定间隔的请求在服务端看是非常规律的机器特征加一点随机偏移会自然得多。这不是什么高深技巧就是别让请求看起来太整齐。4.3 缓存、去重和增量刷新缓存做得好脚本的请求量能降一个数量级。评论区里同一个账号经常连着出现好几条去重能省掉重复请求同一个账号你昨天查过今天没查缓存能直接命中。缓存的键用用户 ID值里带时间戳读取时判断是否超过有效期const TTL 24 * 3600 * 1000; async function getCache(mid) { const raw await GM_getValue(cache_ mid, null); if (!raw) return null; if (Date.now() - raw.ts TTL) return null; return raw.data; }有个坑要注意宿主的存储是有容量上限的几百个账号的记录堆下来某些宿主会开始报配额错误而且是静默失败——写入不报错但读出来是空。稳妥一点的做法是定期清理过期的键或者干脆只把最近五十条放内存里长期存储只留最重要的几个字段。4.4 412 和那串负数错误码是平台在喊停用这类工具一定会碰到 412 状态码或者响应体里带-352、-799这类业务错误码。这些东西的含义很直白你的请求频率超出正常用户的水平了。这时候正确的操作只有一个停下来等。不是换个地址、不是加个请求头、不是搞个什么技巧绕过去。绕过去意味着你在跟平台做对抗而平台的反制手段永远比你多。我自己定的规矩是碰到一次就停十分钟以上连续碰到第二次当天就不用了。还有一个容易被忽略的来源如果脚本开了自动检测你只是正常刷视频后台也在一直发请求。这种用户无感的流量最容易被判异常。这也是我前面强调把自动检测关掉的原因。5. 数据出来了怎么读才不跑偏5.1 权重是怎么算的为什么不能只看总分绝大多数脚本会给一个 0 到 100 的综合分但那个分数背后的权重是作者拍脑袋定的换个作者就换一套标准。我见过一个脚本把关注数大于 300直接算 20 分结果把一堆正常的老用户判成了可疑账号。下面这张表是我自己改脚本时用的权重纯属个人经验你可以按自己的使用场景调整指标观察价值建议权重说明注册年限高25号段粗估只作参考不作依据关注/粉丝比高20比值大于 50 且粉丝少于 10 时权重拉满投稿原创比中高20全转载且无自定义简介的加权评论长度中位数中15极短评论小于 5 字占比高时加分活跃时段集中度中10全天候无休的加分是否带外链高10单个账号反复带同一外链重点标记看明白一件事没有任何一项单独能定案。注册年限短的可能是真新人关注数多的可能是单纯爱看评论短的可能是懒得打字。只有多项同时指向同一个方向才值得多看一眼。5.2 四种常见形态对照着看形态一刚注册的空壳号。ID 号段很新等级个位数头像默认零投稿动态为空或者全是系统消息但已经在评论区高强度发言。这种组合的指向性最强因为正常新用户的第一周通常在刷内容不会一上来就密集评论。形态二沉默多年的老号突然活跃。注册时间很早等级不低投稿有些但很久没更新动态停在两年前然后最近一周突然在特定话题下高频出现。这种反而要多想一层——有可能是老用户重新活跃也有可能是账号易手。单看指标下不了结论得结合发言内容判断。形态三高关注低粉丝的转发型。关注数几百上千粉丝个位数投稿里全是转发或者搬运简介里带联系方式。这类是营销号的概率确实高但也不排除是纯粹的活跃观众判断的关键是看它有没有在评论区主动贴链接、贴联系方式。形态四各项指标都很健康。投稿有原创、粉丝数合理、动态有生活气息、评论有来有回。这类账号画像上完全正常但不代表它的发言就一定客观。工具只能筛行为特征筛不了立场和动机这一点务必记住。5.3 三种高频误判我自己都踩过误判一把家庭共用账号当成批量号。同一个家庭里几个人共用一个账号登录设备和时段都很杂活跃时间跨度大看起来就像二十四小时在线的机器号。实际上什么都不是。误判二把搬运号当成营销号。有些账号专门搬运其他平台的内容投稿量大、原创少、简介里带来源说明指标很难看但人家就是干这个的跟恶意营销是两码事。分辨点是看它有没有在评论区做引流动作。误判三把情绪化发言当成异常行为。一个人在某个视频下连着回了二十条可能只是真的被气到了。指标上短时间内高频评论会触发告警但这跟账号性质无关。这类误判在热点事件下特别常见所以我一般会避开热点高峰期使用这类工具那时候所有人的行为都异常。6. 想自己改的话这三个参数最值得动6.1 请求间隔从 1200 毫秒开始试这是所有参数里影响最大的一个。设成 300 毫秒跑二十个账号就会被拦设成 3000 毫秒五十个账号要跑两分半体验很差。我的建议是默认 1200 毫秒遇到限流就翻倍不要一次调到 5000那样跑一次要等太久。同时把随机抖动加上interval Math.random() * 400这种写法能让请求节奏自然一些。还有个细节如果你要检测的账号数超过三十个中间加一次较长的停顿比如每处理十个休息五秒。这个不是玄学是给服务端一个喘息也降低被判定为连续异常的概率。6.2 缓存有效期24 小时是个甜点缓存时间太短没意义太长数据会过期——一个账号三个月前是空壳现在可能已经正常运营了。24 小时是个比较平衡的值。如果你只是偶尔看看可以调到 72 小时如果你在追踪某个特定账号的变化那就调到 6 小时但代价是请求量翻四倍。另外建议把缓存做成手动可清的。加一个清空缓存按钮排查问题的时候特别有用——很多数据不对的情况清一下缓存就恢复正常了因为读到的是过期的旧记录。6.3 指标权重先把误报调下去改权重之前先明确你的使用目标。如果你的目标是尽量不漏那就把阈值调低宁可多标几个自己再人工看如果你的目标是尽量不冤枉人阈值就得调高只标那些多项指标同时异常的。我自己的做法是先把所有阈值调到极宽松跑一批已知正常的账号比如自己的号、朋友的号、几个常看的大 UP 主看看会不会被误标。有误标就调权重把导致误标的那一项降下来。这个校准过程大概要半小时但校准完之后的准确率提升非常明显。代码上就是把权重抽成一个对象别硬编码在判断逻辑里const WEIGHTS { accountAge: 25, followFanRatio: 20, originalRatio: 20, commentLen: 15, activeHours: 10, externalLink: 10, }; function score(profile) { let s 0; if (profile.ageDays 30) s WEIGHTS.accountAge; if (profile.following 300 profile.fans 10) s WEIGHTS.followFanRatio; if (profile.originalRatio 0.05 profile.videos 0) s WEIGHTS.originalRatio; if (profile.medianCommentLen 5) s WEIGHTS.commentLen; if (profile.activeHourSpread 18) s WEIGHTS.activeHours; if (profile.repeatedLink) s WEIGHTS.externalLink; return Math.min(s, 100); }这样改起来就是改数字不用动逻辑也不容易改出 bug。7. 用这类工具的分寸感7.1 不存储、不传播、不点名这条不是道德说教是实际操作层面的建议。不存储脚本默认把查到的数据缓存到本地就够了别去做批量导出。导出成 CSV 存在电脑里万一哪天电脑丢了或者被人拿到那堆数据就是现成的名单解释起来很麻烦。不传播面板上的分数是给你自己看的参考不是拿来当证据贴出去的。截图带分数去挂人本质上是在用一种粗糙的量化指标给人贴标签很容易误伤而且特别容易引战。我见过几个因为这事被封号的例子不值当。不点名如果你要讨论某个账号的行为用某个账号这种模糊描述就够了把 ID 抹掉。这不是怕事是没必要。7.2 拿它照自己比照别人有用得多说句实在话这类工具最大的价值其实不是用来审视别人。我自己的习惯是每隔一段时间把自己的账号丢进去跑一遍看看在别人眼里我是什么样注册多少年了、关注和粉丝的比例是多少、评论平均多长、活跃时段集中在哪。跑完经常会有意外发现——比如我以为自己挺活跃的结果投稿数一年才三个以为自己评论挺有内容中位数长度只有八个字。这种自我对照比给别人打分有意思得多也更不容易翻车。毕竟你没法通过一堆统计数字真正了解一个陌生人但你可以通过这些数字了解自己在网上留下的痕迹是什么样的。7.3 平台规则这条线别踩最后提醒一句这类脚本属于第三方工具平台的服务条款里通常有关于自动化访问的说明。我自己的原则是只用来看不用来刷——不做批量扫描、不做数据采集、不做定时任务、不给别人用。碰到限流就停手碰到接口变更就等作者更新不自己动手去对抗。真要说这三分钟值不值装好确实三分钟但把结果看明白、把参数调准、把心态摆正得花的时间远不止三分钟。工具本身没什么门槛难的是知道它能干什么、不能干什么。我个人的体会是最有用的那一刻往往不是你发现这个人有问题而是你发现自己差点因为一个分数冤枉了一个正常用户——那种还好我没直接下结论的感觉比任何检测结果都值钱。