DFA敏感词过滤算法详解

功能简介

DFA算法模型是解决针对一段文本,过滤出其他文本。比如我们定义了三段文本a、b和c为“张三”、“程序员”、“开发”,而在文本d“我的名字叫张三,我是一名刚工作一年的程序员”的内容中,可以发现文本d是包含a和b文本的,这个过程就是DFA算法模型实现的功能。DFA算法的核心是构建出一个模型。

抛出疑问

  1. java中有contain方法进行判断a文本是否包含b文本,为什么不直接用contain进行比较呢?
  2. DFA算法用Java如何构建?
  3. DFA算法性能如何?

算法思想

举个例子,假如我们认为以下是敏感词。

 1. 我是张三
 2. 我是李四
 3. 大王八
 4. 大王来了

然后我们根据上面的敏感词构建出一个模型,该模型实际上是一颗树,或者说是森林。在java代码中实际上是多个map,为了方便观察,我转换成了json。(不完整版本)

    {
        "我": {
            "是": {
                "张": {
                    "三": {}
                },
                "李": {
                    "四": {}
                }
            }
        },
        "大": {
            "王": {
                "来": {
                    "了": {}
                },
                "八": {}
            }
        }
    }

我们通过上面构建的模型对目标文本“我是张三,我是大王”进行敏感词过滤,大致过程如下:

1. 遍历目标文本,获取到第一个字符‘我’,在我们构建的模型的第一层比较,发现第一层的字符串是‘我’和‘大’。
2. 字符‘我’能够在模型中匹配到,继续获取目标文本的下一个字符‘是’,发现第二层也是‘是’,也是匹配上了,依次是‘张’、‘三’。
3. 通过步骤2我们可以断定目标文本中“我是张三”是敏感词,记录下来。
4. 继续获取下一个字符,这里节省时间直接跳到‘大’的字符,发现可以和第一层的‘大’匹配上,然后获取目标文本中的下一个字符‘王’,也能够在模型中匹配上。
5. 这样整个匹配过程完成了,大家可能有疑问,那到底“大王”算不算敏感词呢?如果算,但是我们在定义敏感词的时候并没有“大王”;如果不算,但确实是匹配上了。

为了解决这个问题,我们在构建好的模型中加一个字段就可以解决这种摸棱两可的问题。

{
        "我": {
            "isEnd": false,
            "是": {
                "张": {
                    "三": {
                        "isEnd": true
                    },
                    "isEnd": false
                },
                "isEnd": false,
                "李": {
                    "四": {
                        "isEnd": true
                    },
                    "isEnd": false
                }
            }
        },
        "大": {
            "王": {
                "来": {
                    "了": {
                        "isEnd": true
                    },
                    "isEnd": false
                },
                "八": {
                    "isEnd": true
                },
                "isEnd": false
            },
            "isEnd": false
        }
    }

可以看到加了一个isEnd字段进行记录构建的模型,每段敏感词的结尾是true,如果不是结尾则是false,比如“我”“我是”“我是张”都为false,而“我是张三”为true。通过这个字段每次来判断敏感词校验是否到了敏感词的结尾。
回到上面的问题:“大王”到底算不算敏感词呢?
根据上面的分析,只有完整匹配上模型中的敏感词才能算敏感词,所以在比较‘王’的时候发现isEnd是false,说明“大王”只是“大王八”或者“大王来了”的一部分,并不能算敏感词。所以目标文本“我是张三,我是大王”中“我是张三”是敏感词,“大王”不是敏感词。

构建DFA模型

下面用java代码构建一个DFA模型

    public static Map sensitiveWordMap = Maps.newHashMap();

    /**
     * 构建DFA模型
     */
    public synchronized void initMap(Set<String> sensitiveWordSet) {
        // 避免扩容操作
        sensitiveWordMap = Maps.newHashMapWithExpectedSize(sensitiveWordSet.size());

        sensitiveWordSet.forEach(sensitiveWord -> {
            Map currentMap = sensitiveWordMap;
            for (int j = 0; j < sensitiveWord.length(); j++) {
                char charKey = sensitiveWord.charAt(j);
                Map innerMap = (Map) currentMap.get(charKey);
                // 如果下层map为null,则创建新的map
                if (Objects.isNull(innerMap)) {
                    innerMap = Maps.newHashMap();
                    currentMap.put(charKey, innerMap);
                }
                currentMap = innerMap;

                // 如果是最后一个字符,isEnd为true;如果不是,isEnd为false
                if (j == sensitiveWord.length() - 1) {
                    currentMap.put("isEnd", true);
                } else {
                    currentMap.merge("isEnd", false, (o, n) -> Boolean.TRUE.equals(o) ? o : n);
                }
            }
        });
    }

构建好模型之后,通过下面方法进行判断是否存在敏感词。

    /**
     * @param sourceText
     * @return
     */
    public Set<String> getSensitive(String sourceText) {
        Set<String> sensitiveWordSets = new HashSet<>();
        for (int n = 0; n < sourceText.length(); n++) {
            // 判断是否包含敏感字符
            int length = judgeSensitiveWithIndex(sourceText, n);
            if (length > 0) {
                // 存在,加入set
                sensitiveWordSets.add(sourceText.substring(n, n + length));
                // 减1的原因,是因为for会自增
                n = n + length - 1;
            }
        }
        return sensitiveWordSets;
    }

    private int judgeSensitiveWithIndex(String txt, int beginIndex) {
        // 匹配标识数默认为0
        int matchFlag = 0;
        char word;
        Map nowMap = sensitiveWordMap;
        for (int i = beginIndex; i < txt.length(); i++) {
            word = txt.charAt(i);
            // 获取指定key
            nowMap = (Map) nowMap.get(word);
            // 存在,则判断是否为最后一个
            if (nowMap != null) {
                // 找到相应key,匹配标识+1
                matchFlag++;
                if (Boolean.TRUE.equals(nowMap.get("isEnd"))) {
                    // 如果为最后一个匹配规则,结束循环,返回匹配标识数
                    return matchFlag;
                }
            } else {
                // 不存在,直接返回
                return 0;
            }
        }
        return 0;
    }

解答疑问

1. java中有contain方法进行判断a文本是否包含b文本,为什么不直接用contain进行比较呢?
可以从两方面考虑:
功能方面,contain只能进行最大匹配模式进行匹配,比如目标文本“我是张三,我是大王”,针对敏感词“大王八”,目标文本中的“大王”正常来说是不算敏感词的,但是要是有这么个需求就认为“大王”是敏感词,那么contain就不能满足这个需求了。而DFA可以设置最小匹配模式,可以过滤出“大王”是敏感词。
性能方面,contain是针对每次比对一个敏感词,尽管有containAny,但底层还是对每个敏感词进行比对,也就意味着,有一万个敏感词,就要比对一万次,性能相对很低;而DFA算法不管有多少敏感词,只需要对目标文本遍历一次,便可以获取目标文本涉及到的所有敏感词。
2. DFA算法用Java如何构建?
用java实现实际上就是一层层map的嵌套,核心思想就是通过遍历文本的每个字符,然后去map中找到对应的key一层层进行比对,具体实现如上。
3. DFA算法性能如何?
DFA算法的性能取决于目标文本的长度,遍历一次目标文本即可获取所有涉及到的敏感词,尤其适合针对于实时聊天软件的敏感词过滤;假设目标文本的长度为n,它的时间复杂度为O(n)。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值