java實現敏感詞過濾算法DFA

參考文章: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();
   }
}

測試:

    啓動:

    

單元測試:

   

註明:

本文爲學習記錄筆記,不喜勿噴。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章