这几年做自动化和浏览器脚本,各种评论区机器人见得多了。真正让我下决心写脚本的契机,是某天晚上在一家科技资讯网站刷一篇讨论帖,拉到评论区一看:前排五六条高度雷同的“神回复”,内容都是把同一句营销文案拆成半句、多平台重复发布,点进主页全是每天凌晨3点到5点准点刷屏的痕迹。人工举报根本点不过来,于是我写了一个油猴脚本,把这类机器人评论从页面上直接清掉,从上线到现在全天候运行,基本不用再管它。这篇文章就把这个“24小时屏蔽机器人评论”的脚本从需求、策略到实现和踩坑全过程拆开讲一遍。
1. 内容整体设计与思路拆解
做这个脚本之前,我先花了几天时间观察目标网站上评论区的机器人到底长什么样。不是所有垃圾评论都长得像广告,有一类是自己写了一段像模像样的经验分享,最后拐弯抹角导流到外部平台;还有一类是纯粹的文本拼接,把几个热门词强行缝合在一起,读起来通顺但没有任何信息量。更头疼的是很多机器人会带正经头像、注册时间也有一两年,靠传统的“新号拦截”根本拦不住。
1.1 核心需求分析和脚本选型逻辑
这个脚本需要解决三个层面的问题:一是把已经加载到页面上的机器人评论清理掉,二是让后续动态加载的新评论也能被自动检查,三是不能误伤正常用户。以某个资讯平台的评论区结构来说,评论是通过接口分页拉取后动态渲染的,而且页面里包含“热门评论”和“最新评论”两个区域,同一个评论人可能会在两个区域出现,屏蔽必须覆盖两种渲染路径。
选油猴脚本而不是浏览器插件,理由很简单:插件的开发、上架、签名、审核链路太长,我要的只是一个能控制特定网页DOM的轻量工具;油猴脚本的权限模型对单页面操作已经够用,配合GM存储能保存设置状态。另外脚本分发方便,直接把源码贴给有同类需求的人,装进浏览器就能跑,不需要对方去装完整的开发者插件。
还有一个很重要的设计决策:脚本不能只做“屏蔽”,还得做“记录”。被屏蔽的评论要留在本地日志里,这样用户能随时检查误杀情况,也能反馈哪些新话术没被覆盖。这个日志功能后来在维护阶段起了大作用,很多新规则就是从日志里反推出来的。
1.2 技术方案选型与运行环境评估
脚本的运行目标主要是桌面端浏览器,以Chrome和Edge为主,兼顾Firefox。代码只依赖油猴的GM_setValue、GM_getValue、GM_deleteValue三个存储接口,不依赖任何外部库,DOM操作全部用原生方法,保持零依赖、零构建,拿到源码直接粘贴就能用。
评论区动态渲染这块,我一开始也考虑过用定时器轮询,间隔两秒扫描一次新节点。这个方案简单但有两个缺点:一是CPU占用偏大,长时间挂机每小时会白白消耗电量和性能;二是页面切换Tab时,轮询可能会被浏览器后台节流,导致回来之后页面里混着没过滤的评论。最终改成了MutationObserver监听评论区容器的childList变化,渲染一批就检测一批,性能开销小得多,也不会被后台节流影响。
2. 核心细节解析与实操要点
2.1 机器人评论文本特征提取
文本特征是最先需要整理的。我建立了四类机器人评论特征库,每一类都有不同的命中规则:
| 机器人类型 | 典型表现 | 识别策略 |
|---|---|---|
| 导流号 | 评论末尾带微信号、公众号、二维码关键词 | 正则匹配连续字母数字组合、加V/薇等变体 |
| 复制刷屏 | 同一句话在多个评论区重复发布 | 对评论内容做哈希,统计频率并缓存指纹 |
| 拼接机器 | 把多条热评片段拼成新评论 | 高相似度重复段落检测,对比ngram |
| 时间机器 | 在凌晨固定时段集中发布评论 | 结合浏览器本地时间判断,配合次数阈值 |
这里面最需要小心的就是拼接机器。它会把语句拆成几个词块,再随机组合,单纯查固定关键词会被绕过。我实际用的是bigram指纹对比:将评论按标点切分,再算每两个相邻词语的组合,如果一条评论里有三组以上bigram和近期被屏蔽评论重合,就判定为拼接机。这个策略在防误杀上做了保守处理,必须重合三组才屏蔽,宁可漏过也不能误伤。
2.2 DOM清理策略的选择
匹配到机器人评论后,在DOM操作上也有讲究。最粗暴的方式是直接执行element.remove()把评论节点删掉,但这样会带来两个问题:一是页面的评论计数不会变,用户看到计数却有空白区域,容易怀疑页面坏了;二是有些评论区前几条是“置顶”或“作者回复”,DOM层级和普通评论不一样,直接删可能连带把正常的互动结构删掉。
我的处理方式分成两层:第一层隐藏评论主体,保留评论框的占位,让页面上出现一行“该评论疑似机器人,已自动收起 [展开]”的提示;第二层在收起状态下如果用户点展开,再读取本地存储里记录的原文,手动显示。这个方案比直接删除安全得多,后续排查误杀的时候也不需要刷新页面重新加载。
父节点选择上要注意,评论的外层结构通常是评论列表的容器,内部有子节点分别承载头像、用户名、正文和点赞回复区。如果直接拿正文节点去remove,有可能只删掉一半,所以我统一往上找两层父容器,用CSS隐藏而不是删节点。
提示:在对未知页面结构操作前,先用开发者工具检查DOM树,找到评论文字节点对应的包裹层级再写代码,不要凭着猜测写。不同版本的页面结构可能差两层,选最稳定的那个祖先容器来隐藏。
2.3 MutationObserver监听范围与防抖处理
MutationObserver的监听目标不是整个document.body,而是评论列表的容器。监听body会导致所有页面变化都触发回调,包括图片懒加载、广告节点插入等,白白消耗性能。我通过页面观察确认评论容器的特征,常以class或data-testid标记,脚本启动后先执行一轮全量扫描,再挂上监听。
回调函数里也做了防抖:连续触发的多个子节点变动合并到一次检查里,用requestAnimationFrame或150毫秒的短延时执行。实际测试下来,热门文章的评论区滚动加载时,一秒钟大概触发20多次DOM变更,没有防抖时页面会掉帧,加防抖后完全顺滑。
这里还有一个细节:滚动到底部自动加载新评论时,接口返回的数据量是固定的,一次可能插入20到50条新节点。如果每条评论单独走一次完整检测流程,会频繁操作DOM。所以我收集到的新评论先放入一个数组,统一扫描,匹配到的再批量隐藏,这样浏览器渲染的压力也会小很多。
3. 实操过程与核心环节实现
3.1 脚本框架与元数据配置
脚本最基本的框架就是油猴标准的用户脚本格式,元数据里声明好匹配的网址、运行时机和所需权限。
javascript复制// ==UserScript==
// @name 资讯平台评论区机器人过滤
// @namespace comment-filter
// @version 1.4.2
// @description 自动识别并屏蔽资讯评论区中的机器人评论,支持24小时不间断运行与规则自定义
// @match https://*.example-news-site.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @run-at document-idle
// ==/UserScript==
@match这里具体网址用星号占位,实际部署时替换成目标网站的一级域名和路径通配。@run-at选择document-idle是为了等首屏评论渲染完再扫描,如果选document-start,页面结构还没生成,反而要等更久。
脚本内部启动过程分四步:加载配置、建立规则库、执行全量扫描、挂载监听器。配置从GM_getValue读取,没有配置就使用默认规则,并把状态初始化写入。为了满足“24小时”这个需求,脚本里还加了运行时长统计:启动时记录时间戳,每次扫描前检查当前时间和启动时间的差值,超过24小时自动重新加载规则库,防止正则库越跑越慢。
3.2 评论过滤核心算法实现
核心算法其实不复杂,就是把一条评论依次过四道检测关卡,任何一关命中直接标记。
javascript复制function analyzeComment(node) {
const text = extractCommentText(node);
const author = extractAuthorName(node);
const result = {
matched: false,
reason: '',
ruleId: null,
evidence: ''
};
// 第一关:导流特征
if (rules.contactPattern.test(text)) {
result.matched = true;
result.reason = '导流特征';
result.evidence = rules.contactPattern.exec(text)[0];
}
// 第二关:复制刷屏指纹检测
const fingerprint = bigramHash(text);
if (recentFingerprints.has(fingerprint)) {
result.matched = true;
result.reason = '复制刷屏';
result.evidence = fingerprint.slice(0, 16);
} else {
recentFingerprints.set(fingerprint, Date.now());
}
// 第三关:拼接机器ngram对比
const overlapCount = countOverlapBigrams(text, recentFingerprints);
if (overlapCount >= 3) {
result.matched = true;
result.reason = '拼接机器';
result.evidence = text.slice(0, 30);
}
// 第四关:时间分布
if (isOddHoursComment() && recentCountByAuthor(author) >= 5) {
result.matched = true;
result.reason = '时段刷屏';
}
if (result.matched) {
hideComment(node, result);
recordLog(result);
}
return result;
}
这段代码的运行逻辑是按顺序执行,前面的关卡如果命中,后面的就不再执行,节省正则计算开销。extractCommentText会先清理评论里的标签和多余空白,再做文本归一化处理,全角转半角、繁体转简体、去掉表情符号,这样同一句话的不同写法能归一为同一个指纹。
指纹存储用的是Map结构,key是bigram哈希值,value是时间戳。为了避免内存无限增长,每过一小时会清理一次超过24小时的老指纹。这也是“24小时”这个名字的由来:指纹只保留24小时窗口内的数据,既能识别短时间内的刷屏,又不会因为长期积累把正常的老评论误判成复制。
3.3 动态内容监听与批量处理
动态监听的实现分两个关键部分:一是观察器本身,二是批量处理的调度器。
javascript复制const commentListObserver = new MutationObserver((mutations) => {
let pendingComments = [];
for (const mutation of mutations) {
if (mutation.type !== 'childList') continue;
for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
const commentNodes = collectCommentNodes(addedNode);
pendingComments.push(...commentNodes);
}
}
}
if (pendingComments.length > 0) {
scheduleFilter(pendingComments);
}
});
function scheduleFilter(nodes) {
clearTimeout(window.__filterTimer);
window.__filterTimer = setTimeout(() => {
nodes.forEach(analyzeComment);
}, 150);
}
collectCommentNodes对于新增的节点会先判断是不是评论容器本身,如果不是再从里面查找所有符合评论类名的子节点。这里用了一个巧妙的点:一次性把本次变更加载的所有评论收集起来,而不是每插入一个节点就立刻处理,因为浏览器在渲染滚动加载时经常连续插入好几批节点,分开处理会导致指纹库状态不同步,可能造成漏判。
监听器的挂载时机也踩过坑。早期版本在页面加载完成后立即监听,但页面上的评论区是异步加载的,监听时容器还没渲染出来,怎么也等不到回调。后来改成在启动后先尝试获取容器,获取不到则用MutationObserver监听document.body,等容器出现后再把监听目标切换过去。这个“二次监听”策略稳定解决了评论区晚加载的问题。
3.4 规则自定义与配置面板
规则不能写死,否则机器人换一套话术脚本就失效了。所以我把规则拆成了“基础规则”和“自定义规则”两层。基础规则内置于脚本中,自定义规则通过页面右下角悬浮按钮触发设置面板进行增删。
设置面板是用一个原生div模拟的弹层,不引入任何UI库。面板里主要提供四个配置项:导流关键词列表、正则表达式列表、指纹窗口小时数、隐藏方式选择。“隐藏方式”有三个选项:完全隐藏、显示提示条、仅折叠。不同用户偏好不同,有的就喜欢干净页面,有的希望保留证据以便举报,所以做成可选项。
javascript复制function saveConfig() {
const config = {
blockKeywords: document.getElementById('blockKeywords').value.split(/\n/),
customRegexes: document.getElementById('customRegexes').value.split(/\n/),
fingerprintWindowHours: parseInt(document.getElementById('fingerprintWindow').value, 10),
hideMode: document.getElementById('hideMode').value
};
GM_setValue('commentFilterConfig', JSON.stringify(config));
loadConfig(); // 重新加载规则库
location.reload(); // 让规则立即生效
}
配置保存后脚本会立即重载,这虽然有点粗暴,但能保证新增规则马上生效。也有人建议热更新不用刷新页面,但考虑到规则变更后指纹库也需要清空重算,刷新是最稳妥的方案。
4. 常见问题与排查技巧实录
4.1 监控线程失效与重复执行问题
脚本上线一段时间后,遇到的最典型问题就是页面评论区翻了几页后过滤失效。排查发现原因不是代码逻辑,而是页面本身做了虚拟滚动——DOM里只保留可视区域附近的评论节点,滚动时旧节点被移出DOM,新节点被插入。我之前监听的容器一直存在,但监听器只能捕获子树变化,虚拟滚动复用同一个评论节点时只是修改了innerHTML,不会触发增删节点的回调。
解决办法是给评论容器同时挂两个监听:一个监听childList,另一个监听characterData和attributes。因为虚拟滚动改内容时会修改文本节点或style属性,这些变化也能被捕获到。不过characterData监听会在用户打字输入时也触发,所以我单独做了一次事件来源判断,过滤掉由用户输入产生的变化。
还有重复执行的问题:脚本被油猴管理器重复注入会导致过滤器跑两遍,一条机器人评论被隐藏两次产生叠加样式,影响展开按钮的事件绑定。我在脚本头部用了全局锁,检查window.__commentFilterStarted标记,如果已存在就不再执行初始化逻辑。
4.2 误杀正常评论的防控措施
误杀是最影响口碑的。我采取的防控手段是“三层复查”:第一层,命中规则但文本长度不足10个字的评论不屏蔽,因为广告词至少会写出一句完整话;第二层,命中导流规则但评论里包含多个正常表情符号的暂时放行,很多真人用户也会在评论里顺便提一句微信号;第三层,同一作者被屏蔽后,后续评论如果全部正常则自动解除该作者的隐藏标记。
指纹窗口的大小对误杀影响也很大。一开始我把窗口设成48小时,结果发现有些真人用户在几天内会把同一句话在不同文章里重复评论两三次,都被误伤了。调整到24小时后概率大幅下降,这其实就是我标题里“24小时”的技术来源之一。
4.3 跨页面规则同步与本地缓存
脚本默认只在单页面内生效,刷新后指纹库和屏蔽记录会丢失。为了让“24小时”更完整,我用GM_setValue做了持久化:每隔5分钟把当前页面的指纹快照存入本地存储,刷新页面时恢复指纹库。这个做法让同一个浏览器的多个标签页之间也共享指纹数据,A页面刚屏蔽的评论指纹,B页面立刻能识别出相似内容。
缓存数据量需要控制,长期运行会产生大量日志。我限制了单个指纹快照最多保存2000条,超过后按时间戳淘汰最旧的。日志记录文本做了去重和压缩,每条只保留时间、规则ID和评论摘要,不会无限膨胀。
4.4 兼容性备忘速查表
| 问题现象 | 原因 | 解决办法 |
|---|---|---|
| 脚本完全不生效 | 油猴权限未授权或匹配规则写错 | 检查油猴的“已授权域名”列表,确认是https和域名完全一致 |
| 部分评论过滤不到 | 评论区存在多个渲染容器 | 将监听容器选择器改为数组,遍历全部容器 |
| 评论区加载变卡 | MutationObserver回调执行过重 | 增加防抖时间,把扫描逻辑改成异步批量处理 |
| 页面切换后过滤失效 | SPA路由未触发加载事件 | 监听URL变化,路由变更后重新扫描并重挂观察器 |
| 指纹库存储报错 | 超出GM存储额度 | 精简指纹字段,增加淘汰机制 |
5. 操作心得与后续扩展建议
这套脚本从最初只针对一种导流评论,慢慢长成了现在覆盖四类机器人评论的完整过滤系统。回看整个开发过程,最有价值的不是代码本身,而是观察评论区生态和不断调整策略的思路。机器人评论的话术会跟着热门内容和政策变化,我大概每隔一两周就会根据日志里的漏网之鱼更新一次关键词规则库。规则更新不用发新版脚本,直接在配置面板里追加就行,比起传统插件要改代码发布,省事太多了。
后续我还计划加入一个简单的“机器学习”效果:统计每条被真人用户点踩的评论特征,自动提升同类评论的屏蔽权重。其实不依赖后端,纯前端也能做到。如果你也用油猴脚本并且对评论区净化有需求,建议先从最基础的“关键词屏蔽加DOM隐藏”开始做,跑通之后再逐步加指纹、时间分布这些高级特征。过滤器越灵活,漏网的和误伤的就越少,这个东西没有一劳永逸的完美状态,需要持续维护和观察。我个人用下来的体会是:能看到干净的评论区,比什么都值得。
