需求背景
大家有沒有做過屏蔽敏感詞的需求呢,這個需求一般來說很常見了。比如,系統中有一段話:
我愛吃肯德基
要求【肯德基】三個詞被屏蔽掉,屏蔽后的語句顯示為:
我愛吃***
常規的做法可能是查詢敏感詞庫中的敏感詞,循環每一個敏感詞,然后去輸入的文本中從頭到尾搜索一遍,看是否存在此敏感詞。
但是如果敏感詞很多,對于匹配也是很耗性能的。
這里介紹使用DFA算法匹配敏感詞,并進行處理。性能要優于常規處理方法。
什么是DFA算法
在計算理論中,確定有限狀態自動機或確定有限自動機(英語:deterministic finite automaton, DFA)是一個能實現狀態轉移的自動機。對于一個給定的屬于該自動機的狀態和一個屬于該自動機字母表E的字符,它都能根據事先給定的轉移函數轉移到下一個狀態(這個狀態可以是先前那個狀態)。
——來自維基百科
這里的確定意思為:狀態以及引起狀態轉換的事件都是可確定的,不存在"意外"。有限指的是:狀態以及事件的數量都是可窮舉的。
DFA算法在匹配關鍵字上面有廣泛的應用。

比如上面的關鍵詞【肯德基】,【肯尼瑪】。我們可以抽取成上面的樹狀模型。橢圓表示狀態,狀態與狀態之間的連線叫事件。
當然這里只是簡單地介紹DFA是什么,想深入的童鞋可以看看這篇文章:
常用的DFA最小化算法? - 知乎
-https://www.zhihu.com/question/39767421
里面介紹了如何將正常數據構造成DFA形式。
JAVA代碼實戰
現在我們開始做一個示例吧
現在我們指定了敏感詞【"二愣子","二蛋","狗娃"】,我們按照上面的方式重新構造數據結構:

如上圖,我們構造了3組數據,每個節點有一個狀態標記,1代表節點結束,也就是敏感詞結束,0代表還未結束。
數據結構Json形式如下:

接下來就是如何實現代碼了。
首先我們將敏感詞匯添加進入set集合中:
private Set<String> readSensitivewordFile() {
Set<String> set = Sets.newHashSet();
set.add("二愣子");
set.add("二蛋");
set.add("狗娃");
return set;
}
當然,實際情況需要從數據庫中讀取,或者從文件中讀取,然后再加載進入set集合。接下來我們將set中的數據重新構造成上面Json格式的,Java這里需要使用Map來存儲。

我們創建一個sensitiveWordMap來存儲敏感詞,這里實際就是map套map的過程,我們來調試看看map的結構:

上面的數據結構Map是不是看暈了,其實就是我之前提到Json格式。

在系統初始化時就將敏感詞構造好。

我們將敏感詞的結構構造好后,就開始匹配句子了。

如上代碼,我們需要將句子中的字符一個一個的循環,如果(Map) nowMap.get(word) != null,說明匹配到了敏感詞,這里如果isEnd的結束符為1,代表敏感詞結束,即匹配到了一個敏感詞。

我們還會遇到上圖的情況,【二蛋】是一個敏感詞,【蛋疼】也是一敏感詞。在【蛋】這個節點中,是【二蛋】的結束節點,是【蛋疼】的開始節點。我們通過代碼:
SensitiveWordFilter.minMatchTYpe == matchType
判斷,如果為true,我們在【蛋】結束之后就不再往下匹配,并將匹配到的index返回。之后再進入下一個循環了。反之。
上面我們拿到匹配到的敏感詞的index,接下來就要將句子中的敏感詞顯示出來了。

我們將其存入set集合中:
Set<String> sensitiveWordList = new HashSet<>();
這里大家發現一個問題沒有:
獲取敏感詞index循環了一次txt句子,獲取敏感詞字符又循環了一次,大家有沒有辦法減少一次循環呢
這個問題大家闊以思考一下。
然后我們將句子中的敏感詞替換成指定的字符。

比如我們將敏感詞替換成 "*"。
測試代碼
我們來測試下代碼

我們選取了《凡人修仙傳》中的一段句子:
"韓立被村里人叫作“二愣子”,可人并不是真愣真傻,反而是村中首屈一指的聰明孩子,但就像其他村中的孩子一樣,除了家里人外,他就很少聽到有人正式叫他名字“韓立”,倒是“二愣子”“二愣子”的稱呼一直伴隨至今。而之所以被人起了個“二愣子”的綽號,也只不過是因為村里已有一個叫“愣子”的孩子了。這也沒啥,村里的其他孩子也是“狗娃”“二蛋”之類的被人一直稱呼著,這些名字也不見得比“二愣子”好聽了哪里去。"
測試的結果為:

關于DFA的思考
這里我們將敏感詞構造成map,相對于普通的方法,我們不用循環敏感詞,直接用hash表的形式。效率會快很多。但是我們循環了兩次句子txt,如果我們的句子很大,那就對性能有影響,如果我們的敏感詞庫很大,構建的map集合就會很大,這樣就會很占用內存。
進階-一種基于AC自動機的高性能匹配算法
關于DFA算法的問題,這里又有一種AC自動機的算法,也可以實現敏感詞匹配。網上有關于AC自動機的論文,有興趣的童鞋闊以下載看看:
PARA-AC:一種基于AC自動機的高性能匹配算法-AET-電子技術應用
-http://www.chinaaet.com/tech/designApplication/3000125061
什么是AC自動機呢?
AC自動機全稱是Aho-CorasickAutoMaton,和Trie樹一樣是多模式字符串匹配算法。并且它與Trie樹的關系就相當于KMP與BF算法的關系一樣,AC自動機的效率要遠遠超出Trie樹
AC自動機對Trie進行了改進,在Trie的基礎上結合了KMP算法的思想,在樹中加入了類似next數組的失效指針。
AC自動機的構建主要包含以下兩個操作
將多個模式串構建成Trie樹
為Trie樹中每個節點構建失敗指針

這里給大家推薦一個項目,基于AC自動機的高性能敏感詞匹配:
GitHub - toolgood/ToolGood.Words: 一款高性能敏感詞(非法詞/臟字)檢測過濾組件,附帶繁體簡體互換,支持全角半角互換,漢字轉拼音,模糊搜索等功能。
-https://github.com/toolgood/ToolGood.Words
感謝這個大佬提供的開源項目。
敏感詞構造的數據結構:

封裝的數據結構為

匹配替換敏感詞代碼如下:

代碼中的TrieNode2為存儲的敏感詞結合構
我們用AC自動機算法測試敏感詞

如上代碼,test為我們要測試的句子,list為設置的敏感詞,測試結果如下:

我們對比DFA算法的耗時:

AC自動機耗時低于1ms,而DFA自動機的耗時大于了1ms,當然這里只是初略的測試。需要有意義的性能測試還需要加大敏感詞庫和測試句子的量。
好了,今天的文章到這里就結束了,文章介紹了AC與DFA兩種算法屏蔽敏感詞以及其性能,當然AC自動機的原理還是比較復雜的,本文就不做詳細介紹了,有興趣的同學可以多了解下相關知識。
參考
AC自動機:如何實現敏感詞過濾? -
https://blog.csdn.net/qq_35423154/article/details/109181973