正则引擎入门——正则文法匹配可以简单快捷(三)

  整篇文章是对作者Russ Cox的文章Regular Expression Matching Can Be Simple And Fast的翻译,在我看来,该文章是入门正则引擎的较好的文章之一,读者在阅读之前,最好有一定的正则表达式的基础。翻译内容仅代表作者观点。侵删
  该作者所有的文章的网址在此:https://swtch.com/~rsc/regexp/

正文

正则表达式搜索算法

  现在我们已经有了确定一个正则表达式是否匹配一个字符串的方法,将正则表达式转换为NFA之后以字符串为输入运行该NFA。记住NFA在面对多个选择的下一状态时能够很好地选择下一状态。而使用普通的计算机运行NFA时我们必须找到一个能够模拟这一能力的方法。
  一种模拟猜测多种选择的方法是首先选择一个方向进行匹配,如果匹配失败,就选择余下方向中的一个。举个例子,考虑用abab|abbb的NFA去匹配字符串abbb:


  在step0时,NFA就必须做出选择,是去匹配abab或者abbb?在上图中,它首先尝试了abab,但是在step3时失败了。之后NFA选择了另一个方向,进入了step4,最终到达了匹配状态。这种回溯的匹配方式有一种简单的递归的实现方式但是需要在成功匹配之前多次读入字符串。如果这个字符串并不匹配,那状态机在失败之前就会将所有的可能的路径都尝试一遍。 上图中的例子只有两种不同的路径,但是在最坏的情况下可能会有指数级的可能的路径,这会导致非常缓慢的执行时间。
  一个更加高效但是更加复杂的的方法去处理多路径的问题是在同一时间处理所有的路径。在这种方法中,整个匹配过程允许状态机在一个时间处于多个状态,为了处理每一个字符,匹配过程会让每一个满足当前字符的状态通过箭头进入下一状态。

  状态机开始时处于初始状态以及所有可以从初始状态通过空箭头到达的状态,在第1、2步,NFA同时处于两个状态。直到第3步NFA才将可能的状态确定到了一个。这种多状态的处理方式会在同时尝试所有的路径,并且只需读取一遍字符串。在最坏的情况下,NFA可能会在每一步中均处于所有的状态,但这也只会导致常数级别的工作量,且与输入的字符串的长度无关,因此任意长度的输入均只有线性的处理时间。相对于那些采用回溯方法从而需要指数级处理时间的实现来说,上述方法是一种巨大的提升。效率的提升来自于我们只会沿着没有走过的路径去到达可以到达的状态。在一个具有n个节点的NFA中,在匹配的每一步状态机最多只有n个可以到达的状态,但是整个NFA中可能会有2n条路径。

实现

  Thompson 1968年在他的论文中提出了多个状态同时模拟的匹配方法。在他的构想中,NFA的状态是使用一种机器码片段表示的,而所有的可能的状态的列表只不过是一系列的函数调用的指令。本质上,Thompson将正则表达式编译成了机器码。40年后,计算机的速度大大提高,而采用机器码的这种方法也显得不必要了。接下来的章节我们会介绍一种使用ANSI C编写的实现方式。完整的代码(400行以内)以及所有的测试脚本均被放在了网站上。

实现:编译成NFA

  实现的第一步是将正则表达式编译成等价的NFA。在我们的C程序中,我们会将NFA表示为一系列相互连接的State结构体:

1234567
struct State{	int c;	State *out;	State *out1;	int lastlist;};

  每一个State表示下图中三个中的一个NFA片段,具体取决于c的值。

Lastlist在程序执行时被使用,我们会在下一节中介绍它)
  根据Thompson的论文,编译器首先使用(.)作为连接操作符将正则表达式转换为后缀表示形式。一个名叫re2post的函数会将像”a(bb)+a”这样中序表示的正则表达式重写为”abb.+.a.”这样的等价的后缀形式。(而一个真正的正则引擎则会将圆点用作表示任意字符的元字符而不是一个连接操作符。同时一个成熟的正则引擎会在解析表达式时就将NFA构造出来而不是先转换为后缀形式。但是,首先转换的实现方式会更加的方便同时也更贴合Thompson的论文。)
  在编译器扫描后缀表达式的过程中,它会维护一个用于保存已经处理过的NFA片段的栈。遇到文字则会把新的NFA片段压入栈中,而遇到操作符时,则会将栈顶的两个片段取出,并压入新的片段。例如,在处理完abb.+.a.中的abb之后,栈保存了a,b,b的NFA片段,而接下来对 . 的处理会将栈中两个b的NFA片段取出,之后压入相互连接的bb的NFA片段。每一个NFA的片段均由开始状态和一个向外指的箭头构成:

12345
struct Frag{	State *start;	Ptrlist *out;};

  Start指针指向片段(fragment)的开始状态,而out是一系列的State*指针,最初没有指向任何的状态。这些是NFA片段中的空箭头。
  一下是一些操作指针列表的函数:

1234
Ptrlist *(State **outp);Ptrlist *append(Ptrlist *l1, Ptrlist *l2);

