敏感词替换必须支持最长匹配优先,需用ac自动机构建并按长度降序、起始位置升序排序合并匹配结果,同时将字节偏移转为rune索引以避免中文和emoji乱码,跨行匹配需重叠缓冲,热更新应通过原子指针或读写锁安全切换。

敏感词替换必须支持优先级,否则“王八”会盖过“王八羔子”
敏感词之间存在包含关系时,不加优先级控制就会出错。比如同时配置了 "王八" 和 "王八羔子",用简单 Trie 或 strings.ReplaceAll 逐个替换,大概率先命中短词、切掉中间部分,导致长词永远匹配不到。“王八羔子坏”变成“**羔子坏”,漏过滤。
真正可行的做法是:所有敏感词统一构建成 AC 自动机(如 BobuSumisu/ahocorasick),但匹配结果不能直接返回就完事——得按长度降序排序,再按起始位置升序去重合并。否则无法保证“最长匹配优先”。
- 不要在构建自动机时硬编码优先级字段(比如加
priority int),AC 自动机本身不负责排序,那是匹配后的事 - 匹配返回的是
[]ahocorasick.Match,每个含Start和End字节偏移,必须先转成 rune 索引再排序,否则中文下位置错乱 - 排序逻辑:先按
End - Start降序(长度优先),再按Start升序(左对齐);然后做区间合并,避免重叠替换互相干扰 - 如果业务要求“前缀优先”(如“违”和“违法”都算违规),那就不能只靠长度,得额外加字段标记语义等级,并在排序时参与比较
替换时别直接用 bytes 切片,中文和 emoji 会炸
ahocorasick.Match 的 Start/End 是字节偏移,不是 rune 偏移。直接用 text[start:end] 在中文或 emoji 场景下会切碎字符,轻则乱码,重则 panic。
正确做法是先把原文转成 []rune,再用匹配位置换算成 rune 索引:
- 用
utf8.RuneCountInString(text[:match.Start])算出 start 对应的 rune 位置 - 同理算
match.End对应的 rune 位置,注意text[:match.End]要确保不越界 - 构造替换串时,也要按 rune 数量来生成掩码,比如
"*"重复endRune - startRune次,而不是字节数 - 最后拼接用
string(runes),别试图用bytes.Replace混合操作
流式替换大文件时,跨行敏感词怎么处理
用 bufio.Scanner 按行读取快,但会把 "违\n法" 拆成两行,导致漏匹配。这不是 bug,是设计使然。
初始化并管理阿里云SDK Go 客户端,涵盖 sync.Once 单例、goroutine 安全、endpoint 与 region 配置、VPC 端点及调试。
要支持跨行,必须放弃 Scanner,改用 bufio.NewReader + 分块缓冲:
- 每次读固定大小(如 64KB),但保留末尾 128 字节作为 overlap buffer
- 下次读新块时,把上一块的 overlap 拼到新块开头再匹配,确保跨块边界不丢词
- 匹配结果需过滤掉落在 overlap 区域内的(即
End ),避免重复替换 - 写回时注意:只写新块的非 overlap 部分,overlap 留给下一轮处理
热更新敏感词库时,替换逻辑可能瞬间失效
常见错误是热更新时直接替换全局 Trie 实例,而正在运行的 goroutine 还在用旧指针遍历节点,要么 panic,要么漏匹配。
安全做法只有两种:
- 用原子指针切换:
atomic.StorePointer(&globalTrie, unsafe.Pointer(newTrie)),读取时用atomic.LoadPointer,要求 Trie 实现无锁访问 - 更稳妥的是用
sync.RWMutex包一层,替换时写锁,匹配时读锁;但要注意读锁不能阻塞太久,否则影响吞吐 - 千万别在匹配循环里调
trie.AddWord—— AC 自动机不支持运行时动态增删,重建开销远大于一次全量 reload - 更新后建议加个版本号字段,让下游能感知变更,比如返回
Match{... Version: 123}
实际最难的不是算法,是把字节偏移、rune 位置、分块边界、并发更新这四层坐标系对齐。少对准一层,线上就出脏数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










