ac自动机更合理,尤其敏感词超100条时;正则多词匹配退化为o(n×m),ac自动机匹配仅o(k),且避免串行扫描导致的漏匹配与性能问题。

敏感词过滤该用 AC 自动机还是正则?
AC 自动机是更合理的选择,尤其当敏感词库超过 100 条时。正则 regexp.MustCompile 在多关键词匹配中会退化为 O(n×m) 时间复杂度,且无法共享前缀;而 AC 自动机构建一次后,匹配时间是 O(k),k 为文本长度,对长文本和大词库优势明显。
常见错误是直接用 strings.Contains 或多个 strings.ReplaceAll 串行扫描——这在 500+ 敏感词、10KB 文本下可能耗时超 20ms,且漏匹配(如“苹果”被“苹”提前截断)。
- 小规模(regexp.MustCompile(`(?i)词1|词2|词3`),但注意编译开销和大小写处理
- AC 实现推荐使用社区成熟库,如
github.com/wen0750/nac或轻量版github.com/BoburkhonD/aho-corasick,避免自己实现指针跳转逻辑出错 - 务必预编译词典:初始化时调用
ac.Build(),而非每次过滤都重建自动机
如何安全支持“模糊匹配”和“形近字替换”?
业务常提“用户把‘和谐’打成‘谐和’也要拦截”,但这不能靠暴力枚举所有排列。应在预处理层做归一化,而不是在匹配引擎里加权重或编辑距离——后者会让 AC 自动机失效,性能暴跌。
正确做法是:过滤前对输入文本做一次标准化转换,再交给 AC 匹配。例如将常见形近字映射为统一标识符:
func normalizeText(s string) string {
replacements := map[string]string{
"谐": "和",
"坔": "地",
"銫": "色",
"0": "0", // 全角数字
}
for k, v := range replacements {
s = strings.ReplaceAll(s, k, v)
}
return s
}
- 不要在 AC 自动机里插入“谐和”“和协”等变体词——词库会指数膨胀,维护困难
- 避免用拼音转换(如“xiéhé”→“héxié”),中文同音字歧义高,误杀率陡增
- 若必须支持拼音模糊,应单独走一层
github.com/go-bayes/bayes类似方案,与主过滤通道解耦
替换策略怎么选:星号掩码、删除、还是上下文脱敏?
直接全局 strings.ReplaceAll 替换为 "***" 最危险:它破坏原始文本长度,导致前端光标错位、富文本标签错乱、甚至 JSON 解析失败(如 {"content":"被***了"} 中引号被吞)。
真正健壮的做法是返回结构化结果,让调用方决定如何呈现:
type FilterResult struct {
Text string
Hits []Hit // {Start, End, Word}
}
type Hit struct {
Start int
End int
Word string
}
- 保留原始
Text字段,只提供命中位置,由上层决定是掩码、高亮、还是拒绝提交 - 注意
Start/End必须是 UTF-8 字节偏移(不是 rune 索引),否则中文切片会 panic - 若需掩码,用
strings.Replace配合utf8.RuneCountInString(s[:start])换算 rune 位置,再构造新字符串
并发安全与热更新怎么落地?
词库不可能重启更新,但 sync.RWMutex 加锁 + 原子指针替换是最简可行方案。别用 map 直接读写,AC 自动机内部状态非线程安全。
- 定义全局变量
var ac *nac.AhoCorasick,配合var mu sync.RWMutex - 加载新词表时:构建新
acNew→ 调用acNew.Build()→mu.Lock(); ac = acNew; mu.Unlock() - 过滤时只读:用
mu.RLock(),避免阻塞高频请求 - 热更新触发建议走本地文件监听(
fsnotify)或 HTTP 接口 POST 新词表,避免轮询
容易忽略的是词库编码:确保敏感词文件保存为 UTF-8 无 BOM,否则 io.ReadAll 后首字符可能变成 \ufeff,导致匹配永远失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











