正则ReplaceAllString会误伤正常语义且无法应对同音、形近、拆字等绕过手段;Aho-Corasick算法通过构建状态机实现O(n+m)多模式匹配,支持精准脱敏与词库预加载,是敏感词过滤更优解。

为什么不能直接用正则全局替换敏感词
正则 ReplaceAllString 看似简单,但会误伤正常语义:比如过滤“枪”字,会导致“手枪模型”“枪手”“机枪”全被截断,甚至“呛咳”“抢购”也被波及。更严重的是,它无法处理同音、形近、拆字(如“和-谐”“口口口”)等绕过手段。真实业务中,敏感词库往往带权重、类型、生效范围,硬匹配根本撑不住。
用 Aho-Corasick 算法做多模式匹配
Go 生态里 github.com/BobuSumisu/aho-corasick 或 github.com/gansidui/ahocorasick 是较稳定的实现。它们把所有敏感词构建成状态机,一次扫描完成全部命中,时间复杂度 O(n + m),远优于逐词遍历。
实操建议:
- 预加载词库时调用
NewTrie()构建树,再用Build()编译为自动机——这步必须在服务启动时完成,不能每次请求都重建 - 匹配返回的是
[]Match,含Start、End和Keyword字段,方便做精准脱敏(如只替换中间字符:***) - 注意词库编码统一为 UTF-8;若含繁体或异体字,需提前归一化,否则“後台”和“后台”会被视为两个词
如何接入 Gin / Echo 等 HTTP 框架
别在每个 handler 里手动调 FindAll。应该写成中间件,在 ctx.Request.Body 读取前或 ctx.JSON 返回前拦截。
立即学习“go语言免费学习笔记(深入)”;
以 Gin 为例:
func SensitiveFilterMiddleware(trie *ahocorasick.Trie) gin.HandlerFunc {
return func(c *gin.Context) {
// 拦截 POST/PUT 的 JSON body
if c.Request.Method == "POST" || c.Request.Method == "PUT" {
body, _ := io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
// 过滤 body 中的敏感字段(需解析 JSON,定位 string 类型值)
filteredBody := filterJSONString(body, trie)
c.Request.Body = io.NopCloser(bytes.NewBuffer(filteredBody))
}
c.Next()
}
}
关键点:
- 不能只过滤原始字符串,要解析结构体后对
string字段逐个过筛,否则 JSON 字段名或数字会被误判 - 响应体过滤建议放在
c.Writer包装层(如自定义ResponseWriter),避免重复序列化 - 对高频接口,加
sync.Pool复用bytes.Buffer和 JSON 解析器,减少 GC 压力
绕过检测的常见场景怎么防
用户输入 “w0rd”、“和 谐”、“h e x i a n”、“和谐→” 时,基础 AC 自动机完全失效。必须叠加预处理层。
实操建议:
- 在匹配前先做轻量清洗:空格/零宽字符剔除、常见 leetspeak 映射(
"0"→"o"、"1"→"i")、全角转半角——但不要过度归一化,否则影响搜索体验 - 对高风险字段(如评论、昵称)启用二级校验:调用外部风控 API 或本地部署的轻量 NLP 模型(如 TinyBERT 微调版),识别语义违规
- 记录未命中的可疑输入(如含多个非常规符号、拼音分隔符),用于后续词库扩增和模型迭代
真正难的不是匹配本身,而是平衡准确率与性能:词库超过 10 万条时,AC 树内存占用明显上升;而实时归一化+语义分析又拖慢首字节时间。线上务必按字段分级过滤,非核心字段可降级为异步扫描。


















