- 網上過濾敏感詞工具類有的存在挺多bug,這是我自己改用的過濾敏感詞工具類,目前來說沒啥bug,如果有bug歡迎在評論指出
- 使用前綴樹
Trie
實現的過濾敏感詞,樹節點用靜態內部類表示了,都寫在一個 SensitiveFilter
一個文件里了
package top.linzeliang.util;
import org.apache.commons.lang3.CharUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
/**
* 敏感詞過濾
*
* @Author: linzeliang
* @Date: 2021/12/8
*/
@Component
public class SensitiveFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(SensitiveFilter.class);
/**
* 替換符
*/
private static final String REPLACEMENT = "*";
/**
* 根節點,根節點是不帶值的
*/
private final TrieNode ROOT_NODE = new TrieNode();
/**
* 初始化前綴樹,讀取敏感詞文件構造前綴樹
*
* @date 2021/12/9
*/
@PostConstruct
private void init() {
try (
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))
) {
String keyword;
// 每次讀取一行
while ((keyword = reader.readLine()) != null) {
// 添加到前綴樹
this.addKeyword(keyword);
}
} catch (IOException e) {
LOGGER.error("加載敏感詞文件失敗: " + e.getMessage());
}
}
/**
* 將一個敏感詞添加到前綴樹中
*
* @param keyword 敏感詞
* @date 2021/12/9
*/
private void addKeyword(String keyword) {
TrieNode tempNode = ROOT_NODE;
for (int i = 0; i < keyword.length(); i++) {
//獲取單個字符
char c = keyword.charAt(i);
// 先查詢是否存在,就是是否有這個開頭的敏感詞
TrieNode subNode = tempNode.getSubNode(c);
// 如果子節點中不存在,就新建,並且添加到tempNode的子節點
if (null == subNode) {
subNode = new TrieNode();
tempNode.addSubNodes(c, subNode);
}
// 標記一下最后一個節點,即葉子節點
if (i == keyword.length() - 1) {
subNode.setKeywordEnd(true);
}
// 將指針指向子節點
tempNode = subNode;
}
}
/**
* 過濾敏感詞
*
* @param text 待過濾文本
* @return java.lang.String
* @date 2021/12/9
*/
public String filter(String text) {
// 過濾文本為空返回 null
if (StringUtils.isBlank(text)) {
return null;
}
// 指針1,剛開始指向根節點
TrieNode tempNode = ROOT_NODE;
// 指針2
int start = 0;
// 指針3
int end = 0;
// 過濾結果
StringBuilder sb = new StringBuilder();
// 當指針3未到字符串末尾時,都進行過濾
while (end < text.length()) {
// 獲取待過濾的每個字符
char c = text.charAt(end);
// 如果是無效符號就跳過
if (isSymbol(c) && end != text.length() - 1) {
// 若指針1處於根節點,就將此符號計入結果,讓指針2向下走一步
if (tempNode == ROOT_NODE) {
sb.append(c);
start++;
}
// 無論符號在開頭或中間,指針3都向下走一步
end++;
continue;
}
// 查看敏感字符對應的子節點是否存在
tempNode = tempNode.getSubNode(c);
// 如果沒有敏感詞對應的子節點,說明不包含,因此跳過這個字符
if (tempNode == null) {
// 以begin開頭的字符串不是敏感詞
sb.append(text.charAt(start));
// start 和 begin 都進入下一個位置
end = ++start;
// 重新指向根節點
tempNode = ROOT_NODE;
} else if (tempNode.isKeywordEnd()) {
// 遇到敏感詞結束標識,即發現敏感詞,將begin~position字符串替換掉
for (int i = start; i <= end; i++) {
sb.append(REPLACEMENT);
}
// 進入下一個位置
start = ++end;
// 重新指向根節點
tempNode = ROOT_NODE;
} else {
// 如果找到了敏感字符,但是又沒結束,因此繼續檢查下一個字符
// 如果當前 start 字符到 end 末尾字符沒有識別出敏感詞,那么就從 start 的下一個開始進行查找
if (end < text.length() - 1) {
end++;
} else {
// 這里還是指向 start,並沒有加 1,因為下一步循環就進入到 tempNode == null 判斷里面了
// 因此 start 和 end 都會加 1,同時上一個字符也會被加入到sb中
end = start;
}
}
}
// 將最后一批字符計入結果
sb.append(text.substring(start));
return sb.toString();
}
/**
* 判斷是否為符號
*
* @param c 待判斷符號
* @return boolean
* @date 2021/12/9
*/
private boolean isSymbol(Character c) {
// 0x2E80~0x9FFF 是東亞文字范圍
return !CharUtils.isAsciiAlphanumeric(c) && (c < 0x2E80 || c > 0x9FFF);
}
/**
* 前綴樹節點
* 因為不需要用到外部類SensitiveFilter,所以設置成靜態的就行,能提高性能
*/
private static class TrieNode {
/**
* 關鍵詞結束標識符
*/
private boolean isKeywordEnd;
/**
* 存放子節點
* 因為子節點集合是固定的,只會往這個集合增刪元素,而不會改變這個集合指針指向,所以使用final
*/
private final Map<Character, TrieNode> subNodes;
public TrieNode() {
this.isKeywordEnd = false;
this.subNodes = new HashMap<>();
}
public boolean isKeywordEnd() {
return isKeywordEnd;
}
public void setKeywordEnd(boolean keywordEnd) {
isKeywordEnd = keywordEnd;
}
/**
* 添加子節點
*
* @param c 節點名稱
* @param node 節點
* @date 2021/12/9
*/
public void addSubNodes(Character c, TrieNode node) {
subNodes.put(c, node);
}
/**
* 獲取子節點
*
* @param c 查詢的字符
* @return top.linzeliang.community.util.SensitiveFilter.TrieNode
* @date 2021/12/9
*/
public TrieNode getSubNode(Character c) {
return subNodes.get(c);
}
}
}