大半個月沒有更新了,因為最近有點忙(其實是懶)
最近弄了一個使用者發表評論的功能,使用者上傳了評論,再文章下可以看到自己的評論,但作為社會主義接班人,踐行社會主義核心價值觀,所以給評論敏感詞過濾的功能不可少,在網上找了資料,發現已經有非常成熟的解決方案。 常用的方案用這麼兩種
- 全文搜尋,逐個匹配。這種聽起來就不夠高大上,在資料量大的情況下,會有效率問題,文末有比較
- DFA演算法-確定有限狀態自動機 附上百科連結 確定有限狀態自動機
DFA演算法介紹
DFA是一種計算模型,資料來源是一個有限個集合,通過當前狀態和事件來確定下一個狀態,即 狀態+事件=下一狀態,由此逐步構建一個有向圖,其中的節點就是狀態,所以在DFA演算法中只有查詢和判斷,沒有複雜的計算,從而提高演算法效率
參考文章 Java實現敏感詞過濾
實現邏輯
構造資料結構
將敏感詞轉換成樹結構,舉例敏感詞有著這麼幾個 ['日本鬼子','日本人','日本男人']
,那麼資料結構如下(圖片引用參考文章)
每個文字是一個節點,連續的節點組成一個詞,日本人
對應的就是中間的那條鏈,我們可以使用物件或者map來構建樹,這裡的栗子採用map
構建節點,每個節點中有個狀態標識,用來表示當前節點是不是最後一個,每條鏈路必須要有個終點節點,先來看下構建節點的流程圖
判斷邏輯
先從文字的第一個字開始檢查,比如你我是日本鬼子
,第一個字 你
,在樹的第一層找不到這個節點,那麼繼續找第二個字,到了日
的時候,第一層節點找到了,那麼接著下一層節點中查詢本
,同時判斷這個節點是不是結尾節點,若是結尾節點,則匹配成功了,反之繼續匹配
程式碼實現
####構造資料結構
/**
* @description
* 構造敏感詞map
* @private
* @returns
*/
private makeSensitiveMap(sensitiveWordList) {
// 構造根節點
const result = new Map();
for (const word of sensitiveWordList) {
let map = result;
for (let i = 0; i < word.length; i++) {
// 依次獲取字
const char = word.charAt(i);
// 判斷是否存在
if (map.get(char)) {
// 獲取下一層節點
map = map.get(char);
} else {
// 將當前節點設定為非結尾節點
if (map.get('laster') === true) {
map.set('laster', false);
}
const item = new Map();
// 新增節點預設為結尾節點
item.set('laster', true);
map.set(char, item);
map = map.get(char);
}
}
}
return result;
}
複製程式碼
最終map結構如下
查詢敏感詞
/**
* @description
* 檢查敏感詞是否存在
* @private
* @param {any} txt
* @param {any} index
* @returns
*/
private checkSensitiveWord(sensitiveMap, txt, index) {
let currentMap = sensitiveMap;
let flag = false;
let wordNum = 0;//記錄過濾
let sensitiveWord = ''; //記錄過濾出來的敏感詞
for (let i = index; i < txt.length; i++) {
const word = txt.charAt(i);
currentMap = currentMap.get(word);
if (currentMap) {
wordNum++;
sensitiveWord += word;
if (currentMap.get('laster') === true) {
// 表示已到詞的結尾
flag = true;
break;
}
} else {
break;
}
}
// 兩字成詞
if (wordNum < 2) {
flag = false;
}
return { flag, sensitiveWord };
}
/**
* @description
* 判斷文字中是否存在敏感詞
* @param {any} txt
* @returns
*/
public filterSensitiveWord(txt, sensitiveMap) {
let matchResult = { flag: false, sensitiveWord: '' };
// 過濾掉除了中文、英文、數字之外的
const txtTrim = txt.replace(/[^\u4e00-\u9fa5\u0030-\u0039\u0061-\u007a\u0041-\u005a]+/g, '');
for (let i = 0; i < txtTrim.length; i++) {
matchResult = checkSensitiveWord(sensitiveMap, txtTrim, i);
if (matchResult.flag) {
console.log(`sensitiveWord:${matchResult.sensitiveWord}`);
break;
}
}
return matchResult;
}
複製程式碼
效率
為了看出DFA的效率,我做了個簡單的小測試,測試的文字長度為5095個漢字,敏感詞詞庫中有2000個敏感詞,比較的演算法分別為 DFA演算法 和 String原生物件提供的 indexOf
API做比較
// 簡單的字串匹配-indexOf
ensitiveWords.forEach((word) => {
if (ss.indexOf(word) !== -1) {
console.log(word)
}
})
複製程式碼
分別將兩個演算法執行100次,得到如下結果
可直觀看出,DFA
的平均耗時是在1ms左右,最大為5ms;indexOf
方式的平均耗時在9ms左右,最大為14ms,所以DFA效率上還是非常明顯有優勢的。