void patch(Ptrlist *l, State *s);

  List1函数初始化一个新的只包含指针outp的指针列表。函数Append将两个指针列表连接起来,并返回结果。Patch将列表l中的所有指针与状态s相连。
  通过这些简单的定义以及一个片段栈,编译器的代码就是一个围绕着后缀表达式进行的简单的循环。在程序结束之前,还有最后一步:将结束状态与NFA连接起来从而完成整个NFA的构建。

12345678910111213141516171819202122
State* post2nfa(char *postfix){	char *p;	Frag stack[1000], *stackp, e1, e2, e;	State *s;

	#define pop()   *--stackp

	stackp = stack;	for(p=postfix; *p; p++)	{		switch(*p)		{

		}	}

	e = pop();	patch(e.out, matchstate);	return e.start;}

  以下是模拟整个构建过程的详细的代码:
文字字符:

1234
default:	s = state(*p, NULL, NULL);	push(frag(s, list1(&s->out));	break;

连接符:

123456
case '.':	e2 = pop();	e1 = pop();	patch(e1.out, e2.start);	push(fra 大专栏  正则引擎入门——正则文法匹配可以简单快捷(三)g(e1.start, e2.out));	break;

并联(或):

123456
case '|':	e2 = pop();	e1 = pop();	s = state(Split, e1.start, e2.start);	push(frag(s, append(e1.out, e2.out)));	break;

零个或一个:

12345
case '?':	e = pop();	s = state(Split, e.start, NULL);	push(frag(s, append(e.out, list1(&s->out1))));	break;

零个或多个:

123456
case '*':	e = pop();	s = state(Split, e.start, NULL);	patch(e.out, s);	push(frag(s, list1(&s->out1)));	break;

一个或多个:

123456
case '+':	e = pop();	s = state(Split, e.start, NULL);	patch(e.out, s);	push(frag(e.start, list1(&s->out1)));	break;

实现:模拟NFA

  现在NFA已经被建立了起来,我们现在需要运行它。整个运行过程需要我们记录状态的集合,我们将它们存放于一个简单的数组中:

12345
struct List{	State **s;	int n;};

  运行过程需要两个Listclist是NFA现在正处于的状态的集合,而nlist是NFA在处理完当前的字符之后即将进入的状态的集合。在循环之前clist被初始化为只包含初始状态,之后状态机会一个循环向前推进一步。

1234567891011121314
int match(State *start, char *s){	List *clist, *nlist, *t;

	/* l1 和 l2 是提前初始化好的全局变量 */	clist = startlist(start, &l1);	nlist = &l2;	for(; *s; s++)	{		step(clist, *s, nlist);		t = clist; clist = nlist; nlist = t;	/* 交换 clist, nlist */	}	return ismatch(clist);}

  为了避免在每一次循环时都进行初始化,match函数使用两个提前初始化好的list做为clistnlist,在每一步之后交换两个变量。
  如果最后的状态数组里包含了结束状态,那匹配就成功了。

123456789
int ismatch(List *l){	int i;

	for(i=0; i<l->n; i++)		if(l->s[i] == matchstate)			return 1;	return 0;}

  Addstate函数为list添加一个状态,如果已经存在则不添加。如果每一次添加状态都需要扫描整个列表那就太低效了,我们让listid作为记录list每一代的变量。当调用addstate将状态s加入列表时,它会将listid记录到s->lastlist中。如果它们两者已经相等了,那s已经存在于列表中了。Addstate函数同样会沿着空箭头向前走,如果s是一个Split状态并带有两个空箭头到达新的状态,那Addstate会将这些新状态而不是s加入到列表中。

1234567891011121314
void addstate(List *l, State *s){	if(s == NULL || s->lastlist == listid)		return;	s->lastlist = listid;	if(s->c == Split)	{		/* 顺着空箭头向前一步 */		addstate(l, s->out);		addstate(l, s->out1);		return;	}	l->s[l->n++] = s;}

  Startlist函数初始化一个只包含初始状态的列表:

1234567
List* startlist(State *s, List *l){	listid++;	l->n = 0;	addstate(l, s);	return l;}

  最后,step函数将NFA向前推进一步,它使用当前的列表clist来计算之后一步的列表nlist

1234567891011121314
void step(List *clist, int c, List *nlist){	int i;	State *s;

	listid++;	nlist->n = 0;	for(i=0; i<clist->n; i++)	{		s = clist->s[i];		if(s->c == c)			addstate(nlist, s->out);	}}

原文地址:https://www.cnblogs.com/dajunjun/p/11698438.html

时间: 2024-11-06 10:58:03

正则引擎入门——正则文法匹配可以简单快捷(三)的相关文章

正则引擎在数据包匹配中的工程分析

匹配 常见的通用匹配算法有字符串匹配和正则匹配.字符串匹配常见的算法有Boyer-Moore算法.orspool算法.unday算法.MP算法.R算法.AC自动机.Boyer-Moore.Horspool.Sunday算法都是基于后缀数组的匹配算法,区别在于移动的方式不一样.MP是前缀匹配算法,R算法是hash匹配,AC自动机可以同时匹配多个pattern.正则匹配有两种NFA和DFA,都是基于有穷自动机.NFA支持回朔,DFA的效率比NFA高很多,但支持的情况受限. 正则引擎 正则引擎包括NF

原创 正则引擎完工,记录下思路和设计

最近20天都在写这个...终于完工了(走向无尽的重构道路...)...感谢VC聚聚的博文和RE2作者的博客指导,感谢VC聚聚的源码参考.非常感谢!启发很大.vc聚聚的正则语法树遍历部分的方案.真是精妙!之前我虽然知道用Visitor模式遍历异构树,但是不知道怎么写vistor的框架满足需求.用的时候不断地感叹设计的好.不过我也就抄了这块框架代码:)因为实现的太好了.其他都是根据博文给的参考设计自己去想设计和实现 整个引擎实现了http://blog.csdn.net/lxcnn/article/

