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

字典树介绍

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


 

猜你喜欢

转载自blog.csdn.net/qq_37050329/article/details/84276344