转载

一种简易的敏感词过滤实现

介绍

敏感词过滤是挺常见的需求,分享一种简易的实现

比如,小明和小红两个人因为某些不当行为,要求被全网封杀,所有小明需要被替换为**

准备

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

比如:

  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[]{
    ' ', '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '=','+',
    '//', '|', '[', '{', ']', '}', ';', ':', '/'', '"', ',', '<', '.', '>', '/','?',
    
    //中文字符
    ' ', '!', '¥', '…', '(', ')', '、', '「', '」', '【', '】', ';', ':', '“', '”', ',', '。', '《', '》', '?'};

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

优化的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:
小 明上课吃零食,老师让'小'红'出去
* *上课吃零食,老师让'*'*'出去

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

原文  https://segmentfault.com/a/1190000020226308
正文到此结束
Loading...