不能用std::string::find逐个遍历敏感词,因其时间复杂度O(N×M)过高,不支持前缀共享、失败路径跳过及嵌套匹配;应采用AC自动机,构建Trie加失败指针实现单次扫描全量匹配。

敏感词匹配为什么不能用 std::string::find 逐个遍历?
因为时间复杂度太高:N 个敏感词、M 长文本,暴力匹配最坏 O(N×M),实际业务中几百个词+上万字文本时延迟明显。更糟的是,std::string::find 不支持前缀共享、无法跳过已知失败路径,也无法处理“华为”和“华为空气炸锅”这类嵌套或部分重叠场景。
真正可行的起点是 AC 自动机(Aho-Corasick),它把所有敏感词构建成一棵 Trie,再加失败指针(fail pointer),单次扫描就能完成全部匹配。C++ 标准库没现成实现,但可以手写轻量版——重点不是堆功能,而是控制节点内存布局和指针访问局部性。
- 每个 Trie 节点只存
children[256](ASCII 场景下用数组比std::map<char node></char>快 3–5 倍) - fail 指针在构建后批量 BFS 初始化,避免递归调用栈开销
- 匹配时用 while 循环跳 fail 链,不递归;每次命中立即记录位置,不等完整遍历结束
如何让 AC 自动机支持中文和 UTF-8 编码?
直接按字节索引 children[256] 会崩:一个汉字占 3 字节,单字节查表必然错配。正确做法是预处理输入字符串,转成 Unicode code point 序列(如用 utf8cpp 库的 utf8::utf8to32),再以 int32_t 为 key 构建 Trie。
但代价是内存翻倍、插入变慢。更务实的折中方案是:保持字节级 Trie,但对输入做「UTF-8 安全切分」——即只在合法 UTF-8 字符边界处跳转,用 utf8::next 辅助移动指针,匹配逻辑仍跑在原始字节数组上。
- 不要用
std::wstring或std::u32string做中间容器,转换开销大且易出编码错 - 敏感词词典必须统一用 UTF-8 存储并校验(可用
utf8::is_valid初始化时检查) - 若业务只要匹配常见中文词(GB2312 覆盖率 >95%),可考虑双字节映射表,比全 Unicode 轻量
过滤时怎么避免误伤“苹果手机”里的“苹果”?
纯 AC 匹配只能定位子串位置,是否过滤取决于上下文策略。常见需求有三类:whole_word(整词)、prefix_only(仅前缀)、allow_in_url(URL 中忽略)。这些不能靠正则后处理,否则破坏 AC 的 O(M) 性能优势。
解决方案是在匹配阶段就携带上下文信息:扫描时同步维护前一个字符类别(字母/数字/标点/空白),命中后查该位置前后字符是否满足整词条件(比如前后都是 isspace 或字符串边界)。关键点是把判断逻辑内联进匹配循环,而非事后遍历结果。
- 用
uint8_t context_flags[2]缓存当前和前一字符类型,查表比std::isalnum快 - 整词匹配失败时不返回结果,也不中断扫描——AC 的核心价值是「不停机持续流式处理」
- 若需保留原文但替换为 * 号,直接用
std::string::replace原地操作,避免构造新字符串
多线程环境下如何安全复用 AC 自动机实例?
AC 自动机本身是只读结构(Trie + fail 指针构建完就固定),但匹配过程中的状态变量(如当前节点指针、临时 buffer)必须每线程独享。错误做法是给每个请求 new 一个 Automaton 实例——Trie 占内存大,频繁构造析构反而拖慢性能。
正确姿势是全局单例 Trie,每个线程持有一个 Matcher 对象,内部只存 Node* current 和小块栈 buffer(std::array<int></int> 足够应付绝大多数敏感词长度)。初始化时用 thread_local static Matcher matcher;,无需锁。
- 禁止在匹配过程中修改 Trie 结构(包括 add_word),动态增删必须走重建流程
- 如果要用 mmap 加载词典,确保
mmap的内存页是MAP_PRIVATE且只读,防止 copy-on-write 开销 - 调试时注意:
valgrind --tool=helgrind能抓到隐式共享状态,比如误把current放成 static 变量
真正难的不是写对算法,而是让 fail 指针跳转不 cache miss、让 UTF-8 解码不 malloc、让线程局部状态不被编译器优化掉。这些细节不压测根本看不出来。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











