利用利用字典树(前缀树)过滤敏感词

字典树介绍

Paste_Image.png
  • 又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

特性

  • 根节点不包含字符,除根节点外每一个节点都只包含一个字符
  • 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串
  • 每个节点的所有子节点包含的字符都不相同。

显然面对大量文本和大量敏感词,利用字典树过滤敏感词是明智而有效的,可以大量的减少重复的抖动,从而降低时间复杂度。

算法描述

  1. 通过读入的一个个敏感词来构造我们自己的字典树,可以将敏感词放到配置文件中...
  • 首先创建字典树的结点
    • 我们可以定义一个boolean成员变量isEnd来标识当前结点是否是敏感词的结尾字,即该节点连上其上面的结点可以构成一个敏感词。
    • 定义一个Map成员变量来存储当前结点的所有子节点(一层)(根节点不包含字符)
    • 对外提供一些方法如:添加、获得结点。以方便构造字典树
  • 结点的类定义代码如下:
    private class TrieNode{

        /**
         * 标识当前结点是否是一个“关键词”的最后一个结点
         * true 关键词的终结 false 继续
         */
        private boolean isEnd = false;

        /**
         * 用map来存储当前结点的所有子节点,非常的方便
         * key 下一个字符 value 对应的结点
         */
        private Map<Character , TrieNode> subNodes = new HashMap<>();

        /**
         * 向指定位置添加结点树
         * @param key
         * @param node
         */
        public void addSubNode(Character key , TrieNode node){
            subNodes.put(key , node);
        }

        /**
         * 根据key获得相应的子节点
         * @param key
         * @return
         */
        public TrieNode getSubNode(Character key){
            return subNodes.get(key);
        }

        //判断是否是关键字的结尾
        public boolean isKeyWordEnd(){
            return isEnd;
        }

        //设置为关键字的结尾
        public void setKeyWordEnd(boolean isEnd){
            this.isEnd = isEnd;
        }
    }

2.构造字典树

    /**
     * 核心算法一:构建字典树
     * 根据输入的字符串,逐步构建字典树
     * @param textLine
     */
    private void addDirTreeNode(String textLine){
        //边界处理
        if(textLine == null)
            return;
        //临时结点指向根结点
        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第一次碰到该关键字结点
                node = new TrieNode();
                tempNode.addSubNode(charWord , node);
            }

            //tempNode指向下一个结点,开始下一次循环
            tempNode = node;

            //到敏感词的最后一个字时,标记为红色(关键词结尾)
            if (i == textLine.length() - 1)
                tempNode.setKeyWordEnd(true);
        }
    }

3.过滤算法

  • 定义三个指针
  • tempNode : 指向字典树的根节点。
  • position :当前比较的位置,开始下标0
  • begin : begin总是不断向前,position匹配失败的时候,需要回滚。开始下标为0.
    • position所在位置的字符,字典树的根节点的所有子节点中没有该字符,则说明该字符不可能构成敏感词,因此begin、position均可前进一位,同时tempNode回溯到根节点。
            if (tempNode == null){
                //以begin开始的字符串不存在敏感词
                results.append(text.charAt(begin));
                position = begin + 1;
                begin = position;
                tempNode = root;
            }
  • position向前不断移动,并且和字典树中的敏感词一一对应,最终到tempNode指向isEnd为true的结点时,匹配成功,需要替换敏感词,并且position需要前进一位,begin移动到和position相同的位置。
else if (tempNode.isKeyWordEnd()){
                results.append(DEFAULT_REPLACE_SENSITIVE);
                position = position + 1;
                begin = position;
                tempNode = root;
            }
  • 过滤算法详细代码
    /**
     * 核心算法二:
     * 过滤文本中的敏感词汇
     * @param text
     * @return
     */
    public String filterWords(String text){
        if (StringUtils.isBlank(text))
            return text;

        StringBuilder results = new StringBuilder();
        TrieNode tempNode = root;
        int begin = 0;//回滚数
        int position = 0;//当前比较的位置

        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();
    }

算法实现

优化点

  • 过滤非法字符(颜文字、空格...)即不可能组成敏感词的字符,提高算法准确性、性能...。
    /**
     * 判断是否是非法字符(即不可能存在敏感词汇的字)
     * @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 {
    //默认敏感词替换符
    private static final String DEFAULT_REPLACEMENT = "敏感词";
    //根节点
    private TrieNode rootNode = new TrieNode();

    private class TrieNode {
        /**
         * true 关键词的终结 ; false 继续
         */
        private boolean end = false;

        /**
         * key下一个字符,value是对应的节点
         */
        private Map<Character, TrieNode> subNodes = new HashMap<Character ,TrieNode>();

        /**
         * 向指定位置添加节点树
         */
        void addSubNode(Character key, TrieNode node) {
            subNodes.put(key, node);
        }
        /**
         * 获取下个节点
         */
        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; // 回滚数
        int position = 0; // 当前比较的位置

        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);

            // 当前位置的匹配结束
            if (tempNode == null) {
                // 以begin开始的字符串不存在敏感词
                result.append(text.charAt(begin));
                // 跳到下一个字符开始测试
                position = begin + 1;
                begin = position;
                // 回到树初始节点
                tempNode = rootNode;
            } else if (tempNode.isKeywordEnd()) {
                // 发现敏感词, 从begin到position的位置用replacement替换掉
                result.append(replacement);
                position = position + 1;
                begin = position;
                tempNode = rootNode;
            } else {
                ++position;
            }
        }

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

        return result.toString();
    }

    /**
     * 构造字典树
     * @param lineTxt
     */
    private void addWord(String lineTxt) {
        TrieNode tempNode = rootNode;
        // 循环每个字节
        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) {
                // 关键词结束, 设置结束标志
                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"));
    }
}

  • 运行结果
    klsfjk敏感词lsO(∩_∩)Odjflk敏感词j
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容