正则表达式30分钟入门教程 ——堪称网上能找到的最好的正则式入门教程

本教程堪称网上能找到的最好正则表达式入门教程 原地址:http://www.jb51.net/tools/zhengze.html 本文目标 30分钟内让你明白正则表达式是什么,并对它有一些基本的了解,让你可以在自己的程序或网页里使用它. 如何使用本教程 最重要的是——请给我30分钟,如果你没有使用正则表达式的经验,请不要试图在30秒内入门——除非你是超人 :) 别被下面那些复杂的表达式吓倒,只要跟着我一步一步来,你会发现正则表达式其实并没有想像中的那么困难.当然,如果你看完了这篇教程之后,发现

60分钟正则从入门到深入

https://segmentfault.com/a/1190000013075245 本文转载自网络.转载编辑过程中,可能有遗漏或错误,请以原文为准.原文作者:水墨寒湘原文链接:https://juejin.im/post/582dfc... 正则表达式对于我来说一直像黑暗魔法一样的存在.手机正则去网上搜,邮箱正则去网上搜,复杂点的看看文档拼凑一下,再复杂只能厚着脸皮让其他同事给写一个,从来没有系统的学习过.关于作者这几句话,我是深有感触,有幸畅游网络看到这篇博文和对应的慕课网视频,让我收获颇

正则 —— 第一次入门

正则 正则表达式(regular expression)描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串.将匹配的子串替换或者从某个串中取出符合某个条件的子串等. Nlife+a,可以匹配Nlifea,Nlifeea,Nlifeeea ...... ,+号代表前面的字符至少出现一次. Nlife*a,可以匹配Nlifa,Nlifea,Nlifeea,Nlifeeea ...... ,*号代表前面的字符可以不出现,或者出现多次. Nlife?a,可以匹配Nlifa,

.NET正则基础——.NET正则类及方法应用

1        概述 初学正则时,对于Regex类不熟悉,遇到问题不知道该用哪种方法解决,本文结合一些正则应用的典型应用场景,介绍一下Regex类的基本应用.这里重点进行.NET类的介绍,对于正则的运用,不做深入探讨. 正则的应用最终都是进行模式的匹配,而根据目的的不同,基本上可以分为以下几种应用:验证.提取.替换.分割.结合.NET提供的控件.类以及类的方法,可以很方便的实现这些应用. 以下将结合一些典型的应用场景,对.NET中常见的类.方法及属性进行介绍.本文旨在.NET类基础用法的引导,

看看你的正则行不行——正则优化一般的json字符串

json字符串很有用,有时候一些后台接口返回的信息是字符串格式的,可读性很差,这个时候要是有个可以格式化并高亮显示json串的方法那就好多了,下面看看一个正则表达式完成的json字符串的格式化与高亮显示 首先是对输入进行转换,如果是对象则转化为规范的json字符串,不是对象时,先将字符串转化为对象(防止不规范的字符串),然后再次转化为json串.其中json为输入. if (typeof json !== 'string') { json = JSON.stringify(json); } el

Python正则及geometer正则截图讲解

正则表达式 语法: 1 2 3 4 5 6 import re #导入模块名 p = re.compile("^[0-9]")  #生成要匹配的正则对象 , ^代表从开头匹配,[0-9]代表匹配0至9的任意一个数字, 所以这里的意思是对传进来的字符串进行匹配,如果这个字符串的开头第一个字符是数字,就代表匹配上了 m = p.match('14534Abc')   #按上面生成的正则对象 去匹配 字符串, 如果能匹配成功,这个m就会有值, 否则m为None<br><br

基础正则和扩展正则的作用

*基础正则表达式:basic regular expression BRE包括:^ $ . [] [^] 和扩展正则不同的是grep和sed不需要加参数也可以使用 ^^d 以d开头的行,例如:ls l|grep "^d" 给三剑客使用只查看以d开头的行,正则表达式的意思为,以.....开头,^d就是以d开头[[email protected] data]# grep '^m' oldboy.txt 以m开头的行my qq is 49000448 $以什么什么结尾的行,例如grep &q