评论敏感词过滤-DFA算法

873 阅读3分钟

敏感词

我们应该都遇见过敏感词过滤,比如当我们输入一些包含暴力或者色情的文本,系统会阻止信息提交。敏感词过滤就是检查用户输入的内容有没有敏感词,检查之后有两个策略。

直接阻止信息保存,接口返回错误信息 允许信息保存,但是会把敏感词替换为*** 不管是哪种策略,首先都得找到是否包含敏感词,这个判断一般是在服务端完成的。

要判断用户输入有无敏感词,首先要知道哪些词语是敏感词,也就是得有个敏感词库。比如现在敏感词库里记录了两个敏感词:“abc”和“cde”,如何判断用户输入的内容里是否包含这两个敏感词呢?最容易想到的方法是遍历敏感词库,依次判断输入内容是否有“abc”和“cde”。这种方法是可靠的,但是真实的敏感词库里存放的敏感词是非常多的,这时候遍历敏感词库的性能比较低,而且大部分情况下用户输入的内容都是不包含敏感词的,这时需要完全遍历敏感词库,也就是说大部分情况下遇到的都是这个算法复杂度最高的情形。

有没有一种比较高效的方法来查找敏感词呢?DFA算法可以做到。

DFA全称为:Deterministic Finite Automaton,即确定有穷自动机。其特征为:有一个有限状态集合和一些从一个状态通向另一个状态的边,每条边上标记有一个符号,其中一个状态是初态,某些状态是终态。但不同于不确定的有限自动机,DFA中不会有从同一状态出发的两条边标志有相同的符号。

敏感词过滤很适合用DFA算法,用户每次输入都是状态的切换,如果出现敏感词,既是终态,就可以结束判断。

我们把数组形式的敏感词整理为一个树状结构,准确的说是一个森林。

v2-5f5a815a6c887526a5d0db1f880a5cc3_720w.webp

这样查找敏感词就变成了一个查找路径的问题,如果用户输入的内容中包含一个从根节点到叶子节点的完整路径,就说明包含敏感词。

算法实现逻辑是循环用户输入的字符串,依次查找每个字符是否出现在树的节点上,比如用户输入“打倒日本人”,从第一个字开始判断,“打”不在树的根节点上,进入下一步,“倒”也不在根节点上,进入下一步,“日”出现在了根节点上,这时状态切换,下一步的查找范围变为“日”的子节点;“本”出现在子节点中,状态再次切换为“本”的子节点;“人”出现在子节点中,并且为叶子节点,所以包含敏感词。

上述过程只是基本的思路,具体实现还要考虑中途退出时的状态重置,完整代码放在下面。

public class WordFilterUtils {


    /**
     * 敏感词表
     */
    private  Map wordMap = null;

    /**
     * 构造函数
     */
    public  WordFilterUtils(WordContext context) {
        wordMap = context.getWordMap();
    }


    /**
     * 替换敏感词
     *
     * @param text 输入文本
     * @return String
     */
    public String replace(final String text) {
        return replace(text, 0, '*');
    }

    /**
     * 替换敏感词
     *
     * @param text   输入文本
     * @param symbol 替换符号
     * @return String
     */
    public  String replace(final String text, final char symbol) {
        return replace(text, 0, symbol);
    }

    /**
     * 替换敏感词
     *
     * @param text   输入文本
     * @param skip   文本距离
     * @param symbol 替换符号
     * @return String
     */
    public  String replace(final String text, final int skip, final char symbol) {
        char[] charset = text.toCharArray();
        for (int i = 0; i < charset.length; i++) {
            FlagIndex fi = getFlagIndex(charset, i, skip);
            if (fi.isFlag()) {
                if (!fi.isWhiteWord()) {
                    for (int j : fi.getIndex()) {
                        charset[j] = symbol;
                    }
                } else {
                    i += fi.getIndex().size() - 1;
                }
            }
        }
        return new String(charset);
    }

    /**
     * 是否包含敏感词
     *
     * @param text 输入文本
     * @return boolean
     */
    public  boolean include(final String text) {
        return include(text, 0);
    }

    /**
     * 是否包含敏感词
     *
     * @param text 输入文本
     * @param skip 文本距离
     * @return boolean
     *
     */
    public  boolean include(final String text, final int skip) {
        boolean include = false;
        char[] charset = text.toCharArray();
        for (int i = 0; i < charset.length; i++) {
            FlagIndex fi = getFlagIndex(charset, i, skip);
            if(fi.isFlag()) {
                if (fi.isWhiteWord()) {
                    i += fi.getIndex().size() - 1;
                } else {
                    include = true;
                    break;
                }
            }
        }
        return include;
    }

    /**
     * 获取敏感词数量
     *
     * @param text 输入文本
     * @return int
     */
    public  int wordCount(final String text) {
        return wordCount(text, 0);
    }

    /**
     * 获取敏感词数量
     *
     * @param text 输入文本
     * @param skip 文本距离
     * @return int
     */
    public  int wordCount(final String text, final int skip) {
        int count = 0;
        char[] charset = text.toCharArray();
        for (int i = 0; i < charset.length; i++) {
            FlagIndex fi = getFlagIndex(charset, i, skip);
            if (fi.isFlag()) {
                if(fi.isWhiteWord()) {
                    i += fi.getIndex().size() - 1;
                } else {
                    count++;
                }
            }
        }
        return count;
    }

    /**
     * 获取敏感词列表
     *
     * @param text 输入文本
     * @return List<String>
     */
    public  List<String> wordList(final String text) {
        return wordList(text, 0);
    }

    /**
     * 获取敏感词列表
     *
     * @param text 输入文本
     * @param skip 文本距离
     * @return List<String>
     */
    public  List<String> wordList(final String text, final int skip) {
        List<String> wordList = new ArrayList<String>();
        char[] charset = text.toCharArray();
        for (int i = 0; i < charset.length; i++) {
            FlagIndex fi = getFlagIndex(charset, i, skip);
            if (fi.isFlag()) {
                if(fi.isWhiteWord()) {
                    i += fi.getIndex().size() - 1;
                } else {
                    StringBuilder builder = new StringBuilder();
                    for (int j : fi.getIndex()) {
                        char word = text.charAt(j);
                        builder.append(word);
                    }
                    wordList.add(builder.toString());
                }
            }
        }
        return wordList;
    }

    /**
     * 获取标记索引
     *
     * @param charset 输入文本
     * @param begin   检测起始
     * @param skip    文本距离
     * @return FlagIndex
     */
    private  FlagIndex getFlagIndex(final char[] charset, final int begin, final int skip) {
        FlagIndex fi = new FlagIndex();

        Map current = wordMap;
        boolean flag = false;
        int count = 0;
        List<Integer> index = new ArrayList<Integer>();
        for (int i = begin; i < charset.length; i++) {
            char word = charset[i];
            Map mapTree = (Map) current.get(word);
            boolean flagSecond = count > skip || (i == begin && Objects.isNull(mapTree));
            if (flagSecond) {
                break;
            }
            if (Objects.nonNull(mapTree)) {
                current = mapTree;
                count = 0;
                index.add(i);
            } else {
                count++;
                if (flag && count > skip) {
                    break;
                }
            }
            if ("1".equals(current.get("isEnd"))) {
                flag = true;
            }
            if ("1".equals(current.get("isWhiteWord"))) {
                fi.setWhiteWord(true);
                break;
            }
        }

        fi.setFlag(flag);
        fi.setIndex(index);

        return fi;
    }
}