Aho-Corasick自动机匹配敏感词比暴力查找快一个数量级且耗时稳定,仅与文本长度相关;需归一化预处理、缓存Trie、倒序标记替换,并注意UTF-8偏移与语义歧义。

用 Aho-Corasick 自动机做敏感词匹配,比遍历快一个数量级
单次匹配 1000 个敏感词时,暴力 std::string::find 平均耗时 8–12ms;而构建一次 AC 自动机后,对任意长度文本的单次扫描只要 0.03–0.08ms。关键不是“快”,而是“稳定”——匹配耗时不随敏感词数量线性增长,只跟文本长度相关。
别手写状态转移表,用现成轻量实现更可靠:aho_corasick(C++17)或封装好的 ac_trie 库。核心步骤只有三步:
- 把所有敏感词插入 trie,调用
build()构建失败指针 - 用
search_all(text.c_str(), text.size())获取所有匹配起止位置 - 注意:返回的 offset 是字节偏移,若文本含 UTF-8 多字节字符,需先转为 UTF-32 或用
utf8::distance校准
脱敏不能简单 replace,要保留原始字节边界和上下文
直接 std::string::replace(pos, len, "***") 会破坏后续匹配位置(比如“北京”被替成“***”后,“北京市”就再也匹配不到了)。正确做法是预扫描、标记、后处理:
- 先用 AC 自动机跑一遍,收集所有
std::vector<:pair size_t>></:pair>(起始字节索引 + 长度) - 按起始位置倒序排序,避免替换后索引偏移干扰
- 逐个替换时,用
text.replace(start, len, std::string(len, '*'))—— 保持脱敏长度与原词一致,不压缩也不拉伸 - 若需支持“部分脱敏”(如“张*丰”),得额外维护原字符数组,用
text[start+1] = '*'手动改写
中文敏感词要过 Unicode 归一化,否则“張”和“张”永远不匹配
用户输入可能混用简体、繁体、半宽/全宽标点、甚至形近字(如“丰” vs “豐”)。AC 自动机本身不处理等价性,必须前置标准化:
立即学习“C++免费学习笔记(深入)”;
- 用
icu::UnicodeString::normalize(UNORM_NFKC)或轻量库utf8proc的utf8proc_reencode做 NFKC 归一化 - 敏感词词库也必须用同一套规则预处理,否则归一化后无法命中
- 注意:归一化会改变字节长度(如全角“。”→半角“.”),所以脱敏操作必须在归一化后的字符串上进行,且最终输出需映射回原始字符串位置 —— 这需要保存归一化前后的 byte offset 映射表
生产环境必须加缓存和超时,否则正则 fallback 会拖垮服务
AC 自动机构建耗时约 O(Σ|word|),10 万词需 150–300ms。线上不能每次请求都重建。常见错误是把 trie 对象存在局部变量里,导致每轮都重造:
- 用静态
std::shared_ptr<actrie></actrie>缓存已构建的 trie,敏感词更新时用读写锁保护 - 给匹配过程加硬超时:用
std::chrono::steady_clock记录起点,每匹配 1000 字节检查一次是否超 5ms,超时则中断并降级为std::regex_search(仅限短文本兜底) - 警惕内存泄漏:某些 AC 库的
add_word()接口内部 new 了节点但没提供 clear(),换库或手动管理生命周期
真正难的不是匹配速度,而是如何让脱敏结果既不可逆又不破坏语义结构——比如“苹果手机”里“苹果”是水果还是公司?这得靠词性标注+上下文窗口,AC 自动机只负责“找得到”,不负责“判得准”。


















