過濾敏感詞工具類SensitiveFilter


  • 網上過濾敏感詞工具類有的存在挺多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);
        }
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM