不少時候咱們須要對接受的文本進行過濾,剔除一下不當用詞,好比一些反動的、侮辱性的、淫穢的用語 通常會有一個敏感詞詞庫,基於這個詞庫對輸入的文本進行過濾,分享一種簡易的實現java
示例中爲了和諧,將不會出現上述違反社會主義核心價值觀的詞彙,使用「小明」、「小紅」來舉例
實際生產中能夠用須要過濾的敏感詞列表替換
複製代碼
如今假設「小明」、「小紅」已經屬於敏感詞了,那麼理想的效果:git
輸入: 小明上課吃零食,老師讓小紅出去。
輸出: **上課吃零食,老師讓**出去。
複製代碼
不少時候用戶會簡單的插入一些' '、'_'、'%'相似的簡單字符來躲避過濾,但願算法一樣能過濾敏感詞:github
輸入: 小 明上課吃零食,老師讓'小'紅'出去
輸出: * *上課吃零食,老師讓'*'*'出去
複製代碼
#準備算法
好比:bash
由於在實際生產中,敏感詞數量會比較多,傳入的文本也會比較長
單純的遍歷敏感詞列表對字符串使用String.replace(key, "**")效果會比較差
這裏使用了一種Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配的算法來進行敏感詞的匹配maven
定義敏感詞列表測試
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>
複製代碼
使用匹配器來匹配敏感詞位置並替換爲'*'ui
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();
}
複製代碼
接下來測試一下,須要先初始化一下匹配器spa
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:
小 明上課吃零食,老師讓'小'紅'出去 * *上課吃零食,老師讓'*'*'出去
複製代碼
能夠看到"小 明"已經修改成"* *"了