利用利用字典樹(前綴樹)過濾敏感詞

字典樹介紹

Paste_Image.png
  • 又稱單詞查找樹,Trie樹,是一種樹形結(jié)構(gòu),是一種哈希樹的變種。典型應(yīng)用是用于統(tǒng)計,排序和保存大量的字符串(但不僅限于字符串),所以經(jīng)常被搜索引擎系統(tǒng)用于文本詞頻統(tǒng)計。它的優(yōu)點(diǎn)是:利用字符串的公共前綴來減少查詢時間,最大限度地減少無謂的字符串比較,查詢效率比哈希樹高。

特性

  • 根節(jié)點(diǎn)不包含字符,除根節(jié)點(diǎn)外每一個節(jié)點(diǎn)都只包含一個字符
  • 從根節(jié)點(diǎn)到某一節(jié)點(diǎn),路徑上經(jīng)過的字符連接起來,為該節(jié)點(diǎn)對應(yīng)的字符串
  • 每個節(jié)點(diǎn)的所有子節(jié)點(diǎn)包含的字符都不相同。

顯然面對大量文本和大量敏感詞,利用字典樹過濾敏感詞是明智而有效的,可以大量的減少重復(fù)的抖動,從而降低時間復(fù)雜度。

算法描述

  1. 通過讀入的一個個敏感詞來構(gòu)造我們自己的字典樹,可以將敏感詞放到配置文件中...
  • 首先創(chuàng)建字典樹的結(jié)點(diǎn)
    • 我們可以定義一個boolean成員變量isEnd來標(biāo)識當(dāng)前結(jié)點(diǎn)是否是敏感詞的結(jié)尾字,即該節(jié)點(diǎn)連上其上面的結(jié)點(diǎn)可以構(gòu)成一個敏感詞。
    • 定義一個Map成員變量來存儲當(dāng)前結(jié)點(diǎn)的所有子節(jié)點(diǎn)(一層)(根節(jié)點(diǎn)不包含字符)
    • 對外提供一些方法如:添加、獲得結(jié)點(diǎn)。以方便構(gòu)造字典樹
  • 結(jié)點(diǎn)的類定義代碼如下:
    private class TrieNode{

        /**
         * 標(biāo)識當(dāng)前結(jié)點(diǎn)是否是一個“關(guān)鍵詞”的最后一個結(jié)點(diǎn)
         * true 關(guān)鍵詞的終結(jié) false 繼續(xù)
         */
        private boolean isEnd = false;

        /**
         * 用map來存儲當(dāng)前結(jié)點(diǎn)的所有子節(jié)點(diǎn),非常的方便
         * key 下一個字符 value 對應(yīng)的結(jié)點(diǎn)
         */
        private Map<Character , TrieNode> subNodes = new HashMap<>();

        /**
         * 向指定位置添加結(jié)點(diǎn)樹
         * @param key
         * @param node
         */
        public void addSubNode(Character key , TrieNode node){
            subNodes.put(key , node);
        }

        /**
         * 根據(jù)key獲得相應(yīng)的子節(jié)點(diǎn)
         * @param key
         * @return
         */
        public TrieNode getSubNode(Character key){
            return subNodes.get(key);
        }

        //判斷是否是關(guān)鍵字的結(jié)尾
        public boolean isKeyWordEnd(){
            return isEnd;
        }

        //設(shè)置為關(guān)鍵字的結(jié)尾
        public void setKeyWordEnd(boolean isEnd){
            this.isEnd = isEnd;
        }
    }

2.構(gòu)造字典樹

    /**
     * 核心算法一:構(gòu)建字典樹
     * 根據(jù)輸入的字符串,逐步構(gòu)建字典樹
     * @param textLine
     */
    private void addDirTreeNode(String textLine){
        //邊界處理
        if(textLine == null)
            return;
        //臨時結(jié)點(diǎn)指向根結(jié)點(diǎn)
        TrieNode tempNode = root;
        for(int i = 0; i < textLine.length(); i++){
            char charWord = textLine.charAt(i);

            //直接跳過非法文字
            if (isSymbol(charWord))
                continue;

            TrieNode node = tempNode.getSubNode(charWord);
            if (node == null){
                //說明tempNode第一次碰到該關(guān)鍵字結(jié)點(diǎn)
                node = new TrieNode();
                tempNode.addSubNode(charWord , node);
            }

            //tempNode指向下一個結(jié)點(diǎn),開始下一次循環(huán)
            tempNode = node;

            //到敏感詞的最后一個字時,標(biāo)記為紅色(關(guān)鍵詞結(jié)尾)
            if (i == textLine.length() - 1)
                tempNode.setKeyWordEnd(true);
        }
    }

3.過濾算法

  • 定義三個指針
  • tempNode : 指向字典樹的根節(jié)點(diǎn)。
  • position :當(dāng)前比較的位置,開始下標(biāo)0
  • begin : begin總是不斷向前,position匹配失敗的時候,需要回滾。開始下標(biāo)為0.
    • position所在位置的字符,字典樹的根節(jié)點(diǎn)的所有子節(jié)點(diǎn)中沒有該字符,則說明該字符不可能構(gòu)成敏感詞,因此begin、position均可前進(jìn)一位,同時tempNode回溯到根節(jié)點(diǎn)。
            if (tempNode == null){
                //以begin開始的字符串不存在敏感詞
                results.append(text.charAt(begin));
                position = begin + 1;
                begin = position;
                tempNode = root;
            }
  • position向前不斷移動,并且和字典樹中的敏感詞一一對應(yīng),最終到tempNode指向isEnd為true的結(jié)點(diǎn)時,匹配成功,需要替換敏感詞,并且position需要前進(jìn)一位,begin移動到和position相同的位置。
else if (tempNode.isKeyWordEnd()){
                results.append(DEFAULT_REPLACE_SENSITIVE);
                position = position + 1;
                begin = position;
                tempNode = root;
            }
  • 過濾算法詳細(xì)代碼
    /**
     * 核心算法二:
     * 過濾文本中的敏感詞匯
     * @param text
     * @return
     */
    public String filterWords(String text){
        if (StringUtils.isBlank(text))
            return text;

        StringBuilder results = new StringBuilder();
        TrieNode tempNode = root;
        int begin = 0;//回滾數(shù)
        int position = 0;//當(dāng)前比較的位置

        while (position < text.length()){
            char word = text.charAt(position);

            if (isSymbol(word)){
                if (tempNode == root){
                    results.append(word);
                    ++begin;
                }
                ++position;
                continue;
            }

            tempNode = tempNode.getSubNode(word);

            if (tempNode == null){
                //以begin開始的字符串不存在敏感詞
                results.append(text.charAt(begin));
                position = begin + 1;
                begin = position;
                tempNode = root;
            }else if (tempNode.isKeyWordEnd()){
                results.append(DEFAULT_REPLACE_SENSITIVE);
                position = position + 1;
                begin = position;
                tempNode = root;
            }else {
                ++position;
            }
        }

        results.append(text.substring(begin));
        return results.toString();
    }

算法實(shí)現(xiàn)

優(yōu)化點(diǎn)

  • 過濾非法字符(顏文字、空格...)即不可能組成敏感詞的字符,提高算法準(zhǔn)確性、性能...。
    /**
     * 判斷是否是非法字符(即不可能存在敏感詞匯的字)
     * @param character
     * @return true:非法字符
     */
    private boolean isSymbol(Character character){
        int ic = (int)character;
        //東亞文字 0x2e80 —— 0x9fff
        return !CharUtils.isAsciiAlphanumeric(character) && (ic < 0x2e80 || ic > 0x9fff);
    }

全部源碼

import java.util.HashMap;
import java.util.Map;

public class Test {
    //默認(rèn)敏感詞替換符
    private static final String DEFAULT_REPLACEMENT = "敏感詞";
    //根節(jié)點(diǎn)
    private TrieNode rootNode = new TrieNode();

    private class TrieNode {
        /**
         * true 關(guān)鍵詞的終結(jié) ; false 繼續(xù)
         */
        private boolean end = false;

        /**
         * key下一個字符,value是對應(yīng)的節(jié)點(diǎn)
         */
        private Map<Character, TrieNode> subNodes = new HashMap<Character ,TrieNode>();

        /**
         * 向指定位置添加節(jié)點(diǎn)樹
         */
        void addSubNode(Character key, TrieNode node) {
            subNodes.put(key, node);
        }
        /**
         * 獲取下個節(jié)點(diǎn)
         */
        TrieNode getSubNode(Character key) {
            return subNodes.get(key);
        }

