python文本关键词匹配_NLP利剑篇之模式匹配
模式匹配:查找字符串中是否存在某個(些)子字符串
在NLP任務中,經常會遇到判斷某些關鍵詞是否在文本中以及在文本中的位置,還有些類似分詞的應用場景,這時就可以利用模式匹配這種小而美的方式。本文主要涉及KMP算法、Trie樹、雙數組Trie樹以及AC自動機四種算法的原理與實現。不同的語言都有類似成熟的實現,在實際應用中,大家可以直接調用包。文本代碼都是由Python實現,完整代碼還未開源(會盡快開源),因為沒有經過嚴格測試,所以代碼僅供參考。查看有道云格式,請點擊這里。
- KMP算法
- Trie樹
- 雙數組Trie樹
- AC自動機
KMP算法
當需要在一段文本(長度為n)中尋找某個確定子串的時候,一般需要
的時間復雜度,KMP算法則可以將時間復雜度降低為線性的。其原理如下:圖中,上面表示文本,下面表示匹配的關鍵詞。假設我們匹配到文本6的位置,此時判斷關鍵詞5的位置與其是否一致,如果一致,則繼續往下匹配,如果不一致,樸素的想法則是像下圖一樣。
我們將關鍵詞的頭移至文本3的位置,開始重新匹配。事實上,我們有更好的選擇。我們不需要再匹配文本2-5的位置,因為文本2-5的位置與關鍵詞1-4是一樣的,這樣我們在匹配前,通過對關鍵詞自身的匹配,就可以知道形如關鍵詞1-4的字符串可以被匹配成什么樣,如下圖所示:
此時假設關鍵詞1-4的后綴中(不包含1-4,只有2-4,3-4,4),是關鍵詞的前綴且最長的是3-4(2-4不是前綴,4不管是不是前綴都比3-4短),此時就相當于文本4-5被關鍵詞1-2匹配了,只需要判斷關鍵詞3與文本6是否一致。也就是關鍵詞5的位置不匹配時,可以直接再判斷關鍵詞3是否與當前匹配。此時的狀態與第一個圖的狀態一致,后續的匹配過程依次循環,直至匹配結束。
當然,如果關鍵詞1-4的后綴中不存在關鍵詞的前綴,那么直接重文本6開始重新匹配,如下圖所示:
根據上面的分析,我們可以知道,在查找的過程中,不需要回退查找,只要掃一遍文本就可以了。其中關鍵點在于,當關鍵詞n位置沒能匹配時,我們需要跳到m位置進行匹配,通常我們用next數組表示這一關系,next[n]=m。其查找方式如下:
def self_match(self):if self.length < 2:returnfor i in range(2, self.length):for j in range(1, i):if self.key[:i-1 - j] == self.key[j:i - 1]:# 當前位置的后綴是關鍵詞的前綴self.next[i] = i-jbreak整個匹配過程,則如下:
def match(self, text):match_pair = []current_ind = 0 # 當前關鍵詞位置for i in range(len(text)):if text[i] == self.key[current_ind]:if self.stop_ind(current_ind):match_pair.append((i - self.length + 1, i + 1))current_ind = 0else:current_ind += 1else:while current_ind > 0:current_ind = self.next[current_ind]if text[i] == self.key[current_ind]:current_ind += 1breakreturn match_pairTrie樹
當關鍵詞有很多個的時候,需要遍歷很多次文本才能找到文本中關鍵詞的位置。此時我們可以將關鍵詞構建成Trie樹,這樣遍歷一次文本(不代表時間復雜度是O(n)),就可以查找所有的關鍵詞。假如有3個關鍵詞,北京、南京、南京大學,那么Trie樹將如下圖所示:
這樣我們就把多個關鍵詞組合成了一個樹,在搜索文本的時候,就可以像匹配一個關鍵詞那樣匹配這個樹,就可以達到一次遍歷,匹配所有關鍵詞的效果。
建立這個樹結構,我們首先需要定義節點:
有了節點的定義,我們就可以根據關鍵詞(最好事先做關鍵詞去重處理)構建樹了:
def build(self):for key in self.keys:tem_node = self.rootfor w in key:if not tem_node.has_child(w):tem_node.add_child(w)tem_node = tem_node.get_child(w)tem_node.end = True那么在樹的基礎上的匹配則是:
def match(self, text):match_pair = []node = self.rootstart = 0for i in range(len(text)):w = text[i]if node.has_child(w):if not node.value: # 根節點的值為Nonestart = inode = node.get_child(w)if node.end:match_pair.append((start, i+1))elif node.value:i = startnode = self.rootreturn match_pair雙數組Trie樹
Trie樹在一定程度了解決了多模式匹配的時間復雜度問題,但因為其結構是樹形結構,在程序中會占用不少內存空間,所以為了降低其空間復雜度,雙數組Trie樹應運而生。
雙數組Trie樹,顧名思義,通過兩個數組來表達Trie樹。
如上圖所示,我們使用兩個數組(node與check數組)來表示Trie樹。node數組左邊的數字表示其索引,Trie樹中的節點與數組的索引一一對應,也就是說數組中有意義的位置(一般來說node數組中不等于0的位置才有意義,除了第一個根節點)都代表一個節點。那么如何使用兩個數組唯一確定表示一個樹呢,我們需要實現兩個功能:1、父節點可以明確地找到某個固定的子節點;2、子節點可以確定其唯一的父節點。這兩個功能分別有node數組與check數組實現。在實現這兩個功能之前,我們還需要將Trie樹中出現的所有字符進行編碼,如上圖左下角所示,北的編碼為1,南的編碼為2等等,在實際應用中,你可以任意的編碼形式,只要滿足:1、編碼值是大于0的整數;2、不同的字符編碼值不同。下面我們來看這兩個數組如何來表示Trie樹的。
父節點如何找到子節點:假設我們現在在節點‘南’上面,需要找到其子節點‘京’,我們已經知道‘南’對應這數組索引為2的位置,node[2]的值為2,而且知道‘京’的編碼值是3,那么節點‘京’的索引為:abs(node[2])+index(京)=2+3=5,即索引為5的位置表示子節點‘京’。但是,如果我們在搜索的過程中,‘南’子后面又出現了‘南’,那第二個‘南’字節點的位置可以計算:abs(node[2])+index(南)=2+2=4,這樣我們也找到了一個對應的位置,但實際上并沒有這個節點,所以我們需要通過判斷子節點的父節點的方法,來判斷我們是否找到了正確的節點。
根據子節點判斷父節點:因為子節點有且只有一個父節點,所以check數組中直接保存這當前節點的父節點位置。比如‘南京’的‘京’子節點索引是5,父節點索引是2,則check[5]=2。同樣的‘南南’的‘南’子節點索引是4(根據上面的計算),父節點索引是2,而check[4]=1,表明‘南南’并不存在Trie樹中。
上圖的node數組中,我們可以看到,有些值是負的,這是因為在這些節點上表示某個關鍵詞的結束,所以在求子節點的時候,我們需要用abs函數取其絕對值。比如‘南京’的‘京’子節點,對應的索引是5,因為‘南京’是一個獨立的關鍵詞,所有node[5]的值的負的(在原值的基礎上乘以-1)。
那node與check數組又是怎么構造出來的呢?一般的,我們會以遞歸跟貪婪的方式構造。假設我們已經知道某個節點在數組中的索引是n(初始的根節點索引取0),那么node[n]的值需要根據它的所有子節點來確定:
這樣我們就求得了node[n]的值,如果此節點是某個關鍵詞的結尾,則讓node[n]=-x。check數組比較簡單,讓check[n]等于其父節點的索引即可。然后遞歸上述過程,則可以完整建立node與check數組。初始化時,給定node與check為固定長的數組,當在建立過程中,很可能會出現數組長度不夠的情況,此時則需要動態增長數組。node與check數組具體建立過程如下:
def build(self):trie = Trie(self.keys)self.node[0] = self.get_value(0, 0, trie.root)del triedef get_value(self, parent_value, parent_index, node):while True:flag = Truefor w in node.child_key:index = self.get_index(w)while len(self.node) < index + parent_value:self.node.extend([0 for i in range(20000)])self.check.extend([0 for i in range(20000)])if self.node[index + parent_value] != 0:flag = Falsebreakif flag:breakparent_value += 1for w in node.child.keys():self.node[self.get_index(w) + parent_value] = self.get_value(1, self.get_index(w) + parent_value, node.get_child(w))if node.get_child(w).end:self.node[self.get_index(w) + parent_value] *= -1self.check[self.get_index(w) + parent_value] = parent_indexreturn parent_valuedef get_index(self, w):return ord(w) + 1匹配過程如下:
def match(self, text):match_pair = []start = parent_index = parent_value = 0for i in range(len(text)):w = text[i]w_index = self.get_index(w)if parent_value + w_index < len(self.node) and self.node[parent_value + w_index] != 0 and self.check[parent_value + w_index] == parent_index:if parent_index == 0:start = iparent_index = parent_value + w_indexparent_value = abs(self.node[parent_index])if self.node[parent_index] < 0:match_pair.append((start, i + 1))elif parent_index > 0:i = startparent_index = parent_value = 0return match_pairAC自動機
雙數組Trie樹緩解了Trie樹的空間復雜度問題,但其時間復雜度依然是
,為了進一步降低其時間復雜度,借助kmp算法的思想,由此有了AC自動機。
如上圖所示,左邊是Trie樹(為理解方便,沒有顯示成雙數組的形式),右邊是帶搜索的文本。假如現在文本匹配到5的位置,發現跟Trie樹中9節點不匹配,跟kmp算法的思路一致,我們現在不需要重新從文本3的位置匹配,而是將Trie樹節點8跳到節點5(此時假設節點8的next數組指向節點5),然后文本5直接匹配是否是Trie樹節點5的子節點,如下圖所示:
這里的next數組跟kmp中類似,表明Trie樹中節點[6,8]與節點[2,5]是一致的。如果根節點到8節點這條路徑上,不包含任何關鍵詞的前綴,則8節點的next節點是根節點,此時直接判斷文本5位置是否是根節點的子節點。
這樣,Trie樹可以通過兩個數組表示,現在又多一個next數組,也就是三個數組可以表示AC自動機,有時也叫做三數組Trie樹,如下圖所示:
AC自動機是在Trie樹的基礎上,新增了next數組,所以其構建過程如下:
AC自動機的匹配過程如下:
def match(self, text):match_pair = []start = parent_value = parent_index = 0for i in range(len(text)):w = text[i]w_index = self.get_index(w)if parent_value + w_index < len(self.node) and self.node[parent_value + w_index] != 0 and self.check[parent_value + w_index] == parent_index:if parent_index == 0:start = iparent_index = parent_value + w_indexparent_value = abs(self.node[parent_index])if self.node[parent_index] < 0:match_pair.append((start, i + 1))else:while parent_index > 0:parent_index = self.next[parent_index]parent_value = abs(self.node[parent_index])if parent_value + w_index < len(self.node) and self.node[parent_value + w_index] != 0 and self.check[parent_value + w_index] == parent_index:for tem_i in range(start, i):if self.prefix_search(text[start:i+i]) > 0:start = ibreakparent_index = parent_value + w_indexparent_value = abs(self.node[parent_index])if self.node[parent_index] < 0:match_pair.append((start, i + 1))breakreturn match_pair總結
以上是生活随笔為你收集整理的python文本关键词匹配_NLP利剑篇之模式匹配的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python数据库哪个好_终于明了pyt
- 下一篇: python websocket ser