敏感词过滤算法Aho-Corasick
多模式串匹配算法簡介
敏感詞過濾最基本的原理就是字符串匹配算法,也就是通過維護一個敏感詞的字典,當用戶輸入一段文字內容后,通過字符串匹配算法,來查找用戶輸入的這段文字,是否包含敏感詞。
字符串匹配算法有很多比如BF算法、RK算法、BM算法、KMP算法還有Trie樹。前面四種算法都是單模式串匹配算法,只有Trie樹是多模式串匹配算法。
我們可以針對每個敏感詞,通過單模式匹配算法與用戶輸入的文字內容進行匹配。但是這樣做的話,每個需要匹配的敏感詞都需要掃描一遍用戶輸入的內容。如果敏感詞有很多,并且用戶輸入的內容很長,這種處理的方法就顯得比較低效。
與單模式匹配算法相比,多模式串匹配算法在敏感詞過濾這個問題上處理就很高效了,它只需要掃描一遍主串,就能在主串中一次性查找多個模式串是否存在。
Aho-Corasick算法
Aho-Corasick算法一般稱作AC自動機,AC自動機實際上就是在Trie樹之上,加了類似KMP的next數組,只不過此處的next數組是構建在trie樹上罷了。
AC自動機有三個核心函數,分別是:
- success狀態,成功轉移到下一個節點(即Trie樹)
- failure狀態,在該節點匹配失敗,則跳轉到一個特定的節點,從根節點到這個特定的節點的路徑恰好是失敗前文本的一部分。
- output狀態,匹配到了敏感詞
根據以上AC算法的特點,改進Trie節點的屬性如下:
function TrieNode(key, parent, word) {this.key = key;this.children = []; this.parent = parent; // 該節點的父節點,用于構建failure表this.failure = null; // 失效之后指向的節點this.word = word // 該節點是否為某一個敏感詞的尾字符 }構建Trie樹
和普通的trie構建是一樣的,逐個插入節點。假設敏感詞以及待過濾的字符串,均是小寫的英文字母。以英文字母的ASCII碼作為數組下標存儲節點。
function insert(data) {this.insertData(data, this.root); }function insertData(data, node){if (data === '') {return;}let children = node.children;let haveData = children[data[0].charCodeAt() - 97];if(haveData) {this.insertData(data.substring(1), haveData);}else{let isWord = data.length === 1;let insertNode = new TrieNode(data[0], node, isWord);children[data[0].charCodeAt() - 97] = insertNode;this.insertData(data.substring(1), insertNode); } }添加Failure失效節點
下圖是以['HER', 'HEQ', 'SHR']構建的trie樹:
在這張圖中,虛線表示failure后的指向,上面我們也說到failure狀態的作用,就是在失配的時候告訴程序往哪里走,為什么要這么做,從這張表我們可以很清楚的看到,當我們匹配SHER時,程序會走右邊的分支,當走到S > H > E時,會出現失配,怎么辦?可能有小伙伴會想到回滾到ROOT從H開始重新匹配,但這樣回溯是有成本的,我們既然走了H節點,為什么要回溯呢?
這個時候failure就發揮作用了,我們看到右分支的H有一條虛線指向了左分支的H,我們也知道這就是failure的指向,通過這個指向,我們很輕松的將當前狀態移交過去。程序繼續匹配E > R,加上移交過來的H,我們可以輕松的匹配到HER。
問:假設有一個節點為currNode,它的子節點是childNode,那么子節點childNode的failure指向怎么求?
解:首先,我們需要找到childNode父節點currNode的failure指向,假設這個指向是Q的話,我們就要看看Q的孩子(children屬性)中有沒有與childNode字符相同(key相同)的節點,如果有的話,這個節點就是childNode的failure指向。如果沒有,我們就需要沿著currNode -> failure -> failure重復上述過程,如果一直沒找到,就將其指向root。
由此可知,一個節點的失效指針一定在該節點的上層。需要注意的是,我們在構建Trie樹時,并不知道failure指向到哪里的,所以failure指向需要在Trie樹構建完成后插入。
首先將trie樹第二層節點的失效指針指向root,之后逐層為每一個節點添加失效指針,即采用廣度優先遍歷Trie樹:
function getFailure() {let currQueue = Object.values(this.root.children);while (currQueue.length > 0) {let nextQueue = [];for (let i = 0; i < currQueue.length; i++) {let node = currQueue[i]let key = node.keylet parent = node.parentnode.failure = this.rootfor (let k in node.children) {nextQueue.push(node.children[k])}if (parent) {let failure = parent.failurewhile (failure) {let children = failure.children[key.charCodeAt() - 97]if (children) {node.failure = childrenbreak;}failure = failure.failure}}}currQueue = nextQueue} }敏感詞過濾
對于Trie樹上的一些準備工作已經做完了,下面就是要對待匹配的字符串進行過濾。從頭遍歷當遇到output表中的節點時,就是出現了敏感詞。在匹配失敗的時候順著失效節點繼續匹配過程:
function filter(word) {let children = this.root.children;let currentNode = this.root;for(let i=0; i<word.length; i++){while(currentNode.children[word[i].charCodeAt() - 97] == null && currentNode != this.root) {currentNode = currentNode.failure;}currentNode = currentNode.children[word[i].charCodeAt() - 97];if (currentNode == null) {currentNode = this.root;}let temNode = currentNode;while(temNode != this.root) {if(temNode.word === true) {console.log('出現了敏感詞');} temNode = temNode.failure;}} }測試
let trie = new Trie();// 生成trie樹 trie.insert('he'); trie.insert('his'); trie.insert('she'); trie.insert('hers');trie.getFailure();// 測試數據 trie.filter('ushers')// 該字符串出現了三個敏感詞完整代碼
function TrieNode(key, parent, word) {this.key = key;this.children = [];this.parent = parent;this.failure = null;this.word = word }function Trie() {this.root = new TrieNode('/', null, false); // 添加根節點this.insert = insert; // 插入this.insertData = insertData;this.getFailure = getFailure;this.filter = filter; }function insert(data) {this.insertData(data, this.root); }function insertData(data, node){if (data === '') {return;}let children = node.children;let haveData = children[data[0].charCodeAt() - 97];if(haveData) {this.insertData(data.substring(1), haveData);}else{let isWord = data.length === 1;let insertNode = new TrieNode(data[0], node, isWord);children[data[0].charCodeAt() - 97] = insertNode;this.insertData(data.substring(1), insertNode); } }function getFailure() {let currQueue = Object.values(this.root.children);while (currQueue.length > 0) {let nextQueue = [];for (let i = 0; i < currQueue.length; i++) {let node = currQueue[i]let key = node.keylet parent = node.parentnode.failure = this.rootfor (let k in node.children) {nextQueue.push(node.children[k])}if (parent) {let failure = parent.failurewhile (failure) {let children = failure.children[key.charCodeAt() - 97]if (children) {node.failure = childrenbreak;}failure = failure.failure}}}currQueue = nextQueue} }function filter(word) {let children = this.root.children;let currentNode = this.root;for(let i=0; i<word.length; i++){while(currentNode.children[word[i].charCodeAt() - 97] == null && currentNode != this.root) {currentNode = currentNode.failure;}currentNode = currentNode.children[word[i].charCodeAt() - 97];if (currentNode == null) {currentNode = this.root;}let temNode = currentNode;while(temNode != this.root) {if(temNode.word === true) {console.log('出現了敏感詞');} temNode = temNode.failure;}} }let trie = new Trie();// 生成trie樹 trie.insert('he'); trie.insert('his'); trie.insert('she'); trie.insert('hers');trie.getFailure();// 測試數據 trie.filter('ushers')總結
以上是生活随笔為你收集整理的敏感词过滤算法Aho-Corasick的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【python】10行代码下载B站弹幕
- 下一篇: 关于Hibernate