        boolean isKeywordEnd() {
            return end;
        }

        void setKeywordEnd(boolean end) {
            this.end = end;
        }
    }

    //判斷是否是一個符號
    private boolean isSymbol(char c) {
        int ic = (int) c;
        // 0x2E80-0x9FFF 東亞文字范圍
        return !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'z')|| (c >= 'A' && c <= 'Z')) && (ic < 0x2E80 || ic > 0x9FFF);
    }


    /**
     * 過濾敏感詞
     */
    public String filter(String text) {
        if (text.trim().length() == 0) {
            return text;
        }
        String replacement = DEFAULT_REPLACEMENT;
        StringBuilder result = new StringBuilder();

        TrieNode tempNode = rootNode;
        int begin = 0; // 回滾數(shù)
        int position = 0; // 當(dāng)前比較的位置

        while (position < text.length()) {
            char c = text.charAt(position);
            // 空格直接跳過
            if (isSymbol(c)) {
                if (tempNode == rootNode) {
                    result.append(c);
                    ++begin;
                }
                ++position;
                continue;
            }

            tempNode = tempNode.getSubNode(c);

            // 當(dāng)前位置的匹配結(jié)束
            if (tempNode == null) {
                // 以begin開始的字符串不存在敏感詞
                result.append(text.charAt(begin));
                // 跳到下一個字符開始測試
                position = begin + 1;
                begin = position;
                // 回到樹初始節(jié)點(diǎn)
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 發(fā)現(xiàn)敏感詞, 從begin到position的位置用replacement替換掉
                result.append(replacement);
                position = position + 1;
                begin = position;
                tempNode = rootNode;
            } else {
                ++position;
            }
        }

        result.append(text.substring(begin));

        return result.toString();
    }

    /**
     * 構(gòu)造字典樹
     * @param lineTxt
     */
    private void addWord(String lineTxt) {
        TrieNode tempNode = rootNode;
        // 循環(huán)每個字節(jié)
        for (int i = 0; i < lineTxt.length(); ++i) {
            Character c = lineTxt.charAt(i);
            // 過濾空格
            if (isSymbol(c)) {
                continue;
            }
            TrieNode node = tempNode.getSubNode(c);

            if (node == null) { // 沒初始化
                node = new TrieNode();
                tempNode.addSubNode(c, node);
            }

            tempNode = node;

            if (i == lineTxt.length() - 1) {
                // 關(guān)鍵詞結(jié)束, 設(shè)置結(jié)束標(biāo)志
                tempNode.setKeywordEnd(true);
            }
        }
    }

    public static void main(String[] argv) {
        Test s = new Test();
        s.addWord("sb");
        s.addWord("zz");
        System.out.print(s.filter("klsfjkzzlsO(∩_∩)Odjflksbj"));
    }
}

  • 運(yùn)行結(jié)果
    klsfjk敏感詞lsO(∩_∩)Odjflk敏感詞j
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 前綴樹的基本性質(zhì)1.根節(jié)點(diǎn)不包含字符,除根節(jié)點(diǎn)外每一個節(jié)點(diǎn)都只包含一個字符。2.從根節(jié)點(diǎn)到某一節(jié)點(diǎn),路徑上經(jīng)過的字...
    icecrea閱讀 1,266評論 0 1
  • B樹的定義 一棵m階的B樹滿足下列條件: 樹中每個結(jié)點(diǎn)至多有m個孩子。 除根結(jié)點(diǎn)和葉子結(jié)點(diǎn)外,其它每個結(jié)點(diǎn)至少有m...
    文檔隨手記閱讀 13,337評論 0 25
  • 課程介紹 先修課:概率統(tǒng)計,程序設(shè)計實(shí)習(xí),集合論與圖論 后續(xù)課:算法分析與設(shè)計,編譯原理,操作系統(tǒng),數(shù)據(jù)庫概論,人...
    ShellyWhen閱讀 2,361評論 0 3
  • 文章作者:Tyan博客:noahsnail.com | CSDN | 簡書 翻譯論文匯總:https://gith...
    SnailTyan閱讀 10,039評論 0 8
  • 告別少年,邁向青春 ——獲嘉第一初級中學(xué)舉行八年級少先隊離隊暨十四歲青春儀式 “我們是共產(chǎn)主...
    若愚若谷若水閱讀 1,451評論 0 0