敏感詞過濾的算法原理之 Aho-Corasick 算法


參考文檔

http://www.hankcs.com/program/algorithm/implementation-and-analysis-of-aho-corasick-algorithm-in-java.html

 

 

簡介

Aho-Corasick算法簡稱AC算法,通過將模式串預處理為確定有限狀態自動機,掃描文本一遍就能結束。其復雜度為O(n),即與模式串的數量和長度無關。

思想

自動機按照文本字符順序,接受字符,並發生狀態轉移。這些狀態緩存了“按照字符轉移成功(但不是模式串的結尾)”、“按照字符轉移成功(是模式串的結尾)”、“按照字符轉移失敗”三種情況下的跳轉與輸出情況,因而降低了復雜度。

基本構造

AC算法中有三個核心函數,分別是:

  • success; 成功轉移到另一個狀態(也稱goto表或success表)

  • failure; 不可順着字符串跳轉的話,則跳轉到一個特定的節點(也稱failure表),從根節點到這個特定的節點的路徑恰好是失敗前的文本的一部分。

  • emits; 命中一個模式串(也稱output表)

舉例

以經典的ushers為例,模式串是he/ she/ his /hers,文本為“ushers”。構建的自動機如圖:

其實上圖省略了到根節點的fail邊,完整的自動機如下圖:

匹配過程

自動機從根節點0出發

  1. 首先嘗試按success表轉移(圖中實線)。按照文本的指示轉移,也就是接收一個u。此時success表中並沒有相應路線,轉移失敗。

  2. 失敗了則按照failure表回去(圖中虛線)。按照文本指示,這次接收一個s,轉移到狀態3。

  3. 成功了繼續按success表轉移,直到失敗跳轉步驟2,或者遇到output表中標明的“可輸出狀態”(圖中紅色狀態)。此時輸出匹配到的模式串,然后將此狀態視作普通的狀態繼續轉移。

算法高效之處在於,當自動機接受了“ushe”之后,再接受一個r會導致無法按照success表轉移,此時自動機會聰明地按照failure表轉移到2號狀態,並經過幾次轉移后輸出“hers”。來到2號狀態的路不止一條,從根節點一路往下,“h→e”也可以到達。而這個“he”恰好是“ushe”的結尾,狀態機就仿佛是壓根就沒失敗過(沒有接受r),也沒有接受過中間的字符“us”,直接就從初始狀態按照“he”的路徑走過來一樣(到達同一節點,狀態完全相同)。

構造過程

看來這三個表很厲害,不過,它們是怎么計算出來的呢?

goto表

很簡單,了解一點trie樹知識的話就能一眼看穿,goto表就是一棵trie樹。把上圖的虛線去掉,實線部分就是一棵trie樹了。

output表

output表也很簡單,與trie樹里面代表這個節點是否是單詞結尾的結構很像。不過trie樹只有葉節點才有“output”,並且一個葉節點只有一個output。下圖卻違背了這兩點,這是為什么呢?其實下圖的output會在建立failure表的時候進行一次拓充。

以上兩個表通過一個dfs就可以構造出來。關於trie樹的更詳細內容,請參考:《Ansj分詞雙數組Trie樹實現與arrays.dic詞典格式》,《Trie樹分詞》,《雙數組Trie樹(DoubleArrayTrie)Java實現》。

failure表

這個表是trie樹沒有的,加了這個表,AC自動機就看起來不像一棵樹,而像一個圖了。failure表是狀態與狀態的一對一關系,別看圖中虛線亂糟糟的,不過你仔細看看,就會發現節點只會發出一條虛線,它們嚴格一對一。

這個表的構造方法是:

  1. 首先規定與狀態0距離為1(即深度為1)的所有狀態的fail值都為0。

  2. 然后設當前狀態是S1,求fail(S1)。我們知道,S1的前一狀態必定是唯一的(剛才說的一對一),設S1的前一狀態是S2,S2轉換到S1的條件為接受字符C,測試S3 = goto(fail(S2), C)。

  3. 如果成功,則fail(S1) = goto(fail(S2), C) = S3。

  4. 如果不成功,繼續測試S4 = goto(fail(S3), C)是否成功,如此重復,直到轉換到某個有效的狀態Sn,令fail(S1) = Sn。

