用 12 组正则写一个骗局话术检测器:权重打分、硬红旗保底和高亮区间合并

前阵子做了个纯前端小工具「副业照妖镜」:把招聘广告或群聊话术粘进去,它会把可疑的词标红,给一个 0 到 100 的风险分,再告诉你下一句该问对方什么。整个东西就一个 HTML,没有后端,文字不出浏览器。

这篇只讲实现:规则怎么组织、分数怎么算、标红怎么做不会乱套,以及正则上几个容易出错的地方。

规则就是一个数组

每类骗局一条规则,字段固定:

js 复制代码
const RULES = [
  { id: 'fee', name: '先交钱才能开始', w: 30, hard: 1,
    re: /(会员费|押金|保证金|培训费|学费|报名费|激活费|解冻费|交\s*\d+\s*元|先交|买课)/g,
    why: '正规工作是别人付钱给你,不是你先付钱。',
    ask: '费用能从第一笔收入里扣吗?能签合同写明可退吗?' },
  { id: 'brush', name: '刷单 / 垫付返佣', w: 25, hard: 1,
    re: /(刷单|垫付|返本金|本金.{0,8}?佣金|做任务|联单|卡单)/g, ... },
  { id: 'urgent', name: '催你马上决定', w: 10,
    re: /(名额有限|仅剩|只剩\s*\d+|限时|今天截止|先到先得)/g, ... },
  // 一共 12 条
];

w 是权重,hard 标记硬红旗。why 和 ask 直接给界面用,规则和文案放在一起,改一条规则不用去翻模板。

词表是照着公安机关公开通报的常见诈骗类型整理的:刷单返利、付费学 AI 接单、导师带单炒币、出租银行卡、拉人头这些。

打分:加权求和,再加一个保底

js 复制代码
const HARD_FLOOR = 70;
function score(ids) {
  let s = 0, hard = false;
  ids.forEach(id => {
    const r = RULES.find(x => x.id === id);
    s += r.w;
    if (r.hard) hard = true;
  });
  s = Math.min(100, s);
  if (hard) s = Math.max(s, HARD_FLOOR);
  return s;
}

同一类规则命中多少个词都只算一次,否则一段话里「刷单」「垫付」「返本金」各出现一次就能把分数堆上去,而一段只写了「先交 99 元会员费」的话术分数反而低。后者其实更危险,因为它已经在要钱了。

所以分两层:类别去重后按权重加,只要碰到一条硬红旗(先交钱、拉人头、垫付、保本收益、炒币、借卡),分数至少 70,直接落进「高度疑似」档。轻度信号像「宝妈学生党」「名额有限」单独出现最多二三十分,只提示去查证。

用工具自带的 4 个示例跑出来是这样:

示例 分数 命中类别
刷单返利 85 垫付返佣、先给甜头、轻松高薪、私聊群、催促
AI 接单交学费 100 先交钱、保本收益、先给甜头、轻松高薪、私聊、催促
带单炒币 90 保本收益、炒币、陌生 App、先给甜头
正常兼职前端 0 无

正常兼职那段里有「可远程」「按页面结算」「试稿付费」,都没被误伤,这是我比较在意的一点。

标红:区间要先合并

每条规则跑一遍 exec,把命中位置收集成 [start, end] 区间,最后统一拼 HTML:

js 复制代码
function scan(text) {
  const hits = [], ranges = [];
  RULES.forEach(r => {
    r.re.lastIndex = 0;
    const kws = new Set();
    let m;
    while ((m = r.re.exec(text)) !== null) {
      if (!m[0]) { r.re.lastIndex++; continue; }
      kws.add(m[0]);
      ranges.push([m.index, m.index + m[0].length]);
    }
    if (kws.size) hits.push({ r, kws: [...kws] });
  });

  ranges.sort((a, b) => a[0] - b[0]);
  const merged = [];
  ranges.forEach(g => {
    const last = merged[merged.length - 1];
    if (last && g[0] <= last[1]) last[1] = Math.max(last[1], g[1]);
    else merged.push([...g]);
  });

  let html = '', p = 0;
  merged.forEach(([a, b]) => {
    html += esc(text.slice(p, a)) + '<mark>' + esc(text.slice(a, b)) + '</mark>';
    p = b;
  });
  return { hits, html: html + esc(text.slice(p)) };
}

不合并的话会出问题。刷单示例里有一句「新人进群领红包」,「先给甜头」规则命中整句,「私聊群」规则又命中中间的「进群」,两个区间重叠,直接按顺序插 <mark> 会切出半截标签。排序后合并重叠区间,是最省事的做法。

这里还有三个容易漏的细节:

  1. 带 g 的正则是有状态的。RULES 是全局常量,第二次点检测时 lastIndex 还停在上次的位置,会漏掉开头的命中。每次扫描前手动 lastIndex = 0。
  2. 防零宽匹配死循环。现在的规则都不会匹配空串,但以后有人往词表里加个 .*? 之类的分支,exec 会在同一位置无限返回空匹配。if (!m[0]) lastIndex++ 是保险。
  3. 先转义再插标签。用户粘进来的是任意文本,切片要各自 esc 之后再拼 <mark>,不能先拼再转义,也不能不转义直接 innerHTML。

非贪婪 + 距离限制

很多话术是两个词隔几个字出现的,比如「本金......佣金」「邀请......返」「新人......红包」。我统一写成 A.{0,8}?B:

js 复制代码
/本金.{0,8}?佣金/
/邀请.{0,8}?(?:返|提成|奖励)/
/新人.{0,8}?(?:奖励|福利|红包)/

{0,8} 限制两个词最多隔 8 个字,避免跨句乱连;? 让它尽早结束,标红范围不会拖太长。测刷单示例时,「新人进群领红包」整段会被标出来,读起来反而更直观。

已知的误判

规则匹配的局限很明显。AI 接单那条示例里,「学员都已回本」被「保本 / 固定高收益」规则里的「回本」命中了,严格说这句是在晒成果,不是承诺保本。好在它同时命中了「报名费」,结论不受影响,但单独出现时就是误报。

反过来,骗子换个说法就能绕过去,比如把「押金」说成「诚意金」。所以工具里一直写着「话术会变」,词表也是在持续加的:有读者在评论里贴过一段「打卡 N 天全额退还学费」的话术,我就把「打卡N天退」「全额退还」「承诺可退」补进了先交钱那一类,原话贴进去现在是 70 分。

想做得更准,下一步是接一个大模型做语义判断,正则当第一道快速过滤。不过那样密钥就不能放前端了,得加个后端,这是后话。

工具在码上掘金上可以直接试,代码也都在里面。

相关推荐
程序员大阳5 小时前
副队长JavaScript教程(48)-异常处理基础
javascript·异常处理·finally·catch·try
志尊宝5 小时前
AI原生前端开发(038):AI辅助的Next.js 15全栈开发
javascript·人工智能·ai-native
jktantan5 小时前
前端也需要事务:Vue Router 乐观更新、回滚与“过期事务”
前端·javascript·vue.js
怕浪猫5 小时前
K3.1 直追 Astra,先补 13%
前端·javascript·面试
天若有情6735 小时前
【前端趣味项目】把防抖玩出新花样!一个带隐藏后门的毫秒级“读秒挑战”
前端·javascript·html·防抖·外挂·趣味demo·读秒挑战
步行cgn6 小时前
JSON.stringify 详解:把 JavaScript 对象变成 JSON 字符串
开发语言·javascript·json
上火的金鱼妹8 小时前
关于多进程sse连接通知的实战
javascript·状态模式·sse
console.log('npc')9 小时前
问题跟踪与知识沉淀:从任务到知识的闭环)
前端·javascript·人工智能
missile1119 小时前
# HDR 算法 ISP 通路客制化方案(G100 / mt6789)
java·前端·javascript