DFA 敏感词过滤
對(duì)于一個(gè)游戲,如果有聊天功能,那么我們就會(huì)希望我們的聊天系統(tǒng)能夠?qū)ν婕业妮斎脒M(jìn)行判斷,如果玩家的輸入中含有一些敏感詞匯,那么我們就禁止玩家發(fā)送聊天,或者把敏感詞轉(zhuǎn)換為 * 來(lái)替換。
為什么要使用 DFA 算法
設(shè)我們已經(jīng)有了一個(gè)敏感詞詞庫(kù)(從相關(guān)部門獲取到的,或者網(wǎng)上找來(lái)的),那么我們最容易想到的過(guò)濾敏感詞的方法就是:
遍歷整個(gè)敏感詞庫(kù),拿到敏感詞,再判斷玩家輸入的字符串中是否有該敏感詞,如果有就把敏感詞字符替換為 *
但這樣的方法,我們需要遍歷整個(gè)敏感詞庫(kù),并且對(duì)玩家輸入的字符串進(jìn)行替換。而整個(gè)敏感詞庫(kù)中一般會(huì)有上千個(gè)字符串。而玩家聊天輸入的字符串一般也就 20~30 個(gè)字符。
因此,這種方法的效率是非常低的,無(wú)法應(yīng)用到真實(shí)的開(kāi)發(fā)中。
而使用 DFA 算法就可以實(shí)現(xiàn)高效的敏感詞過(guò)濾。使用 DFA 算法,我們只需要遍歷一遍玩家輸入的字符串即可將所有存在的敏感詞進(jìn)行替換。
DFA 算法原理
DFA 算法是通過(guò)提前構(gòu)造出一個(gè) 樹(shù)狀查找結(jié)構(gòu)(實(shí)際上應(yīng)該說(shuō)是一個(gè) 森林),之后根據(jù)輸入在該樹(shù)狀結(jié)構(gòu)中就可以進(jìn)行非常高效的查找。
設(shè)我們有一個(gè)敏感詞庫(kù),詞酷中的詞匯為:
我愛(ài)你
我愛(ài)他
我愛(ài)她
我愛(ài)你呀
我愛(ài)他呀
我愛(ài)她呀
我愛(ài)她啊
那么就可以構(gòu)造出這樣的樹(shù)狀結(jié)構(gòu):
設(shè)玩家輸入的字符串為:白菊我愛(ài)你呀哈哈哈
我們遍歷玩家輸入的字符串 str,并設(shè)指針 i 指向樹(shù)狀結(jié)構(gòu)的根節(jié)點(diǎn),即最左邊的空白節(jié)點(diǎn):
str[0] = ‘白’ 時(shí),此時(shí) tree[i] 沒(méi)有指向值為 ‘白’ 的節(jié)點(diǎn),所以不滿足匹配條件,繼續(xù)往下遍歷
str[1] = ‘菊’,同樣不滿足匹配條件,繼續(xù)遍歷
str[2] = ‘我’,此時(shí) tree[i] 有一條路徑連接著 ‘我’ 這個(gè)節(jié)點(diǎn),滿足匹配條件,i 指向 ‘我’ 這個(gè)節(jié)點(diǎn),然后繼續(xù)遍歷
str[3] = ‘愛(ài)’,此時(shí) tree[i] 有一條路徑連著 ‘愛(ài)’ 這個(gè)節(jié)點(diǎn),滿足匹配條件,i 指向 ‘愛(ài)’,繼續(xù)遍歷
str[4] = ‘你’,同樣有路徑,i 指向 ‘你’,繼續(xù)遍歷
str[5] = ‘呀’,同樣有路徑,i 指向 ‘呀’
此時(shí),我們的指針 i 已經(jīng)指向了樹(shù)狀結(jié)構(gòu)的末尾,即此時(shí)已經(jīng)完成了一次敏感詞判斷。我們可以用變量來(lái)記錄下這次敏感詞匹配開(kāi)始時(shí)玩家輸入字符串的下標(biāo),和匹配結(jié)束時(shí)的下標(biāo),然后再遍歷一次將字符替換為 * 即可。
結(jié)束一次匹配后,我們把指針 i 重新指向樹(shù)狀結(jié)構(gòu)的根節(jié)點(diǎn)處。
此時(shí)我們玩家輸入的字符串還沒(méi)有遍歷到頭,所以繼續(xù)遍歷:
str[6] = ‘哈’,不滿足匹配條件,繼續(xù)遍歷
str[7] = ‘哈’ …
str[8] = ‘哈’ …
可以看出我們遍歷了一次玩家輸入的字符串,就找到了其中的敏感詞匯。
而在這一段標(biāo)題的下面,我說(shuō) DFA 算法一開(kāi)始構(gòu)造的結(jié)構(gòu)實(shí)際上算是一種森林,因?yàn)閷?duì)于一個(gè)更完整的敏感詞庫(kù)而言,其構(gòu)造出來(lái)的結(jié)構(gòu)是這樣的:
如果不看該結(jié)構(gòu)的根節(jié)點(diǎn),即那個(gè)空白節(jié)點(diǎn),那么就可以看作是由一個(gè)個(gè)樹(shù)結(jié)構(gòu)組成的森林。
理解了 DFA 算法是如何匹配過(guò)濾詞的,接下來(lái)我們開(kāi)始從代碼層面來(lái)探討如何根據(jù)敏感詞庫(kù)構(gòu)造出這樣的森林結(jié)構(gòu)。
DFA 算法 森林結(jié)構(gòu)的構(gòu)造
不論是樹(shù),還是森林,都是由一個(gè)個(gè)節(jié)點(diǎn)構(gòu)成的,因此我們來(lái)探討該結(jié)構(gòu)中的節(jié)點(diǎn)應(yīng)該存儲(chǔ)哪些信息。
按照正常的樹(shù)結(jié)構(gòu)來(lái)說(shuō),節(jié)點(diǎn)結(jié)束存儲(chǔ)自身的值,和 與其連接的子節(jié)點(diǎn)的指針。
但對(duì)于 DFA 算法的結(jié)構(gòu),子節(jié)點(diǎn)的數(shù)量一開(kāi)始我們是不確定的。所以,我們可以用一個(gè) List 來(lái)存儲(chǔ) 所以子節(jié)點(diǎn)的指針,但是這樣子的話,我們?cè)谄ヅ鋾r(shí)進(jìn)行查找路徑就需要遍歷整個(gè) List,這樣子效率是比較慢的。
為了達(dá)到 O(1) 的查找效率,我們可以使用哈希表來(lái)存儲(chǔ)子節(jié)點(diǎn)的指針。
我們還可以直接用 哈希表來(lái)作為森林的入口節(jié)點(diǎn):
該哈希表中存放著 一系列 Key 為 不同的敏感詞開(kāi)頭字符 Value 為 表示該字符的節(jié)點(diǎn) 的鍵值對(duì)
并且因?yàn)楣1砜梢源娣挪煌愋蛯?duì)象的特點(diǎn)(只要繼承自 object),我們還可以存放可一個(gè) Key 為 ‘IsEnd’ Value 為 0 的鍵值對(duì)。 Value 為 0 表示當(dāng)前節(jié)點(diǎn)不為結(jié)構(gòu)的末尾, Value 為 1 表示當(dāng)前節(jié)點(diǎn)為結(jié)構(gòu)的末尾。
那么對(duì)于結(jié)構(gòu)中的其它節(jié)點(diǎn),同樣可以用哈希表來(lái)構(gòu)造。 對(duì)于該節(jié)點(diǎn)表示的字符,我們?cè)谄涓腹?jié)點(diǎn)中包含的鍵值對(duì)中已經(jīng)存儲(chǔ)了(因?yàn)槲覀兊慕Y(jié)構(gòu)最終有一個(gè)空白根節(jié)點(diǎn),其里面的鍵值對(duì),Key 存儲(chǔ)了敏感詞匯的開(kāi)頭字符, Value 就又是一個(gè)哈希表 即其子節(jié)點(diǎn))
并且每個(gè)節(jié)點(diǎn),也就是哈希表,里面也存儲(chǔ)一個(gè) Kye 為 “isEnd” Value 為 0/1 的鍵值對(duì)。 然后也存儲(chǔ)了一系列的 Key 為其子節(jié)點(diǎn)表示的字符, Value 為其子節(jié)點(diǎn)(哈希表) 的鍵值對(duì)。
我們?cè)賮?lái)舉個(gè)具體例子表述:
設(shè)有這樣的結(jié)構(gòu):
該結(jié)構(gòu)最開(kāi)始就是其空白根節(jié)點(diǎn),即哈希表,我們?cè)O(shè)其為 map
那么,對(duì)于 “我愛(ài)你呀” 這個(gè)敏感詞,其查找過(guò)程就為:
map[‘我’][‘愛(ài)’][‘你’][‘呀’][‘IsEnd’] == 1
經(jīng)過(guò)以上分析,我們就可以得出大概地代碼構(gòu)造該結(jié)構(gòu)的過(guò)程:
1、創(chuàng)建一個(gè)哈希表,作為該結(jié)構(gòu)的空白根節(jié)點(diǎn)
2、遍歷敏感詞詞庫(kù),得到一個(gè)敏感詞字符串
3、遍歷敏感詞字符串,得到一個(gè)當(dāng)前遍歷字符
4、在樹(shù)結(jié)構(gòu)中查找是否已經(jīng)包含了當(dāng)前遍歷字符,如果包含則直接走到樹(shù)結(jié)構(gòu)中已經(jīng)存在的這個(gè)節(jié)點(diǎn),然后繼續(xù)向下遍歷字符。
查找過(guò)程為:
對(duì)于敏感詞的第一個(gè)字符串而言:
indexMap = map // 相當(dāng)于指向樹(shù)結(jié)構(gòu)節(jié)點(diǎn)的指針
if(indexMap.ContainsKey(‘c’)) indexMap = indexMap[‘c’]
這樣,我們的 indexMap 相當(dāng)于一個(gè)指針,就指向了樹(shù)結(jié)構(gòu)中已經(jīng)存在了的相同節(jié)點(diǎn)
對(duì)于后面的字符也是同樣的:
if(indexMap.ContainsKye(‘c’)) indexMap = indexMap[‘c’]
如果樹(shù)結(jié)構(gòu)中不存在,或者是當(dāng)前指針指向的節(jié)點(diǎn),其所有子節(jié)點(diǎn)都沒(méi)有表示當(dāng)遍歷到的字符,則我們就需要?jiǎng)?chuàng)建一個(gè)子節(jié)點(diǎn),即添加一個(gè)鍵值對(duì),其 Key 為當(dāng)前遍歷到的字符, Value 為新建一個(gè)哈希表。
5、判斷當(dāng)前遍歷的字符,是否是當(dāng)前字符串的最后一個(gè)。如果是 則添加鍵值對(duì) Key 為 “IsEnd” Value 為 1。 如果不是,則添加鍵值對(duì) Key 為 “IsEnd” Value 為 0。
對(duì)于 DFA 算法的結(jié)構(gòu)構(gòu)造論述到此完畢,接下來(lái)給出構(gòu)造代碼java版本。
private Hashtable map;private void initFilter(List<String> words) {map = new Hashtable(words.size());for (int i = 0; i < words.size(); i++) {String word = words.get(i);Hashtable indexMap = map;for (int j = 0; j < word.length(); j++) {char c = word.toCharArray()[j];if (indexMap.containsKey(c)) {indexMap = (Hashtable)indexMap.get(c);} else {Hashtable newMap = new Hashtable();newMap.put("IsEnd", 0);indexMap.put(c, newMap);indexMap = newMap;}if (j == word.length() - 1) {if (indexMap.containsKey("IsEnd")){indexMap.put("IsEnd", 1);} else {indexMap.put("IsEnd", 1);}}}}System.out.println(map); // {我={IsEnd=0, 挺={IsEnd=0, 好={IsEnd=1}}}, 你={IsEnd=0, 好={IsEnd=0, 好={啊={IsEnd=1}, 呀={IsEnd=1}, IsEnd=0}}}}}private int CheckFilterWord(String txt, int beginIndex) {boolean flag = false;int len = 0;Hashtable curMap = map;for (int i = beginIndex; i < txt.length(); i++) {char c = txt.toCharArray()[i];Hashtable temp = (Hashtable)curMap.get(c);if (temp != null) {if ((int)temp.get("IsEnd") == 1) flag = true;else curMap = temp;len++;}else{break;}}if (!flag) len = 0;return len;}public String SerachFilterWordAndReplace(String txt) {int i = 0;StringBuilder sb = new StringBuilder(txt);while (i < txt.length()) {int len = CheckFilterWord(txt, i);if (len > 0) {for (int j = 0; j < len; j++) {sb.replace(i + j,i + j+1,"*");}i += len;} else{++i;}}return sb.toString();}@Testpublic void testSxt() {initFilter(Arrays.asList("你好好啊","你好好呀","我挺好"));String ret=SerachFilterWordAndReplace("是是你好好呀試試我挺好試試");System.out.println(ret);//是是****試試***試試}總結(jié)
- 上一篇: java 跳转action_JS 跳转到
- 下一篇: while语句学习