SpringBoot DFA 实现敏感词过滤

前言

敏感词过滤系统已经成为各大平台的必备功能。无论是社交平台的内容审核、电商系统的商品管理,还是游戏系统的聊天监控,都需要高效可靠的敏感词过滤机制来维护健康的内容生态。

传统的字符串查找方式在处理大量敏感词时性能急剧下降,而正则表达式在匹配复杂规则时更是捉襟见肘。

今天,介绍一种基于 DFA(有限状态自动机)算法的高效敏感词过滤方案,通过 Trie 树数据结构实现毫秒级响应,轻松应对敏感内容的实时过滤需求。


为什么需要 DFA 算法?

传统方案的性能瓶颈

在敏感词过滤的实际应用中,我们面临着诸多挑战。传统的实现方式往往存在以下问题:

暴力匹配的低效

java 复制代码
// 时间复杂度:O(n × m × k)
// 随着敏感词数量增加,性能急剧下降
for (String word : sensitiveWords) {
    if (text.contains(word)) {
        // 处理敏感词
    }
}

这种简单粗暴的方式在小规模应用中尚可接受,但当敏感词数量达到数千甚至数万时,处理一篇1000字的文章可能需要数秒时间,严重影响用户体验。

正则表达式的局限性 虽然正则表达式提供了强大的模式匹配能力,但在敏感词过滤场景中却存在明显缺陷:

  • 构建超大的正则表达式会导致编译时间过长
  • 动态添加敏感词需要重新编译正则表达式
  • 内存占用随敏感词数量线性增长
  • 匹配效率在复杂规则下大幅下降

DFA 算法的优势

DFA 算法通过构建状态机模型,将敏感词匹配过程转化为状态转移,带来了质的飞跃:

线性时间复杂度 : O(n) - 只需遍历文本一次,不受敏感词数量影响 ✅ 空间共享优化 : 敏感词前缀共享存储,显著减少内存占用 ✅ 确定性匹配 : 无需回溯,避免正则表达式的回溯开销 ✅ 动态扩展友好: 支持运行时添加、删除敏感词,无需重建整个数据结构

以实际场景为例,当敏感词库从1000个扩展到10000个时:

  • 暴力匹配算法耗时增加约10倍
  • DFA算法耗时几乎保持不变

DFA 算法原理解析

DFA 算法的数学本质

DFA(Deterministic Finite Automaton)是一种数学模型,它定义了一个五元组 M = (Q, Σ, δ, q₀, F):

  • Q: 有限状态集合
  • Σ: 输入字母表
  • δ: 状态转移函数 δ: Q × Σ → Q
  • q₀: 初始状态
  • F: 接受状态集合

在敏感词过滤中,每个状态代表匹配到某个字符位置的状态转移路径。

Trie 树的可视化理解

Trie 树是 DFA 的直观实现,也称为字典树或前缀树。让我们通过一个具体例子来理解:

假设有以下敏感词:["apple", "app", "application", "apply", "orange"]

构建的 Trie 树结构如下:

css 复制代码
root
├── a
│   └── p
│       └── p [结束]  ← "app"
│           └── l
│               ├── e [结束]  ← "apple"
│               ├── i
│               │   └── c
│               │       └── a
│               │           └── t
│               │               └── i
│               │                   └── o
│               │                       └── n [结束]  ← "application"
│               └── y [结束]  ← "apply"
└── o
    └── r
        └── a
            └── n
                └── g
                    └── e [结束]  ← "orange"

Trie 树结构的关键特征

1. 前缀共享优化

  • "app" 作为 "apple"、"application"、"apply" 的共同前缀,只存储一次
  • 从 "app" 节点分支出不同的后续字符路径

2. 状态转移路径

  • 每个 ├──└── 表示一个字符状态转移
  • [结束] 标记表示 DFA 的接受状态(敏感词结束)

3. 空间效率

  • 5个敏感词只需要存储 10 个字符节点(含重复字符)
  • 如果单独存储需要 5 + 3 + 11 + 4 + 6 = 29 个字符

4. 查找效率

  • 查找 "application" 只需要 11 次字符比较
  • 查找 "app" 只需要 3 次字符比较且可以提前终止

关键特征解释

1. 前缀共享: "app" 作为 "apple"、"application"、"apply" 的前缀,只在树中存储一次

2. 节点状态: 每个节点代表 DFA 的一个状态

3. 边转移: 每条边代表一个字符输入引起的状态转移

4. 接受状态: 标记 * 的节点表示敏感词的结束状态(接受状态)

