基于前缀树(Trie)的敏感词过滤器实现详解

在实际开发中,尤其是社区、评论、社交等平台,敏感词过滤是保障平台内容合规的重要手段。今天我们来介绍一个基于 Java 编写的 敏感词过滤器,它采用了经典的 前缀树(Trie)算法,可以高效识别并屏蔽文本中的敏感词。

一、前缀树简介

前缀树(Trie)是一种树形结构,主要用于快速查找和匹配字符串集合。其本质上是一个多叉树,每个节点代表一个字符,路径从根节点到某个节点的字符拼接起来,组成了一个单词或前缀。

它的优点:

  • 查询速度快:相比暴力匹配更高效,适用于敏感词过滤这种需要频繁查询的场景。
  • 内存可控:适合大量词汇管理。

二、项目结构概览

我们通过一个类 SensitiveFilter 实现敏感词过滤,核心逻辑包括:

  • 构建前缀树
  • 检测敏感词
  • 替换敏感词

完整代码如下:

public class SensitiveFilter {

    // 定义常量,检测到敏感词,变成该常量
    private static final String REPLACEMENT = "***";
    //根节点
    private TrieNode rootNode = new TrieNode();

    //容器实例化bean后(服务启动后就初始化了),调用构造器之后,该方法就被调用
    @PostConstruct
    public void init() {
        // 这里写死敏感词列表
        String[] keywords = {"敏感词", "违法", "暴力"};
        for (String keyword : keywords) {
            this.addKeyword(keyword);
        }
    }
    
    // 将一个敏感词加到前缀树中
    private void addKeyword(String keyword) {
        TrieNode tempNode = rootNode;
        for (int i = 0; i < keyword.length(); i++) {
            char c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
            if (subNode == null) {
                //初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNode(c, subNode);
            }
            //指向子节点,进入下一轮循环
            tempNode = subNode;
            // 设置结束标识
            if (i == keyword.length() - 1) {
                tempNode.setKeywordEnd(true);
            }
        }
    }

    // 前缀树内部类
    private class TrieNode {
        //关键词结束标识 标记是否结束
        private boolean isKeywordEnd = false;
        // 子节点(key是下级字符,value是下级节点)
        private Map<Character, TrieNode> subNodes = new HashMap<>();
        //子节点映射

        public boolean isKeywordEnd() {
            return isKeywordEnd;
        }

        public void setKeywordEnd(boolean keywordEnd) {
            isKeywordEnd = keywordEnd;
        }

        // 添加子节点
        public void addSubNode(Character c, TrieNode node) {
            subNodes.put(c, node);
        }

        // 获取子节点
        public TrieNode getSubNode(Character c) {
            return subNodes.get(c);
        }


    }

    //判断是否为符号
    private boolean isSymbol(Character c) {
        // 0x2E80~0x9FFF 是东亚文字范围
        //如果是特殊字符,返回flase
        return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
    }

    public String filter(String text) {
        StringBuilder res = new StringBuilder();
        int start = 0;
        int current = 0;
        //start记录的是敏感词开始的位置,current记录的是当前检查的位置
        TrieNode temp = rootNode;
        while (current < text.length()) {
            char c = text.charAt(current);
            boolean symbol = isSymbol(c);
            if (symbol) {
                //如果是特殊字符就直接跳过了
                if (temp == rootNode) {
                    res.append(c);
                    start++;
                }
                current++;
                continue;
            }
            temp = temp.getSubNode(c);
            if (temp == null) {
                res.append(text.charAt(start));
                start++;
                current = start;
                temp = rootNode;//恢复到根结点
            } else if (temp.isKeywordEnd()) {
                res.append(REPLACEMENT);
                current++;
                start = current;
            } else {
                current++;
            }
        }
        res.append(text.substring(start)); // 处理剩余字符
        return res.toString();
    }
}

三、核心实现详解

1. TrieNode:前缀树节点定义

private class TrieNode {
    private boolean isKeywordEnd = false;
    private Map<Character, TrieNode> subNodes = new HashMap<>();
    ...
}

每个节点包含两个核心属性:

  • isKeywordEnd:是否是一个关键词的结尾;
  • subNodes:当前节点的子节点(映射关系为“字符 -> 子节点”)。
    这里使用HashMap构建父节点和子节点关系,实现O(1)的查询速度。

2. 初始化敏感词列表(@PostConstruct

@PostConstruct
public void init() {
    String[] keywords = {"敏感词", "违法", "暴力"};
    for (String keyword : keywords) {
        this.addKeyword(keyword);
    }
}

这段代码在 Bean 初始化后执行,将敏感词加入前缀树。实际项目中,这里可以替换为从数据库、配置文件或远程接口动态加载。

3. 添加关键词到前缀树:addKeyword()

private void addKeyword(String keyword) {
    TrieNode tempNode = rootNode;
    ...
}

我们从根节点开始,将每个字符添加为子节点。如果该字符已存在,就继续向下;否则创建新节点。如果当前字符是最后一个字符,则设置 isKeywordEnd = true

4. 判断字符是否为符号:isSymbol()

private boolean isSymbol(Character c) {
    return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}

此方法用于跳过非中文、英文、数字的符号字符,避免影响匹配。例如:“暴!力” 仍可被识别为敏感词“暴力”。

5. 敏感词过滤主逻辑:filter()

public String filter(String text) {
    StringBuilder res = new StringBuilder();
    int start = 0;
    int current = 0;
    TrieNode temp = rootNode;
    ...
}
指针说明:
  • start:记录敏感词匹配的起始位置;
  • current:当前扫描位置;
  • temp:当前所处前缀树节点,如果当temp等于根结点,那么表示当前并没有扫描到敏感词汇。
核心流程:
  1. 跳过符号:当字符是符号时,如果在根节点(也就是说现在并没有检测到敏感词汇),直接追加到结果;

  2. 前缀匹配

    • getSubNode(c) 返回 null,说明匹配失败,这个字符并不是敏感词的第一个字符,把第start个字符加到结果集中,同时startcurrent都+1从而检测下一个字符,而且要执行 temp = rootNode;,表示恢复到根结点。
    • isKeywordEnd() 为 true,表示这个时候已经检测完毕,也就是说匹配到了敏感词汇的最后一个字符,这个时候startcurrent是不相等的,直接把我们定义的常量加到最后的结果当中即可,输出替换字符串(如“***”);
    • 第三个分支是,我们现在已经检测到了敏感词汇,但是并没有结束,后面还会有,所以我们只需要先让current++检测下一个字符即可,而不需要管start指针。
  3. 剩余字符追加:处理未匹配的最后部分。

示例

String text = "这里含有违法内容和暴力事件!";
System.out.println(filter(text));

输出:

这里含有***内容和***事件!

四、总结

本敏感词过滤器采用前缀树实现,兼顾了性能与可维护性,是实际项目中一种非常实用的方式。配合业务需求可进一步增强,比如接入管理后台维护词库、记录触发日志等。

你可以将该模块封装为一个 Spring Boot Starter,供多个服务复用,也可以部署为独立服务接收 HTTP 请求进行敏感词过滤,提升系统解耦性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值