Echo/docs/90-过滤敏感词.md
2021-01-22 12:10:47 +08:00

205 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 过滤敏感词
---
采用数据结构 - 前缀树:
- 名称Trie、字典树、查找树
- 特点:查找效率高,消耗内存大
- 前缀树的根节点为空,其余节点只包含一个字符
- 一条路径就是一个字符串(到叶子节点)
- 每个节点的所有子节点包含的字符串不同(也即相同的要合并)
- 应用:字符串检索、词频统计、字符串排序
![](https://gitee.com/veal98/images/raw/master/img/20210121215623.png)
![](https://gitee.com/veal98/images/raw/master/img/20210121220145.png)
敏感词过滤器:
- 定义前缀树
- 根据敏感词,初始化前缀树;
- 编写过滤敏感词的方法
### 定义前缀树
```java
/**
* 定义前缀树
*/
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);
}
}
```
### 根据敏感词,初始化前缀树;
` @PostConstruct ` 在容器实例化这个类(容器在启动的时候就会实例化),并调用这个类的构造器之后(由用户调用构造器),该注解标注的方法就会被自动调用
```java
@Component
public class SensitiveFilter {
private static final Logger logger = LoggerFactory.getLogger(SensitiveFilter.class);
// 将敏感词替换成 ***
private static final String REPLACEMENT = "***";
// 根节点
private TrieNode rootNode = new TrieNode();
/**
* 初始化前缀树
*/
@PostConstruct // 初始化方法
public void init() {
try (
InputStream is = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
) {
String keyword;
while ((keyword = reader.readLine()) != null) {
// 添加到前缀树
this.addKeyword(keyword);
}
} catch (IOException e) {
logger.error("加载敏感词文件失败" + e.getMessage());
}
}
/**
* 将一个敏感词添加进前缀树中
* @param 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);
}
}
}
}
```
### 编写过滤敏感词的方法
```java
/**
* 过滤敏感词
* @param text 待过滤的文本
* @return 过滤后的文本(即用 *** 替代敏感词)
*/
public String filter(String text) {
if (StringUtils.isBlank(text)) {
return null;
}
// 指针 1前缀树的工作指针
TrieNode tempNode = rootNode;
// 指针 2指向文本中某个敏感词的第一位
int begin = 0;
// 指针 3指向文本中某个敏感词的最后一位
int end = 0;
// 记录过滤后的文本(结果)
StringBuilder sb = new StringBuilder();
while (end < text.length()) {
char c = text.charAt(end);
// 跳过符号(防止敏感词混合符号,比如 ☆赌☆博)
if (isSymbol(c)) {
// 若指针 1 处于根节点,则将此符号计入结果(直接忽略),让指针 2 向下走一步
if (tempNode == rootNode) {
sb.append(c);
begin ++;
}
// 无论符号在开头还是在中间,指针 3 都会向下走一步
end ++;
continue;
}
// 检查子节点
tempNode = tempNode.getSubNode(c);
if (tempNode == null) {
// 以指针 begin 开头的字符串不是敏感词
sb.append(text.charAt(begin));
// 进入下一位的判断
begin ++;
end = begin;
// 指针 1 重新指向根节点
tempNode = rootNode;
}
else if (tempNode.isKeywordEnd()) {
// 发现敏感词,将 begin~end 的字符串替换掉
sb.append(REPLACEMENT);
// 进入下一位的判断
end ++;
begin = end;
// 指针 1 重新指向根节点
tempNode = rootNode;
}
else {
// 检查下一个字符
end ++;
}
}
// 将最后一批字符计入结果(如果最后一次循环的字符串不是敏感词,上述的循环逻辑不会将其加入最终结果)
sb.append(text.substring(begin));
return sb.toString();
}
// 判断某个字符是否是符号
private boolean isSymbol(Character c) {
// 0x2E80~0x9FFF 是东亚文字范围
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
```