例如,当检测文本 "I love apples" 时:

  • 从根节点开始,遇到 'a' 转移到 a 节点
  • 遇到 'p' 转移到 p 节点
  • 遇到 'p' 转移到 p 节点
  • 遇到 'l' 转移到 l 节点
  • 遇到 'e' 转移到 e 节点(到达接受状态,发现敏感词 "apple")
  • 继续检测 's' 时状态转移失败,回到初始位置继续检测

状态转移的实际过程

假设我们要在文本 "I like apples and apps" 中查找敏感词:

  1. 从根节点开始,遇到 'a',转移到 'a' 节点
  2. 遇到 'p',转移到 'p' 节点
  3. 遇到 'p',转移到 'p' 节点(此时检测到 "app" 是敏感词)
  4. 遇到 'l',转移到 'l' 节点
  5. 遇到 'e',转移到 'e' 节点(此时检测到 "apple" 是敏感词)

整个过程只需要一次线性遍历,无需重复扫描或回溯。

核心实现详解

1. Trie 节点结构设计

Trie 节点是整个 DFA 算法的基础,每个节点代表一个状态:

java 复制代码
public class TrieNode {
    // 子节点映射:字符 -> Trie节点
    private Map<Character, TrieNode> children = new HashMap<>();

    // 是否为敏感词的结束节点
    private boolean isEnd = false;

    // 完整敏感词内容(便于输出)
    private String keyword;

    public TrieNode getChild(char c) {
        return children.get(c);
    }

    public TrieNode addChild(char c) {
        return children.computeIfAbsent(c, k -> new TrieNode());
    }

    public boolean hasChild(char c) {
        return children.containsKey(c);
    }

    // getters and setters...
}

2. DFA 过滤器核心实现

以下是最精简的 DFA 过滤器实现,包含了核心的匹配逻辑:

ini 复制代码
public class SensitiveWordFilter {
    private TrieNode root;
    private int minWordLength = 1;

    public SensitiveWordFilter(List<String> sensitiveWords) {
        this.root = buildTrie(sensitiveWords);
        this.minWordLength = sensitiveWords.stream()
            .mapToInt(String::length).min().orElse(1);
    }

    /**
     * 构建 Trie 树
     */
    private TrieNode buildTrie(List<String> words) {
        TrieNode root = new TrieNode();
        for (String word : words) {
            TrieNode node = root;
            for (char c : word.toCharArray()) {
                node = node.addChild(c);
            }
            node.setEnd(true);
            node.setKeyword(word);
        }
        return root;
    }

    /**
     * 检查是否包含敏感词 - 核心 DFA 匹配算法
     */
    public boolean containsSensitiveWord(String text) {
        if (text == null || text.length() < minWordLength) {
            return false;
        }

        char[] chars = text.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            if (dfaMatch(chars, i)) {
                return true;
            }
        }
        return false;
    }

    /**
     * DFA 状态转移匹配
     */
    private boolean dfaMatch(char[] chars, int start) {
        TrieNode node = root;

        for (int i = start; i < chars.length; i++) {
            char c = chars[i];

            if (!node.hasChild(c)) {
                break; // 状态转移失败
            }

            node = node.getChild(c);

            if (node.isEnd()) {
                return true; // 到达接受状态
            }
        }
        return false;
    }

    /**
     * 查找并替换敏感词
     */
    public String filter(String text, String replacement) {
        List<SensitiveWordResult> words = findAllWords(text);

        // 从后往前替换,避免索引变化问题
        StringBuilder result = new StringBuilder(text);
        for (int i = words.size() - 1; i >= 0; i--) {
            SensitiveWordResult word = words.get(i);
            String stars = String.valueOf(replacement != null ? replacement : "*")
                .repeat(word.getEnd() - word.getStart() + 1);
            result.replace(word.getStart(), word.getEnd() + 1, stars);
        }
        return result.toString();
    }

    /**
     * 查找所有敏感词
     */
    public List<SensitiveWordResult> findAllWords(String text) {
        List<SensitiveWordResult> results = new ArrayList<>();

        if (text == null || text.length() < minWordLength) {
            return results;
        }

        char[] chars = text.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            TrieNode node = root;
            int j = i;

            while (j < chars.length && node.hasChild(chars[j])) {
                node = node.getChild(chars[j]);
                j++;

                if (node.isEnd()) {
                    results.add(new SensitiveWordResult(
                        text.substring(i, j), i, j - 1));
                }
            }
        }
        return results;
    }
}

3. 敏感词结果封装

java 复制代码
public class SensitiveWordResult {
    private String word;      // 敏感词内容
    private int start;        // 起始位置
    private int end;          // 结束位置

    public SensitiveWordResult(String word, int start, int end) {
        this.word = word;
        this.start = start;
        this.end = end;
    }

    // getters and toString...
}

实战应用场景

即时通讯系统中的实时过滤

在高并发的聊天系统中,敏感词过滤需要满足低延迟、高吞吐的要求:

java 复制代码
public class ChatMessageFilter {
    private SensitiveWordFilter wordFilter;

    // 异步处理敏感词检测
    private ExecutorService filterExecutor = Executors.newFixedThreadPool(10);

    public CompletableFuture<Message> filterMessageAsync(Message message) {
        return CompletableFuture.supplyAsync(() -> {
            String content = message.getContent();

            if (wordFilter.containsSensitiveWord(content)) {
                // 实时替换
                String filtered = wordFilter.filter(content, "***");
                message.setContent(filtered);

                // 记录敏感词统计
                recordSensitiveWords(content);
            }

            return message;
        }, filterExecutor);
    }

    private void recordSensitiveWords(String content) {
        List<SensitiveWordResult> words = wordFilter.findAllWords(content);
        // 统计敏感词出现频率,用于优化词库
        updateWordFrequency(words);
    }
}

内容审核系统的多级策略

不同类型的敏感词需要不同的处理策略:

java 复制代码
public class ContentAuditor {
    private SensitiveWordFilter highRiskFilter;  // 高风险词
    private SensitiveWordFilter mediumRiskFilter; // 中风险词
    private SensitiveWordFilter lowRiskFilter;   // 低风险词

    public AuditResult auditContent(String content) {
        AuditResult result = new AuditResult();

        // 按风险级别检测
        List<SensitiveWordResult> highRiskWords = highRiskFilter.findAllWords(content);
        if (!highRiskWords.isEmpty()) {
            result.setStatus(AuditStatus.REJECT);
            result.setReason("包含高风险敏感词");
            return result;
        }

        List<SensitiveWordResult> mediumRiskWords = mediumRiskFilter.findAllWords(content);
        if (!mediumRiskWords.isEmpty()) {
            result.setStatus(AuditStatus.MANUAL_REVIEW);
            result.setReason("包含中风险敏感词,需要人工审核");
            return result;
        }

        List<SensitiveWordResult> lowRiskWords = lowRiskFilter.findAllWords(content);
        if (!lowRiskWords.isEmpty()) {
            // 低风险词汇直接过滤
            String filtered = lowRiskFilter.filter(content, "***");
            result.setFilteredContent(filtered);
            result.setStatus(AuditStatus.PASS_WITH_FILTER);
        } else {
            result.setStatus(AuditStatus.PASS);
        }

        return result;
    }
}

动态词库管理

实际项目中,敏感词库需要动态更新:

java 复制代码
@Service
public class SensitiveWordManager {
    private volatile SensitiveWordFilter filter;
    private ScheduledExecutorService updateExecutor =
        Executors.newSingleThreadScheduledExecutor();

    @PostConstruct
    public void init() {
        loadWords();
        // 定期更新词库
        updateExecutor.scheduleAtFixedRate(this::loadWords, 0, 1, TimeUnit.HOURS);
    }

    public void loadWords() {
        try {
            // 从数据库或配置中心加载最新词库
            List<String> words = fetchLatestWords();
            SensitiveWordFilter newFilter = new SensitiveWordFilter(words);

            this.filter = newFilter;

            log.info("敏感词库更新完成,当前词数:{}", words.size());
        } catch (Exception e) {
            log.error("词库更新失败", e);
        }
    }

    public boolean containsSensitiveWord(String text) {
        return filter != null && filter.containsSensitiveWord(text);
    }

    public String filterText(String text) {
        return filter != null ? filter.filter(text, "***") : text;
    }
}

高级优化方案

对于不同规模的敏感词过滤需求,基础的 DFA 算法可能需要进一步优化。以下是几种常用的高级方案:

方案1:双数组 Trie(Double-Array Trie)

核心思想:将 Trie 树压缩为两个数组,减小内存使用。

java 复制代码
// 双数组结构示意
int[] base = new int[size];  // 状态转移基础值
int[] check = new int[size]; // 状态转移检查值

// 状态转移:next_state = base[current_state] + char_code
// if check[next_state] == current_state: 转移成功

适用场景:词库规模较大

优点:内存占用减少 50%-80%

缺点:构建复杂度增加,动态更新困难

应用:搜索引擎、大型社交平台的离线词库

方案2:AC 自动机(Aho-Corasick)

核心思想:在 Trie 基础上增加失败指针,实现一次遍历匹配多个模式。

java 复制代码
public class AhoCorasickAutomaton {
    private Node root = new Node();

