AC自动机与敏感词检查
·
一、什么是AC自动机
AC自动机(Aho-Corasick Automaton)是由Alfred V. Aho和Margaret J. Corasick在1975年发明的多模式串匹配算法。它能在一个文本中同时查找多个模式串,时间复杂度为O(n),其中n是文本长度。
典型应用场景:
- 敏感词过滤
- 关键词高亮
- 日志分析
- 入侵检测系统(IDS)
- 搜索引擎
二、核心原理
AC自动机 = Trie树 + KMP的失败指针(Fail指针)
2.1 构建过程
第一步:构建Trie树
将所有模式串插入Trie树中。
模式串: ["she", "he", "her", "his"]
root
/ \
s h
| | \
h e i
| | |
e r s
第二步:构建Fail指针
Fail指针指向最长的相同后缀对应的节点。
构建规则:
- 根节点的fail指针指向自己
- 第一层节点的fail指针指向根节点
- 其他节点通过BFS构建:
- 当前节点为u,其父节点为p,边的字符为c
- fail[u] = fail[p]沿着fail指针找到第一个有c边的节点
示例(简化):
节点"she"中的'e' → fail指向 "he"中的'e'
节点"her"中的'e' → fail指向 "he"中的'e'
第三步:匹配过程
文本: "ushers"
1. u → 不匹配,停留在root
2. s → 匹配到"s"节点
3. h → 匹配到"sh"节点
4. e → 匹配到"she"节点 ✓ 找到匹配!
同时通过fail指针检查"he" ✓ 也匹配!
5. r → 匹配到"her" ✓ 找到匹配!
6. s → 继续...
三、代码实现
3.1 Python实现
from collections import deque, defaultdict
class ACAutomaton:
def __init__(self):
# Trie树节点结构
self.goto = defaultdict(dict) # goto[state][char] = next_state
self.fail = {} # fail[state] = fail_state
self.output = defaultdict(list) # output[state] = [matched_patterns]
self.state_count = 0
def add_pattern(self, pattern):
"""添加模式串到Trie树"""
state = 0
for char in pattern:
if char not in self.goto[state]:
self.state_count += 1
self.goto[state][char] = self.state_count
state = self.goto[state][char]
self.output[state].append(pattern)
def build(self):
"""构建fail指针"""
queue = deque()
# 第一层节点的fail指向root(0)
for char, state in self.goto[0].items():
self.fail[state] = 0
queue.append(state)
# BFS构建其他节点的fail指针
while queue:
current_state = queue.popleft()
for char, next_state in self.goto[current_state].items():
queue.append(next_state)
# 沿着fail链找到第一个有该字符转移的状态
fail_state = self.fail[current_state]
while fail_state != 0 and char not in self.goto[fail_state]:
fail_state = self.fail[fail_state]
self.fail[next_state] = self.goto[fail_state].get(char, 0)
# 合并输出(继承fail指针指向节点的输出)
self.output[next_state].extend(self.output[self.fail[next_state]])
def search(self, text):
"""在文本中查找所有模式串"""
results = []
state = 0
for i, char in enumerate(text):
# 沿着fail链找到可以转移的状态
while state != 0 and char not in self.goto[state]:
state = self.fail[state]
state = self.goto[state].get(char, 0)
# 记录所有匹配
if self.output[state]:
for pattern in self.output[state]:
results.append({
'pattern': pattern,
'position': i - len(pattern) + 1,
'end': i + 1
})
return results
def replace(self, text, replacement='*'):
"""替换文本中的敏感词"""
results = self.search(text)
if not results:
return text
# 从后往前替换,避免索引变化
text_list = list(text)
for match in sorted(results, key=lambda x: x['position'], reverse=True):
start = match['position']
end = match['end']
text_list[start:end] = replacement * (end - start)
return ''.join(text_list)
# 使用示例
ac = ACAutomaton()
patterns = ["她", "他妈", "傻*", "f*ck"]
for pattern in patterns:
ac.add_pattern(pattern)
ac.build()
text = "她真是他妈的傻*"
print(ac.search(text))
# [{'pattern': '她', 'position': 0, 'end': 1},
# {'pattern': '他妈', 'position': 3, 'end': 5}, ...]
print(ac.replace(text))
# "**是***的**"
3.2 Java实现
import java.util.*;
public class ACAutomaton {
private static class Node {
Map<Character, Node> children = new HashMap<>();
Node fail;
List<String> outputs = new ArrayList<>();
}
private Node root = new Node();
public void addPattern(String pattern) {
Node current = root;
for (char c : pattern.toCharArray()) {
current = current.children.computeIfAbsent(c, k -> new Node());
}
current.outputs.add(pattern);
}
public void build() {
Queue<Node> queue = new LinkedList<>();
root.fail = root;
// 第一层fail指向root
for (Node node : root.children.values()) {
node.fail = root;
queue.offer(node);
}
// BFS构建fail指针
while (!queue.isEmpty()) {
Node current = queue.poll();
for (Map.Entry<Character, Node> entry : current.children.entrySet()) {
char c = entry.getKey();
Node child = entry.getValue();
queue.offer(child);
Node failNode = current.fail;
while (failNode != root && !failNode.children.containsKey(c)) {
failNode = failNode.fail;
}
child.fail = failNode.children.getOrDefault(c, root);
child.outputs.addAll(child.fail.outputs);
}
}
}
public List<Match> search(String text) {
List<Match> results = new ArrayList<>();
Node current = root;
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
while (current != root && !current.children.containsKey(c)) {
current = current.fail;
}
current = current.children.getOrDefault(c, root);
for (String pattern : current.outputs) {
results.add(new Match(pattern, i - pattern.length() + 1));
}
}
return results;
}
static class Match {
String pattern;
int position;
Match(String pattern, int position) {
this.pattern = pattern;
this.position = position;
}
}
}
四、与其他方案对比
| 方案 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 正则表达式 | O(m×n) | O(m) | 实现简单、灵活 | 性能差、易超时 | 词库<100 |
| 暴力遍历 | O(m×n×k) | O(1) | 无需预处理 | 极慢 | 不推荐 |
| HashMap查找 | O(n×L) | O(m×L) | 简单快速 | 无法处理重叠 | 精确匹配 |
| Trie树 | O(n×L) | O(m×L×Σ) | 前缀共享 | 需要遍历所有前缀 | 精确前缀 |
| AC自动机 | O(n+m) | O(m×L×Σ) | 最优性能 | 实现复杂 | 大规模词库 |
| DFA | O(n) | O(m×L×Σ) | 最快匹配 | 构建慢 | 静态词库 |
说明:
- n: 文本长度
- m: 模式串数量
- k: 平均模式串长度
- L: 最长模式串长度
- Σ: 字符集大小
实际性能测试
# 测试环境:10000个敏感词,100KB文本
正则表达式: ~5000ms
HashMap遍历: ~800ms
Trie树: ~200ms
AC自动机: ~50ms
五、推荐组件与词库
5.1 Python组件
1. pyahocorasick
pip install pyahocorasick
import ahocorasick
# 创建自动机
A = ahocorasick.Automaton()
# 添加词
words = ['fuck', 'shit', 'damn']
for idx, word in enumerate(words):
A.add_word(word, (idx, word))
# 构建
A.make_automaton()
# 搜索
text = "fuck this shit"
for end_index, (idx, word) in A.iter(text):
start_index = end_index - len(word) + 1
print(f"Found '{word}' at {start_index}")
优点: C实现、速度极快、支持Unicode
2. flashtext
pip install flashtext
from flashtext import KeywordProcessor
processor = KeywordProcessor()
processor.add_keywords_from_list(['敏感词1', '敏感词2'])
# 查找
processor.extract_keywords('文本中包含敏感词1')
# 替换
processor.replace_keywords('文本中包含敏感词1')
优点: 简单易用、性能优秀
5.2 Java组件
1. aho-corasick
<dependency>
<groupId>org.ahocorasick</groupId>
<artifactId>ahocorasick</artifactId>
<version>0.6.3</version>
</dependency>
Trie trie = Trie.builder()
.addKeyword("敏感词")
.build();
Collection<Emit> emits = trie.parseText("包含敏感词的文本");
2. Hutool (SensitiveUtil)
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
SensitiveUtil.init("sensitive_words.txt");
String result = SensitiveUtil.sensitiveFilter("文本");
5.3 JavaScript/Node.js组件
aho-corasick-node
npm install aho-corasick-node
const AhoCorasick = require('aho-corasick-node');
const ac = new AhoCorasick(['bad', 'word']);
ac.search('this is a bad word'); // [{word: 'bad', index: 10}, ...]
5.4 推荐敏感词库
| 来源 | 规模 | 地址 | 说明 |
|---|---|---|---|
| sensitive-words | 10万+ | GitHub | 综合词库 |
| sensitive-word-filter | 5万+ | GitHub | 分类词库 |
| ToolGood.Words | 20万+ | GitHub | .NET/.Core |
六、实战优化建议
6.1 处理中文特殊情况
class ChineseACAutomaton(ACAutomaton):
def normalize(self, text):
"""中文标准化"""
# 繁体转简体
text = HanziConv.toSimplified(text)
# 全角转半角
text = self.full_to_half(text)
# 去除特殊字符
text = re.sub(r'[^\w]', '', text)
return text
def search(self, text):
normalized = self.normalize(text)
return super().search(normalized)
6.2 性能优化
# 1. 词库预加载
class SensitiveFilter:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.ac = ACAutomaton()
cls._instance.load_words()
return cls._instance
# 2. 使用缓存
from functools import lru_cache
@lru_cache(maxsize=10000)
def check_sensitive(text):
return ac.search(text)
# 3. 异步处理
import asyncio
async def filter_batch(texts):
return [ac.replace(text) for text in texts]
6.3 误杀处理
# 白名单机制
WHITE_LIST = {'张三丰', '李世民'} # 可能包含敏感词但是正常的词
def smart_filter(text):
# 先检查白名单
for white in WHITE_LIST:
if white in text:
text = text.replace(white, '【WHITE】')
# 过滤
result = ac.replace(text)
# 还原白名单
result = result.replace('【WHITE】', white)
return result
七、总结
AC自动机是处理大规模敏感词过滤的最佳选择
使用建议:
- 词库 < 100:可用正则或HashMap
- 词库 100-10000:推荐AC自动机
- 词库 > 10000:AC自动机 + 索引优化
最佳实践:
- Python:
pyahocorasick+ 自定义词库 - Java:
Hutool或aho-corasick - 定期更新词库
- 添加白名单机制
- 考虑语义分析(AI辅助)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)