模式匹配:查找字符串中是否存在某个(些)子字符串
在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_pair

Trie树

当关键词有很多个的时候,需要遍历很多次文本才能找到文本中关键词的位置。此时我们可以将关键词构建成Trie树,这样遍历一次文本(不代表时间复杂度是O(n)),就可以查找所有的关键词。假如有3个关键词,北京、南京、南京大学,那么Trie树将如下图所示:

这样我们就把多个关键词组合成了一个树,在搜索文本的时候,就可以像匹配一个关键词那样匹配这个树,就可以达到一次遍历,匹配所有关键词的效果。
建立这个树结构,我们首先需要定义节点:

class Node(object):def __init__(self, value):self.value = valueself.child = {}self.child_key = []    # 为了保证key的顺序self.end = False

有了节点的定义,我们就可以根据关键词(最好事先做关键词去重处理)构建树了:

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]的值需要根据它的所有子节点来确定:

  1. 设其为x,令x=1
  2. 求出它所有子节点的索引[m1,m2,...]
  3. 如果node[m1],node[m2],...都为0,则结束,返回x
  4. 否者x=x+1,转到第2步。

这样我们就求得了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_pair

AC自动机

双数组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数组,所以其构建过程如下:

class AhoCorasickAutoMation(DoubleArrayTrie):def __init__(self, keys):super(AhoCorasickAutoMation, self).__init__(keys)self.next = [0 for v in self.check]self.build_next()def build_next(self):for key in self.keys:key_index = self.prefix_search(key)if len(key) < 2:self.next[key_index] = 0continuetem_index = 0for i in range(1, len(key)):tem_index = self.prefix_search(key[i:])if tem_index > 0:breakself.next[key_index] = tem_index if tem_index > 0 else 0def prefix_search(self, key):parent_value = parent_index = 0for i in range(len(key)):w = key[i]w_index = self.get_index(w)if w_index + parent_value < len(self.check) and self.node[w_index + parent_value] != 0 and self.check[w_index + parent_value] == parent_index:parent_index = w_index + parent_valueparent_value = abs(self.node[parent_index])else:return -1return parent_index

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利剑篇之模式匹配相关推荐

  1. python文本关键词提取_python提取文本关键词

    python提取关键词textrank算法,将数据库中的数据提取出来,然后进行分析,代码如下 import pymysql import jieba from textrank4zh import T ...

  2. python字符串模糊匹配_NLP教程:用Fuzzywuzzy进行字符串模糊匹配

    在计算机科学中,字符串模糊匹配( fuzzy string matching)是一种近似地(而不是精确地)查找与模式匹配的字符串的技术.换句话说,字符串模糊匹配是一种搜索,即使用户拼错单词或只输入部分 ...

  3. python文本关键词提取_python实现关键词提取

    1 importjieba2 importjieba.analyse3 4 #第一步:分词,这里使用结巴分词全模式 5 text = '''新闻,也叫消息,是指报纸.电台.电视台.互联网经常使用的记录 ...

  4. python的总结与心得词云设计理念_1 Python文本分析——词云分析篇

    1首先打开Pycharm,创建一个项目,命名English-Wordcloud,然后创建一个English-Wordcloud.py文件,见下图,继而开始敲代码,非常简短的代码. 2 导入词云包,导入 ...

  5. python 实现关键词提取

    Python 实现关键词提取 看到一篇很好的关键词提取的论文,<融合LDA与TextRank算法的主题信息抽取方法>.里面对LDA和TextRank的发展过程描述的很详细.如果你跟我一样对 ...

  6. python关键字匹配_python通过BF算法实现关键词匹配的方法

    本文实例讲述了python通过BF算法实现关键词匹配的方法.分享给大家供大家参考.具体实现方法如下: #!/usr/bin/python # -*- coding: UTF-8 # filename ...

  7. python问题关键词匹配算法_python通过BF算法实现关键词匹配的方法

    本文实例讲述了python通过BF算法实现关键词匹配的方法.分享给大家供大家参考.具体实现方法如下: #!/usr/bin/python # -*- coding: UTF-8 # filename ...

  8. python地图匹配_python通过BF算法实现关键词匹配的方法

    本文实例讲述了python通过BF算法实现关键词匹配的方法.分享给大家供大家参考.具体实现方法如下: #!/usr/bin/python # -*- coding: UTF-8 # filename ...

  9. python中文文本分词_SnowNLP:?中文分词?词性标准?提取文本摘要,?提取文本关键词,?转换成拼音?繁体转简体的 处理中文文本的Python3 类库...

    SnowNLP是一个python写的类库,可以方便的处理中文文本内容,是受到了TextBlob的启发而写的,由于现在大部分的自然语言处理库基本都是针对英文的,于是写了一个方便处理中文的类库,并且和Te ...

最新文章

  1. php ci提交表单验证,ci表单验证代码
  2. cocos2d-x初探学习笔记(13)--内存回收机制
  3. weblogic运行项目_在WebLogic 12c上运行RichFaces
  4. python基础总结(6)
  5. android中将日志文件输出到sd卡
  6. 在计算机中如何共享文件夹,如何打开计算机共享-在电脑里设置了共享文件在另在一台电脑里怎么 – 手机爱问...
  7. 中国教育与软件企业的共同误区
  8. 车辆违章演示示例代码
  9. Dart教程(二):基本语法
  10. 【游戏开发3D数学笔记】1.有话说在前面
  11. PR菜鸟入门 -- PR下载安装
  12. 利用sql循环语句实现基本的数据累加和阶乘
  13. 网页保存视频最有效的几种方法
  14. 日常英语口语900句
  15. 《图解TCP/IP》读书笔记
  16. java 生成2位随机数_java生成随机数保留数点后两位
  17. spring事务管理器的源码和理解
  18. 求大神帮帮我,万分感谢,源码运行需要时间段,帮帮忙哈……
  19. 梅科尔工作室-李庆浩 深度学习-KNN算法
  20. ARouter路由解析

热门文章

  1. c#中调用Excel
  2. JavaScript不区分 '
  3. LGB + KFold 代码 (1)
  4. android.support.v7.app.ActionBarActivity
  5. 程序员面试系列——有符号数的溢出
  6. c++ 一个头文件引用另一个头文件的类
  7. 日常生活小技巧 -- Notepad++一次删除带指定关键字的行
  8. C语言再学习 -- 三字母词(转)
  9. 二分匹配(匈牙利算法)
  10. Android 中 RegistrantList消息处理机制 以android 5.0 MT为例