    // Trie 节点结构
    static class Node {
        Map<Character, Node> children = new HashMap<>();
        Node fail;           // 失败指针
        List<String> output = new ArrayList<>();  // 输出模式
    }

    // 构建自动机
    public void build(List<String> patterns) {
        // 1. 构建 Trie 树
        for (String pattern : patterns) {
            Node node = root;
            for (char c : pattern.toCharArray()) {
                node = node.children.computeIfAbsent(c, k -> new Node());
            }
            node.output.add(pattern);
        }

        // 2. 添加失败指针
        Queue<Node> queue = new LinkedList<>();

        // 初始化根节点的子节点
        for (Node child : root.children.values()) {
            child.fail = root;
            queue.add(child);
        }

        // BFS 构建失败指针
        while (!queue.isEmpty()) {
            Node current = queue.poll();

            for (Map.Entry<Character, Node> entry : current.children.entrySet()) {
                char c = entry.getKey();
                Node child = entry.getValue();

                // 找到失败指针
                Node failNode = current.fail;
                while (failNode != null && !failNode.children.containsKey(c)) {
                    failNode = failNode.fail;
                }

                child.fail = (failNode == null) ? root : failNode.children.get(c);
                child.output.addAll(child.fail.output);

                queue.add(child);
            }
        }
    }

    // 搜索匹配
    public List<MatchResult> search(String text) {
        List<MatchResult> results = new ArrayList<>();
        Node node = root;

        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);

            // 失败指针跳转
            while (node != null && !node.children.containsKey(c)) {
                node = node.fail;
            }

            node = (node == null) ? root : node.children.get(c);

            // 输出匹配结果
            for (String pattern : node.output) {
                results.add(new MatchResult(pattern, i - pattern.length() + 1, i));
            }
        }

        return results;
    }

    // 匹配结果
    static class MatchResult {
        String pattern;
        int start;
        int end;

        public MatchResult(String pattern, int start, int end) {
            this.pattern = pattern;
            this.start = start;
            this.end = end;
        }
    }
}

适用场景:多模式匹配、日志分析

优点:可同时匹配多个敏感词,无需多次遍历

缺点:空间复杂度较高,实现相对复杂

应用:网络安全、内容审核系统

方案3:分片 + 布隆过滤器预筛选

核心思想:通过分片降低单机压力,用布隆过滤器快速过滤明显不包含敏感词的文本。

java 复制代码
// 分片处理示例
public class ShardedFilter {
	// 模拟分片
    private List<SensitiveWordFilter> shards;
    private BloomFilter<String> preFilter;

    public boolean containsSensitive(String text) {
        // 布隆过滤器预筛选
        if (!preFilter.mightContain(text)) {
            return false; // 肯定不包含敏感词
        }

        // 分片精确匹配
        int shardIndex = text.hashCode() % shards.size();
        return shards.get(shardIndex).containsSensitiveWord(text);
    }
}

适用场景:高并发、大规模文本处理

优点:支持水平扩展,预处理可过滤大量无效请求

缺点:存在误判概率,系统复杂度增加

应用:弹幕系统、即时通讯、微服务架构

总结

DFA 算法通过 Trie 树结构,为敏感词过滤提供了一个兼具效率和准确性的解决方案。

在实际应用中,需要根据具体的业务场景选择合适的实现策略。

无论是追求极致性能的聊天系统,还是注重准确率的内容审核平台,DFA 算法都能提供坚实的基础支撑。

仓库

ruby 复制代码
https://github.com/yuboon/java-examples/tree/master/springboot-dfa
相关推荐
bcbnb5 小时前
如何解析iOS崩溃日志:从获取到符号化分析
后端
许泽宇的技术分享5 小时前
当AI学会“说人话“:Azure语音合成技术的魔法世界
后端·python·flask
用户69371750013845 小时前
4.Kotlin 流程控制:强大的 when 表达式:取代 Switch
android·后端·kotlin
用户69371750013845 小时前
5.Kotlin 流程控制:循环的艺术:for 循环与区间 (Range)
android·后端·kotlin
vx_bisheyuange5 小时前
基于SpringBoot的宠物商城网站的设计与实现
spring boot·后端·宠物
bcbnb5 小时前
全面解析网络抓包工具使用:Wireshark和TCPDUMP教程
后端
leonardee5 小时前
Spring Security安全框架原理与实战
java·后端
回家路上绕了弯6 小时前
包冲突排查指南:从发现到解决的全流程实战
分布式·后端
爱分享的鱼鱼6 小时前
部署Vue+Java Web应用到云服务器完整指南
前端·后端·全栈
麦麦麦造6 小时前
比 pip 快 100 倍!更现代的 python 包管理工具,替代 pip、venv、poetry!
后端·python