实现 敏感词过滤 我们用的是DFA思想,就是提前构建好一个Trie树(前缀树),让指定词在前缀树中搜索,搜索过程类似于KMP算法,找到了就是敏感词,否则就不是。那么Trie树是什么呢?
Trie树 是一个数据结构,一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
例如:我们将ABD、ABE、AC三个字符串按Trie树存储(上图)。由于ABD和ABE有相同的前缀AB,因此我们将D、E分别放在B节点下,这样就避免了相同前缀的重复查找,从而提高查找效率。
Trie树可以最大限度地减少无谓的字符串比较,可以用于词频统计和大量字符串排序,其最坏情况时间复杂度比hash表好。但它是一个以 空间换时间 的算法,在查找效率高的前提下也要消耗一定的内存。
DFA(Deterministic Finite Automaton):确定有穷自动机。表现为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。由Trie树为基础可以构造出Trie图。
代码实现:
过滤敏感词首先需要将库里的敏感词构建成Trie图,再根据指定词的字符逐个获取,直至结束。
- 首先定义一个用于存储每个敏感词字符结点的数据结构,通过该节点可以指向该敏感词的下一个敏感字符,每个节点需要一个 isEnd 字段标识,用于识别该字符是否是敏感词的最后一个字符。
import java.util.HashMap;
import java.util.Map;
public class WordTree {
private Map<Character, WordTree> treeMap;
private boolean isEnd;
public WordTree() {
this.treeMap = new HashMap<>();
}
public void addChar(char c) {
this.treeMap.put(c, new WordTree());
}
public WordTree get(char c) {
return treeMap.get(c);
}
public boolean containsChar(char c) {
return this.treeMap.containsKey(c);
}
public void setEnd() {
this.isEnd = true;
}
public boolean isEnd() {
return isEnd;
}
@Override
public String toString() {
return "WordTree [treeMap=" + treeMap + ", isEnd=" + isEnd + "]";
}
}
- 初始化 敏感词树
/**
* 敏感词树
*/
private static WordTree sensitiveWords;
static {
sensitiveWords = new WordTree();
constructTree("src/sensitive1.txt");
}
private static void constructTree(String path) {
Set<String> words;
try {
words = readSensitiveFile(path);
} catch (Exception e) {
log.error("e -> ", e);
return;
}
// 构建前缀树
WordTree tree = sensitiveWords;
for (String elem : words) {
WordTree tTree = tree;
for (int i = 0; i < elem.length(); i++) {
char c = elem.charAt(i);
if (!tTree.containsChar(c)) {
tTree.addChar(c);
}
tTree = tTree.get(c);
if (i == elem.length() - 1) {
tTree.setEnd();
}
}
}
log.info("敏感词树构建结束");
}
private static Set<String> readSensitiveFile(String path) throws IOException {
File file = new File(path);
if (!file.exists()) {
log.error("敏感词文件不存在");
throw new FileNotFoundException();
}
// 读取文件中敏感词
Set<String> words = new HashSet<>();
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
while (null != (line = reader.readLine()) && line.trim().length() > 0) {
words.add(line.trim());
}
reader.close();
log.info("敏感词文件读取结束");
return words;
}
- 实现敏感词查找
/**
* 无意义字符正则
*/
private static final String meaninglessRegex = "[^(a-zA-Z0-9\\u4e00-\\u9fa5)+]";
/**
* 检查敏感词
*/
public static Set<String> checkSensitive(String sentence) {
final Set<String> output = new HashSet<>();
if (sentence != null && sentence.length() > 0) {
// 去除无意义的字符
sentence = sentence.replaceAll(meaninglessRegex, "");
int start = 0;
boolean lastCharIsSensitive = false; // 标识上一个次是否为敏感词
WordTree initTree = sensitiveWords;
WordTree tree = initTree;
for (int i = 0; i < sentence.length(); i++) {
char c = sentence.charAt(i);
// 本次的词与上一敏感词无关,则重头开始
if (lastCharIsSensitive && !tree.containsChar(c)) {
tree = initTree.get(c);
start = i;
} else {
tree = tree.get(c);
}
if (tree == null) {
start = i + 1;
tree = initTree;
lastCharIsSensitive = false;
} else {
lastCharIsSensitive = true;
if (tree.isEnd()) {
output.add(sentence.substring(start, i+1));
start = i + 1;
tree = initTree;
}
}
}
}
return output;
}