敏感词过滤是挺常见的需求,分享一种简易的实现
比如,小明和小红两个人因为某些不当行为,要求被全网封杀,所有小明需要被替换为**
比如:
因为在实际生产中,敏感词数量会比较多,传入的文本也会比较长
单纯的遍历敏感词列表对字符串使用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: 小 明上课吃零食,老师让'小'红'出去 * *上课吃零食,老师让'*'*'出去
可以看到"小 明"已经修改为" "了