一、什么是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指针指向最长的相同后缀对应的节点。

构建规则:

  1. 根节点的fail指针指向自己
  2. 第一层节点的fail指针指向根节点
  3. 其他节点通过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辅助)
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