敏感词过滤的简易实现

1,120 阅读4分钟

介绍

很多时候我们需要对接受的文本进行过滤,剔除一下不当用词,比如一些反动的、侮辱性的、淫秽的用语 一般会有一个敏感词词库,基于这个词库对输入的文本进行过滤,分享一种简易的实现

示例中为了和谐,将不会出现上述违反社会主义核心价值观的词汇,使用“小明”、“小红”来举例    
实际生产中可以用需要过滤的敏感词列表替换

现在假设“小明”、“小红”已经属于敏感词了,那么理想的效果:

输入:  小明上课吃零食,老师让小红出去。   
输出:  **上课吃零食,老师让**出去。

很多时候用户会简单的插入一些' '、'_'、'%'类似的简单字符来躲避过滤,希望算法同样能过滤敏感词:

输入:  小 明上课吃零食,老师让'小'红'出去
输出:  * *上课吃零食,老师让'*'*'出去

#准备

首先,需要准备一个敏感词列表

比如:

  1. 小明
  2. 小红

然后,需要一个高效的匹配算法

因为在实际生产中,敏感词数量会比较多,传入的文本也会比较长
单纯的遍历敏感词列表对字符串使用String.replace(key, "**")效果会比较差
这里使用了一种Aho Corasick自动机结合DoubleArrayTrie极速多模式匹配的算法来进行敏感词的匹配

实现

定义敏感词列表

private static final String[] SENSITIVE_KEYS = new String[]{
    "小明",
    "小红"
};

使用maven将算法库引用进来

<dependencies>
    <dependency>
        <groupId>com.hankcs</groupId>
        <artifactId>aho-corasick-double-array-trie</artifactId>
        <version>1.2.1</version>
    </dependency>
</dependencies>

使用匹配器来匹配敏感词位置并替换为'*'

public static String shadowSensitive(String text) {
    StringBuffer sb = new StringBuffer(text);
    // filter sensitive words
    List<AhoCorasickDoubleArrayTrie.Hit<String>> hits = acdat.parseText(sb);
    // shadow sensitive words
    for (AhoCorasickDoubleArrayTrie.Hit<String> hit : hits) {
        for (int i = hit.begin; i < hit.end; i++ ){
            sb.deleteCharAt(i);
            sb.insert(i, "*");
        }
    }
    return sb.toString();
}

接下来测试一下,需要先初始化一下匹配器

public static void main(String[] args) {
    TreeMap<String, String> keys = new TreeMap<String, String>();
    for (String key : SENSITIVE_KEYS) {
        keys.put(key, key);
    }
    acdat = new AhoCorasickDoubleArrayTrie<String>();
    acdat.build(keys);
    
    String text1 = "小明上课吃零食,老师让小红出去";
    String text2 = "小 明上课吃零食,老师让'小'红'出去";
    
    System.out.println("text1:");
    System.out.println(text1);
    System.out.println(shadowSensitive(text1));
    System.out.println("text2:");
    System.out.println(text2);
    System.out.println(shadowSensitive(text2));
}

执行程序后,控制台输出

text1:
小明上课吃零食,老师让小红出去
**上课吃零食,老师让**出去
text2:
小 明上课吃零食,老师让'小'红'出去
小 明上课吃零食,老师让'小'红'出去

可以看到text1中“小明”和“小红”已经被替换成了“**”
但是,在词语中简单的加入一些字符就可以绕开过滤器,这还需要优化一下

优化

匹配器只能匹配到“小明”,而无法匹配到“小 明”或者“小_明”
优化的思路如下:

  • 输入的文本
小 明上课吃零食,老师让'小''出去
  • 将一些常见的字符取出来,只留下文字内容
小明上课吃零食,老师让小红出去
  • 进行敏感词的匹配,将敏感词改为*
**上课吃零食,老师让**出去
  • 将取出的字符再重新插回去
* *上课吃零食,老师让'*'*'出去

参照这个思路,改写了上面的shadowSensitive方法
改线前需要定义一些常见的字符

private static final char[] SPECIAL_CHARS = new char[]{
    ' ', '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=','+',
    '\\', '|', '[', '{', ']', '}', ';', ':', '\'', '"', ',', '<', '.', '>', '/','?',
    
    //中文字符
    '&emsp;', '!', '¥', '…', '(', ')', '、', '「', '」', '【', '】', ';', ':', '“', '”', ',', '。', '《', '》', '?'};

为了方便展示,这里仅仅列举了常见的一部分字符,有需要的话,可以随时添加字符进去
优化的shadowSensitive方法:

public static String shadowSensitive(String text) {
    // detect special chars
    List<int[]> descriptors = new ArrayList<int[]>();
    for (int i = 0; i < text.length(); i++) {
        for (int j = 0; j < SPECIAL_CHARS.length; j++) {
            if (text.charAt(i) == SPECIAL_CHARS[j]) {
                int[] descriptor = new int[2];
                descriptor[0] = i;
                descriptor[1] = j;
                descriptors.add(descriptor);
            }
        }
    }
    
    // remove special chars
    StringBuffer sb = new StringBuffer(text);
    for (int i = descriptors.size() - 1; i >= 0; i--) {
        sb.deleteCharAt(descriptors.get(i)[0]);
    }
    
    // filter sensitive words
    List<AhoCorasickDoubleArrayTrie.Hit<String>> hits = acdat.parseText(sb);
    
    // shadow sensitive words
    for (AhoCorasickDoubleArrayTrie.Hit<String> hit : hits) {
        for (int i = hit.begin; i < hit.end; i++ ){
            sb.deleteCharAt(i);
            sb.insert(i, "*");
        }
    }
    
    // refill special chars
    for (int[] descriptor : descriptors) {
        sb.insert(descriptor[0], SPECIAL_CHARS[descriptor[1]]);
    }
    return sb.toString();
}

接下来测试一下

public static void main(String[] args) {
    TreeMap<String, String> keys = new TreeMap<String, String>();
    for (String key : SENSITIVE_KEYS) {
        keys.put(key, key);
    }
    acdat = new AhoCorasickDoubleArrayTrie<String>();
    acdat.build(keys);
    
    String text1 = "小明上课吃零食,老师让小红出去";
    String text2 = "小 明上课吃零食,老师让'小'红'出去";
    
    System.out.println("text1:");
    System.out.println(text1);
    System.out.println(shadowSensitive(text1));
    System.out.println("text2:");
    System.out.println(text2);
    System.out.println(shadowSensitive(text2));
}

执行程序后,控制台输出:

text1:
小明上课吃零食,老师让小红出去
**上课吃零食,老师让**出去
text2:
小 明上课吃零食,老师让'小''出去
* *上课吃零食,老师让'*'*'出去

可以看到"小 明"已经修改为"* *"了