參考文章:https://blog.csdn.net/chenssy/article/details/26961957
補充說明:
1.具體的DFA介紹參考原文章,此處只是補充了文章中沒有介紹的點以及根據實際需求進行了改造
2.最大/小匹配規則:比如說存在兩個敏感詞[abc,ab],最大規則匹配中abc,最小匹配規則匹配中ab
3.添加自動忽略特殊字符的功能
/**
* 判斷是否是要忽略的字符(忽略所有特殊字符以及空格)
* @param specificChar 指定字符
* @return 特殊字符或空格true否則false
*/
private boolean isIgnore(char specificChar){
String regex = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\s*";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(String.valueOf(specificChar));
return matcher.matches();
}
實際業務需求:
一堆敏感詞文件,一個詞一行,把目錄下的所有敏感詞文件在啓動項目的時候加載進去,並且提供匹配敏感詞的方法
優化點:
做到不重啓應用更新敏感詞庫,這個需要進行對敏感詞文件的動態監控,之前使用過的FileAlterationMonitor進行動態監控文件無法滿足需求,因爲刪除文件這種變化無法讀取到文件內容,所以應該採用線程通知的方式去監控文件變化,這中方式後續有時間再研究下吧
實現代碼(採用springboot方式實現,其實就是java,但是用的springboot的@PostConstruct註解在啓動前加載敏感詞庫):
package com.holidaylee.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.io.*;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author : HolidayLee
* @description : 敏感詞過濾工具
*/
@Component
public class SensitiveWordCheckUtils {
private final Logger logger = LoggerFactory.getLogger(SensitiveWordCheckUtils.class);
/**
* 最小匹配規則
*/
private final Integer MIN_MATCH_TYPE = 0;
/**
* 最大匹配規則
*/
private final Integer MAX_MATCH_TYPE = -1;
/**
* 敏感詞DFA樹關係標記key
*/
private final String IS_END = "isEnd";
/**
* 不是敏感詞的最後一個字符
*/
private final String END_FALSE = "0";
/**
* 是敏感詞的最後一個字符
*/
private final String END_TRUE = "1";
/**
* 所有敏感詞DFA樹的列表
*/
private Map sensitiveWordMap = null;
/**
* 敏感詞文件存放路徑
*/
private final String SENSITIVE_WORD_FILE_PATH = "sensitiveWord" + File.separator;
/**
* 敏感詞文件默認編碼格式
*/
private final String DEFAULT_ENCODING = "utf-8";
/**
* 忽略特殊字符的正則表達式
*/
private final String IGNORE_SPECIAL_CHAR_REGEX = "[`~!@#$%^&*()+=|{}':;',\\\\[\\\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?]|\\s*";
/**
* 啓動前將敏感詞文件中的敏感詞構建成DFA樹
*/
@PostConstruct
private void initSensitiveWords() {
sensitiveWordMap = new ConcurrentHashMap();
File dir = new File(SENSITIVE_WORD_FILE_PATH);
if (dir.isDirectory() && dir.exists()) {
for (File file : dir.listFiles()) {
createDFATree(readSensitiveWordFileToSet(file));
logger.info(String.format("將敏感詞文件加載到DFA樹列表成功{%s}", file));
}
logger.info(String.format("總共構建%s棵DFA敏感詞樹", sensitiveWordMap.size()));
}else {
throw new RuntimeException(String.format("敏感詞文件目錄不存在{%s}",dir));
}
}
/**
* 讀取文件中的敏感詞
*
* @param file 敏感詞文件
* @return 敏感詞set集合
*/
private Set<String> readSensitiveWordFileToSet(File file) {
Set<String> words = new HashSet<>();
if (file.exists()) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), DEFAULT_ENCODING));
String line = "";
while ((line = reader.readLine()) != null) {
words.add(line.trim());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
logger.info(String.format("從文件{%s}讀取到{%s}個敏感詞", file, words.size()));
return words;
}
/**
* 將敏感詞構建成DFA樹
*{
* 出={
* isEnd=0,
* 售={
* isEnd=0,
* 手={
* isEnd=0,
* 刀={
* isEnd=1
* }
* },
* 軍={
* isEnd=0,
* 刀={
* isEnd=1
* }
* }
* }
* }
*}
* @param sensitiveWords 敏感詞列表
*/
private void createDFATree(Set<String> sensitiveWords) {
Iterator<String> it = sensitiveWords.iterator();
while (it.hasNext()) {
String word = it.next();
Map currentMap = sensitiveWordMap;
for (int i = 0; i < word.length(); i++) {
char key = word.charAt(i);
if (isIgnore(key)){
continue;
}
Object oldValueMap = currentMap.get(key);
if (oldValueMap == null) {
// 不存在以key字符的DFA樹則需要創建一個
Map newValueMap = new ConcurrentHashMap();
newValueMap.put(IS_END, END_FALSE);
currentMap.put(key, newValueMap);
currentMap = newValueMap;
} else {
currentMap = (Map) oldValueMap;
}
if (i == word.length() - 1) {
// 給最後一個字符添加結束標識
currentMap.put(IS_END, END_TRUE);
}
}
}
}
/**
* 按照最小規則獲取文本中的敏感詞(例如敏感詞有[出售,出售軍刀],則在此規則下,獲取到的敏感詞爲[出售])
*
* @param content 文本內容
* @return 文本中所包含的敏感詞set列表
*/
public Set<String> getSensitiveWordMinMatch(String content) {
return getSensitiveWord(content, MIN_MATCH_TYPE);
}
/**
* 按照最大規則獲取文本中的敏感詞(例如敏感詞有[出售,出售軍刀],則在此規則下,獲取到的敏感詞爲[出售軍刀])
*
* @param content 文本內容
* @return 文本中所包含的敏感詞set列表
*/
public Set<String> getSensitiveWordMaxMatch(String content) {
return getSensitiveWord(content, MAX_MATCH_TYPE);
}
/**
* 按照指定規則獲取文本中的敏感詞
*
* @param content 文本內容
* @return 文本中所包含的敏感詞set列表
*/
private Set<String> getSensitiveWord(String content, int matchType) {
Set<String> sensitiveWordList = new HashSet<>();
for (int i = 0; i < content.length(); i++) {
// 檢查敏感詞,長度爲0則表示文本中不包含敏感詞
int length = checkSensitiveWord(content, i, matchType);
if (length > 0) {
sensitiveWordList.add(content.substring(i, i + length));
i = i + length - 1;
}
}
return sensitiveWordList;
}
/**
* 從指定索引處檢查文本中的敏感詞
*
* @param content 文本內容
* @param beginIndex 起始索引
* @return 檢索到的敏感詞的長度
*/
private int checkSensitiveWord(String content, int beginIndex, int matchType) {
// 敏感詞結束標識位:用於敏感詞只有一個字符的情況
boolean flag = false;
// 匹配到的敏感詞的長度
int matchedLength = 0;
// 當前詞的DFA樹
Map currentWordMap = sensitiveWordMap;
for (int i = beginIndex; i < content.length(); i++) {
char key = content.charAt(i);
// 解決空格等特殊字符造成的漏匹配,比如 [軍, 刀]
if (isIgnore(key)) {
matchedLength++;
continue;
}
// 獲取當前字符的DFA樹,樹爲空則表示不存在包含該字符的敏感詞
currentWordMap = (Map) currentWordMap.get(key);
if (currentWordMap == null) {
//不存在直接返回
break;
} else {
// 存在則匹配長度+1
matchedLength++;
// 判斷是否是匹配中的敏感詞的最後一位
if (END_TRUE.equals(currentWordMap.get(IS_END))) {
flag = true;
// 如果是最小匹配規則則直接返回(例如敏感詞中有[出售,出售軍刀],當匹配到"售"字符時,如果是最小規則則不繼續向下匹配)
if (matchType == MIN_MATCH_TYPE) {
break;
}
}
}
}
// 長度小於1則表示不是單詞
if (matchedLength < 1 || !flag) {
matchedLength = 0;
}
return matchedLength;
}
/**
* 判斷是否是要忽略的字符(忽略所有特殊字符以及空格)
* @param specificChar 指定字符
* @return 特殊字符或空格true否則false
*/
private boolean isIgnore(char specificChar){
Pattern pattern = Pattern.compile(IGNORE_SPECIAL_CHAR_REGEX);
Matcher matcher = pattern.matcher(String.valueOf(specificChar));
return matcher.matches();
}
}
測試:
啓動:
單元測試:
註明:
本文爲學習記錄筆記,不喜勿噴。