在实际开发中,尤其是社区、评论、社交等平台,敏感词过滤是保障平台内容合规的重要手段。今天我们来介绍一个基于 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等于根结点,那么表示当前并没有扫描到敏感词汇。
核心流程:
-
跳过符号:当字符是符号时,如果在根节点(也就是说现在并没有检测到敏感词汇),直接追加到结果;
-
前缀匹配:
- 若
getSubNode(c)
返回 null,说明匹配失败,这个字符并不是敏感词的第一个字符,把第start
个字符加到结果集中,同时start
和current
都+1从而检测下一个字符,而且要执行temp = rootNode;
,表示恢复到根结点。 - 若
isKeywordEnd()
为 true,表示这个时候已经检测完毕,也就是说匹配到了敏感词汇的最后一个字符,这个时候start
和current
是不相等的,直接把我们定义的常量加到最后的结果当中即可,输出替换字符串(如“***”); - 第三个分支是,我们现在已经检测到了敏感词汇,但是并没有结束,后面还会有,所以我们只需要先让
current++
检测下一个字符即可,而不需要管start
指针。
- 若
-
剩余字符追加:处理未匹配的最后部分。
示例
String text = "这里含有违法内容和暴力事件!";
System.out.println(filter(text));
输出:
这里含有***内容和***事件!
四、总结
本敏感词过滤器采用前缀树实现,兼顾了性能与可维护性,是实际项目中一种非常实用的方式。配合业务需求可进一步增强,比如接入管理后台维护词库、记录触发日志等。
你可以将该模块封装为一个 Spring Boot Starter,供多个服务复用,也可以部署为独立服务接收 HTTP 请求进行敏感词过滤,提升系统解耦性。