Java實現

原理誰都可以說幾句的,可是優雅健壯的代碼卻不是那么容易寫的。我考察了Git上幾個AC算法的實現,發現robert-bor的實現非常好。一趟代碼看下來,學到了不少設計上的知識。我fork了下來,針對Ascii做了優化,添加了中文注釋。

另外,我實現了基於雙數組Trie樹的AC自動機:《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。性能更高,內存可控。

開源項目

開源在https://github.com/hankcs/aho-corasick

調用方法

1
2
3
4
5
6
7
         Trie trie =  new  Trie();
         trie.addKeyword( "hers" );
         trie.addKeyword( "his" );
         trie.addKeyword( "she" );
         trie.addKeyword( "he" );
         Collection<Emit> emits = trie.parseText( "ushers" );
         System.out.println(emits);

輸出:

1
[2:3=he, 1:3=she, 2:5=hers]

此外,還有一些配置選項:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
     /**
      * 大小寫敏感
      * @return
      */
     public  Trie caseInsensitive()
     {
         this .trieConfig.setCaseInsensitive( true );
         return  this ;
     }
 
     /**
      * 不允許模式串在位置上前后重疊
      * @return
      */
     public  Trie removeOverlaps()
     {
         this .trieConfig.setAllowOverlaps( false );
         return  this ;
     }
 
     /**
      * 只匹配完整單詞
      * @return
      */
     public  Trie onlyWholeWords()
     {
         this .trieConfig.setOnlyWholeWords( true );
         return  this ;
     }

org.ahocorasick.trie包

這里封裝了Trie樹,其中比較重要的類是Trie樹的節點State:

我重構了State,將其異化為UnicodeState和AsciiState類。其中UnicodeState類使用 Map<Character, State> 來儲存goto表,而AsciiState類使用數組 State[] success = new State[256]來儲存,這樣在Ascii表上面,AsciiState的匹配要稍微快一些,相應的在構建時會慢一些,內存占用也會多一些。

 

 

從對萬字的英語詞典的測試結果來看,AsciiState的確有那么一點優勢:

1
2
3
4
5
6
7
8
asciiTrie adding time:1013ms
unicodeTrie adding time:96ms
 
asciiTrie building time:903ms
unicodeTrie building time:312ms
 
asciiTrie parsing time:355ms
unicodeTrie parsing time:463ms

org.ahocorasick.interval包

這里封裝了一棵線段樹,關於線段樹的介紹請查看:線段樹

線段樹用於修飾最后的匹配結果,匹配結果中有一些可能會重疊,比如she和he,這棵線段樹對匹配結果(一系列區間)進行索引,能夠在log(n)時間內判斷一個區間與另一個是否重疊。詳細的實現請看代碼,都有中文注釋,應該很好懂。

基於雙數組Trie樹的Aho Corasick自動機

AC自動機能高速完成多模式匹配,然而具體實現聰明與否決定最終性能高低。大部分實現都是一個Map<Character, State>了事,無論是TreeMap的對數復雜度,還是HashMap的巨額空間復雜度與哈希函數的性能消耗,都會降低整體性能。

雙數組Trie樹能高速O(n)完成單串匹配,並且內存消耗可控,然而軟肋在於多模式匹配,如果要匹配多個模式串,必須先實現前綴查詢,然后頻繁截取文本后綴才可多匹配,這樣一份文本要回退掃描多遍,性能極低。

如果能用雙數組Trie樹表達AC自動機,就能集合兩者的優點,得到一種近乎完美的數據結構。具體實現請參考《Aho Corasick自動機結合DoubleArrayTrie極速多模式匹配》。

 

Reference

部分圖片和介紹來自:

http://www.cnblogs.com/zzqcn/p/3525636.html

http://blog.csdn.net/sealyao/article/details/4560427


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM