1、历史起源
正则表达式的“鼻祖”或许可一直追溯到科学家对人类神经系统工作原理的早期研究。美国新泽西州的Warren McCulloch和出生在美国底特律的Walter Pitts这两位神经生理方面的科学家,研究出了一种用数学方式来描述神经网络的新方法,他们创新地将神经系统中的神经元描述成了小而简单的自动控制元,从而作出了一项伟大的工作革新。
在1956 年,美国的一位名叫Stephen Kleene的数学科学家,他在Warren McCulloch和Walter Pitts早期工作的基础之上,发表了一篇题目是《神经网事件的表示法》的论文,利用称之为正则集合的数学符号来描述此模型,引入了正则表达式的概念。正则表达式被作为用来描述其称之为“正则集的代数”的一种表达式,因而采用了“正则表达式”这个术语。
之后一段时间,人们发现可以将这一工作成果应用于其他方面。Ken Thompson就把这一成果应用于计算搜索算法的一些早期研究,Ken Thompson是 Unix的主要发明人,也就是大名鼎鼎的Unix之父。Unix之父将此符号系统引入编辑器QED,然后是Unix上的编辑器ed,并最终引入grep。
Unix的grep家族包括grep、egrep和fgrep。egrep和fgrep的命令只跟grep有很小不同。egrep是grep的扩展,支持更多的元字符,fgrep就是把所有的字母都看作单词,也就是说,正则表达式中的元字符表示回其自身的字面意义,不再特殊。
众多UNIX工具支持正则表达式,近二十年来,在WINDOW的阵营下,正则表达式的思想和应用在大部分 Windows 开发者工具包中得到支持和嵌入应用!目前主流的开发语言(PHP、C#、Java、C++、VB、Javascript、Ruby以及python等)、数以亿万计的各种应用软件中,都可以看到正则表达式优美的舞姿。
2、Egrep元字符
文本检索式正则表达式最简单的应用之一——许多文本编辑器和文字处理软件都提供了正则表达式检索的功能。最简单也是最流行的就是egrep。几乎所有流派(在这里我把不同的语言或处理软件称作流派)支持的元字符都是根据egrep进行的扩展或者修改。
2.1Egrep元字符^$
脱字符号‘^’和美元符号‘$’分别代表了一行的开始和结束。
例如:`^cat`代表需要匹配以cat开头的一行。希望大家按照字符来理解正则表达式的习惯:不要将`^cat`理解为“匹配以cat开头的行”,而是理解为“匹配的是以c作为一行的第一个字符,紧接一个a,紧接一个t的文本。
(注:后面的我都会把正则表达式用``括起来)
如上两种理解并无差异,但按照字符来解读更易于明白正则表达式的内部逻辑。脱字符号和美元符号的特别之处在于,它们匹配的是一个位置,而不是具体的文本。egrep会如何解释`^cat$`、`^$`和单个的`^`呢?
`^cat$` 文字意义:匹配的条件是,行开头(显然,每一行都有开头 ),然后是字母cat,然后是行末尾。
应用意义:只包含cat的行——没有多余的单词、空白字符……只有cat。
`^$` 文字意义:匹配的条件是,行开头,然后就是行末尾。
应用意义:空行(没有任何字符,包括空白字符)。
`^` 文字意义:匹配条件是行的开头。
应用意义:无意义!因为每一行都有开头,所以每一行都能匹配——空行也不例外。
例子1:
var reg=/^[abc][abc][0123456789]/; var result1=reg.exec("ca4at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/cat$/; var result1=reg.exec("cat"); var result2=reg.exec("catg"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/^cat$/; var result1=reg.exec("cat"); var result2=reg.exec("catcat"); document.write(result1+" "+result2);
结果:
2.2Egrep元字符[]-
如果我们需要搜索的单词是“grey”,同时又不能确定它是否写作“gray”,就可以使用正则表达式结构体‘[]’,也称作字符组。所以我们可以写作`gr[ae]y`,这里需要注意:字符组只要满足一个字符就可以匹配成功;字符组里面的顺序一般情况下可以随意。
如果我们想匹配1到6中的任意数字,我们可以写作`[123456]`,我们也可以通过字符组元字符‘-’(连字符)表示,也就是`[1-6]`,同样的效果。`[0-9]`和`[a-z]`是常用的匹配数字和小写字母的简便方式。多重范围也是允许的,如`[0-9a-zA-F]`。我们还可以随心所欲的把字符范围与普通文本结合起来:`[0-9A-Z_!.?]`能够匹配一个数字、大写字母、下划线、惊叹号、点号或者是问号。
请注意,只有在字符组内部,连字符才可能是元字符——否则它就只能匹配普通的连字符。这里用可能是因为如果连字符出现在字符组的开头,它代表的就只是匹配一个普通连字符,而不是一个范围。同样的道理,问号和点号通常被当做元字符处理,但在字符组中则不是如此。如`[-a-z]`就是匹配一个连字符或者小写字母。
例子1:
var reg=/gr[ae]y/; var result1=reg.exec("grygrey"); var result2=reg.exec("grygraey"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/^[abc][abc][0123456789]/; var result1=reg.exec("ca4at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/^[a-z]/; var result1=reg.exec("c24at"); var result2=reg.exec("1catg"); document.write(result1+" "+result2);
结果:
例子4:
var reg=/^[0-9a-z_]$/; var result1=reg.exec("_"); var result2=reg.exec("catcat"); document.write(result1+" "+result2);
结果:
例子5:
var reg=/^[-!][0-9][a-z]/; var result1=reg.exec("-7fff"); var result2=reg.exec("!75fff"); document.write(result1+" "+result2);
结果:
2.3Egrep元字符[^]
用“[^]”取代“[]”,这个字符组就会匹配任何未列出的字符,我们称作排除型字符组。我们注意到这里的‘^’和我们表示首行的脱字符是一样的,字符确实相同,但意义截然不同,在字符组内部必须是紧接在字符组的第一个方括号之后,它才表示一个元字符。
如果我们用`q[^u]`来匹配字符串伊拉克“Iraq”是否会成功?这里需要强调一下一个字符组,即使是排除型字符组,也需要匹配一个字符。上面的匹配伊拉克字符串会失败,因为q后面没有字符,而`[^u]`必须匹配一个字符,无论是空格、换行符或者其他单词,都必须至少有一个。
例子1:
var reg1=/[^a-z]/; var result1=reg1.exec("a24at"); var result2=reg1.exec("catg"); document.write(result1+" "+result2);
结果:
例子2:
var reg1=/^[0-9a-z_^]$/; var reg2=/^[^^0-9]/; var result1=reg1.exec("^"); var result2=reg1.exec("catcat"); var result3=reg2.exec("-7fff"); var result4=reg2.exec("^75fff"); document.write(result1+" "+result2+" "+result3+" "+result4);
结果:
例子3:
var reg=/q[^0-9]/; var result1=reg.exec("a2qat"); var result2=reg.exec("Iraq"); document.write(result1+" "+result2);
结果:
2.4Egrep元字符.
元字符‘.’是用来匹配任意字符(除换行以外)的字符组的简便写法。如果我们需要在表达式中使用一个“匹配任何字符”的占位符,用点号就很方便。例如:我们需要搜索2013/01/04、2013-01-04或者2013.01.04,我们可以使用`2013.01.04`或者`2013[-./]01[-./]04[-./]`都可以,但是哪种好呢?
在`2013[-./]01[-./]04[-./]`中的点号不是元字符,因为它在字符组内部(记住在字符组里面和外面,元字符的定义和意义是不一样的)。这里的连字符同样也不是字符,因为它们都紧接在[或者[^之后。点号是元字符时能够匹配任意字符,所以也可以匹配如“2013301504”的字符串,所以`2013[-./]01[-./]04[-./]`更加精确,但是更难读,也更难写。 `2013.01.04`更容易理解,但是不够细致。
例子:
var reg=/2013.01.04/; var result1=reg.exec("reg2013-01-04"); var result2=reg.exec("2013-1-04"); document.write(result1+" "+result2);
结果:
2.5Egrep元字符| ()
‘|’是一个非常简捷的元字符,它的意思是“或”。回头来看`gr[ea]y`的例子,我们还可以写作`grey|gray`或者是`gr(e|a)y`。后者用括号来划定多选结构的范围(正常情况下括号也是元字符),对于表达式`gr(e|a)y`来说括号是必须的,不然就变成了`gre|ay`,而这个代表匹配gre或者ay。
`Jeffrey|Jeffery`、`Jeff(rey|ery)`和`Jeff(re|er)y`三个表达式是等价的。gr[ea]y`与`gr(e|a)y`的例子可能会让人觉得多选结构与字符组没太大区别,但是请不要混淆这两个的概念。一个字符组只能匹配目标文本中的单个字符,而每个多选结构自身可能是完整的正则表达式,都可以匹配任意长度的文本。
例子1:
var reg=/grey|gray/; var result1=reg.exec("reggray"); var result2=reg.exec("greay"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/(ca)t/; var result1=reg.exec("ttcatty"); var result2=reg.exec("ctat"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/gr(e|a)y/; var result1=reg.exec("sagrey"); var result2=reg.exec("greay"); document.write(result1+" "+result2);
结果:
2.6Egrep元字符?+*
现在来看color和colour的匹配。他们区别在后面的单词比前面的多一个u,我们可以使用`colou?r`来解决这个问题。元字符‘?’代表可选项。再来看`four(th)?`,这里‘?’作用的元素是整个括号,括号内的表达式可以任意复杂,但是“从括号外来看”它们是一个整体。
‘+’(加号)和‘*’(星号)的作用于问号类似。元字符‘+’表示“之前紧邻的元素出现一次或多次”,而‘*’表示“之前紧邻的元素出现任意多次,或者不出现”。问号、加号和星号这三个元字符统称为量词,因为它们限定了所作用元素的重现次数。
接下来看类似<HR·SIZE=14>这样的HTML tag,它表示一条高度为14像素的穿越屏幕的水平线。在最后的尖括号之前可能出现任意多个空格,此外在等号两边也容易出现任意多个空格,最后在HR和SIZE之间必须至少一个空格。所以我们得到`<HR·+SIZE·*=·*14·*>`。如果我们找的tag的14这个数字希望是任意的,那么可以使用`<HR·+SIZE·*=·*[0-9]+·*>`。
(注:后面都使用’·’代表一个空格)
如果我们希望SIZE也是可有可无的呢?改成`<HR(·+SIZE·*=·*[0-9]+)?·*>`就可以达到效果。总结:问号、星号和加号:
例子1:
var reg=/colou?r/; var result1=reg.exec("color"); var result2=reg.exec("colouur"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/four(th)?/; var result1=reg.exec("fourth"); var result2=reg.exec("fourt"); var result3=reg.exec("fuorth"); document.write(result1+" "+result2+""+ result3);
结果:
例子3:
var reg=/ab+/; var result1=reg.exec("abbbc"); var result2=reg.exec("abdb"); document.write(result1+" "+result2);
结果:
例子4:
var reg=/(abc*)/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
例子5:
var reg=/(ab *f)/; var result1=reg.exec("ab fdd"); var result2=reg.exec("abfdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
2.7Egrep元字符
某些版本的egrep支持使用元字符序列来自定义重现次数的区间“{min,max}”这称为“区间量词”。有人就使用`[a-zA-Z]{1,5}`来匹配美国的股票代码(1到5个字母),问号对应的区间量词是“{0,1}”。JavaScript也支持这种形式。
例子1:
var reg=/abc{3}/; var result1=reg.exec("abcccccdd"); var result2=reg.exec("abccdb"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/abc{1,}/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); document.write(result1+" "+result2);
结果:
例子3:
var reg=/abc{0,1}/; var result1=reg.exec("abcccdd"); var result2=reg.exec("abdb"); var result3=reg.exec("acc") document.write(result1+" "+result2+" "+result3);
结果:
2.8Egrep元字符\1
目前为止我们已经见过括号可以限制多选的范围、将若干字符组合为一个单元。括号还有一个很有用途的功能——记忆文本。括号记下来的文本都是存在\n里面的,这里的\n不是换行符,n是一个具体的数字,如:`([a-z])([0-9])\1\2`中\1记录的就是一个小写字母,\2记录的就是一个一位的数字。
例子:
var reg=/a(b)c\1/; var result1=reg.exec("abcbccccdd"); var result2=reg.exec("accdb"); document.write(result1+" "+result2);
结果:
2.9Egrep元字符(?:)
此为非捕获型小括号,只用于分隔字符,组成单元,不用于记录,由于`()`会记录内部匹配的信息,所以需要占用部分内存,当用户不需要记录此匹配的信息时可以使用`(?:)`,例如`ab(?:c)d(ef)g\1`需要匹配的是字符串…abcdefgef…,而不是…abcdefgc…。
例子:
var reg=/ab(?:c)d(ef)g\1/; var result1=reg.exec("11abcdefgef22"); var result2=reg.exec("abcdefgc"); document.write(result1+" "+result2);
结果:
2.10Egrep元字符\
如果我们需要匹配的某个字符本身就是元字符,我们就需要使用‘\’。比如我们之前提到过的匹配2013.01.04就需要写作`2013\.01\.04`。如果反斜线后紧跟的不是元字符,那么就需要依流派、版本来定了,例如在某些egrep的版本里面使用`\<`和`\>`来单词的左右边界符,在javascript中使用的是`\b`,这两种流派同样都支持`\1`这种形式。
例子:
var reg=/\([a-zA-Z]+\)/; var result1=reg.exec("tt(cat)ty"); var result2=reg.exec("c(tat"); document.write(result1+" "+result2);
结果:
到此基本介绍了常用的简单的元字符,不过在每一种语言里面元字符都不一样,需要读者自己注意。
其实这些都是最基础的正则表达式的使用,只能算是入门,主要是给刚接触正则表达式的朋友了解的。后面再细讲正则表达式的核心——正则引擎。最终用来提高我们的表达式的效率。
《精通正则表达式(元字符)》这篇讲解了正则表达式常用的一些简单的元字符的使用,但是如果不能理解正则表达式匹配的核心,那么你永远不能在这方面有质的突破。
这一篇就重点讲解正则表达式的核心——正则引擎。
3、正则引擎
正则引擎主要可以分为基本不同的两大类:一种是DFA(确定型有穷自动机),另一种是NFA(不确定型有穷自动机)。DFA和NFA都有很长的历史,不过NFA的历史更长一些。使用NFA的工具包括.NET、PHP、Ruby、Perl、Python、GNU Emacs、ed、sec、vi、grep的多数版本,甚至还有某些版本的egrep和awk。而采用DFA的工具主要有egrep、awk、lex和flex。也有一些系统采用了混合引擎,它们会根据任务的不同选择合适的引擎(甚至对同一表达式中的不同部分采用不同的引擎,以求得功能与速度之间的平衡)
NFA和DFA都发展了很多年了,产生了许多不必要的变体,结果,现在的情况比较复杂。POSIX标准的出台,就是为了规范这种现象,POSIX标准清楚地规定了引擎中应该支持的元字符和特性。除开表面细节不谈,DFA已经符合新的标准,但是NFA风格的结果却与此不一,所以NFA需要修改才能符合标准。这样一来,正则引擎可以粗略地分为三类:DFA;传统型NFA;POSIX NFA。
我们来看使用`to(nite|knight|night)`来匹配文本‘…tonight…’的一种办法。正则表达式从我们需要检查的字符串的首位(这里的位置不是指某个字符的位置,而是指两个相邻字符的中间位置)开始,每次检查一部分(由引擎查看表达式的一部分),同时检查“当前文本”(此位置后面的字符)是否匹配表达式的当前部分。如果是,则继续表达式的下一部分,如果不是,那么正则引擎向后移动一个字符的位置,继续匹配,如此继续,直到表达式的所有部分都能匹配,即整个表达式能够匹配成功。在此例子中,由于表达式的第一个元素是`t`,正则引擎将会从需要匹配的字符串的首位开始重复尝试匹配,直到在目标字符中找到‘t’为止。之后就检查紧随其后的字符是否能由`o`匹配,如果能,就检查下面的元素。下面是`nite`或者`knight`或者`night`。引擎会依次尝试这3种可能。尝试`nite`的过程与之前一样:“尝试匹配`n`,然后是`i`,然后是`t`,最后是`e`”。如果这种尝试失败,引擎就会尝试另一种可能,如此继续下去,直到匹配成功或是报告失败。表达式的控制权在不同的元素之间转换,所以我们可以称它为“表达式主导”。
与表达式主导的NFA不同,DFA引擎在扫描字符串时会记录“当前有效”的所有匹配可能。在此例中引擎会对‘…tonight…’进行扫描,当扫描到t时,引擎会在表达式里面的t上坐上一个标记,记录当前位置可以匹配,然后继续扫描o,同样可以匹配,继续扫描到n,发现有两个可以匹配(knight被淘汰),当扫描到g时就只剩下一个可以匹配了,当h和t匹配完成后,引擎发现匹配已经成功,报告成功。我们称这种方式为“文本主导”,因为它扫描的字符串中的每个字符都对引擎进行了控制。
从它们匹配的逻辑上我们不难发现:一般情况下,文本主导的DFA引擎要快一些。正则表达式主导的NFA引擎,因为需要对同样的文本尝试不同的子表达式匹配,可能会浪费时间。在NFA的匹配过程中,目标文本的某个字符可能会被正则表达式反复检测很多遍(每一个字符被检测的次数不确定,所以NFA叫做不确定型有穷自动机)。相反,DFA引擎在匹配过程中目标文本中的每个字符只会最多检查一遍(每个字符被检测的次数相对确定,所以DFA叫做确定型有穷自动机)。由于DFA取得一个结果可能有上百种途径,但是因为DFA能够同时记录它们,选择哪一个表达式并无区别,也就是说你改变写法对于效率是没有影响的。而NFA是表达式主导,改变表达式的编写方式可能会节省很多功夫。
所以后面我们讲解的知识都是涉及的NFA的。
4、回溯
何为回溯?先来看一个例子,我们使用`a(b|c)d`去尝试匹配字符串“cabb”,正则引擎首先处于字符'c'的前面,开始查看正则表达式,发现第一个为a,不能匹配,然后引擎移动到'c'和'a'之间的位置,继续查看表达式,发现a可以匹配,然后查看表达式的后面,发现有两条路,引擎会做好标记,选择其中一条路,加入选择区匹配b,发现字符'a'后面就是'b',可以匹配,然偶再次查看表达式,需要匹配d,发现字符串后面是'b',不符合条件,这条路失败,引擎会自动回到之前做选择的地方,这里就称作一次回溯。那么引擎会尝试匹配a后面的c,发现'a'后面是'b',这条路也走不通,没有其它的路线了,然后引擎又会移动位置,现在到了'a'和'b'之间,引擎回去尝试匹配表达式的a,发现当前位置后面是'b',无法匹配,引擎又开始向后移动位置,直到移动到最后,发现没有一次匹配成功,然后引擎才会报告失败。而如果中间又一次成功完整匹配了,引擎会自动停止(传统型NFA会停止,而POSIX NFA还会继续,把所有可能匹配完,选择其中一个),报告成功。
现在应该知道回溯其实就是引擎在匹配字符串的过程中出现多选的情况,当其中一种选择无法匹配时再次选择另种的过程叫做回溯。其实我们在优化正则表达式的时候就是考虑的尽量减少回溯的次数。
4.1回溯 匹配优先和忽略优先
《精通正则表达式》这本书里面叫做匹配优先和忽略优先,网上有很多人叫做贪婪模式和非贪婪模式,反正都一样,叫法无所谓。
匹配优先量词我们已经学习了,就是?、+、*、{}这四个。匹配优先量词在匹配的时候首先会尝试匹配,如果失败后回溯才会选择忽略。比如`ab?`匹配"abb"会得到"abb"。这里当匹配成功'a'后,引擎有两个选择,一个是尝试匹配后面的b,一个是忽略后面的b,而由于是匹配优先,所以引擎会尝试匹配b,发现可以匹配,得到了"ab",接着引擎又一次遇到了同样的问题,还是会选择先匹配,所以得到了"abb",接着引擎发现后面没有字符了,就上报匹配成功。
忽略优先量词使用的是在?、+、*、{}后面添加?组成的,忽略优先在匹配的时候首先会尝试忽略,如果失败后回溯才会选择尝试。比如`ab??`匹配“abb”会得到‘a’而不是“ab”。当引擎匹配成功a后,由于是忽略优先,引擎首先选择不匹配b,继续查看表达式,发现表达式结束了,那么引擎就直接上报匹配成功。
例子1:
var reg1=/ab?/; var reg2=/ab??/; var result1=reg1.exec("abc"); var result2=reg2.exec("abc"); document.write(result1+" "+result2);
结果:
例子2:
var reg1=/ab+/; var reg2=/ab+?/; var result1=reg1.exec("abbbc"); var result2=reg2.exec("abbbc"); document.write(result1+" "+result2);
结果:
例子3:
var reg1=/ab*/; var reg2=/ab*?/; var result1=reg1.exec("abbbc"); var result2=reg2.exec("abbbc"); document.write(result1+" "+result2);
结果:
例子4:
var reg1=/ab{2,4}/; var reg2=/ab{2,4}?/; var result1=reg1.exec("abbbbbbc"); var result2=reg2.exec("abbbbbbc"); document.write(result1+" "+result2);
结果:
下面我们来看稍微复杂一点的匹配优先的情况,使用`c.*d`去匹配字符串“caaadc”,我们发现当c匹配成功后,`.*`会一直匹配到最后的'c',然后再去匹配表达式里面的d,发现后面没有字符可以匹配,这是就会回溯到`.*`匹配'c'的地方,选择`.*`忽略'c',那么c就留给后面了,但是发现还是不能匹配d,又得回溯到匹配d的位置,`.*`再次选择忽略匹配,发现就可以匹配d了,这是停止匹配,上报匹配成功,所以结果是“caaad”。
再看一个忽略优先的情况,使用`a.*?d`去匹配字符串“caaadc”,我们发现当匹配成功a时,引擎有两条路,会选择忽略匹配,直接匹配d,但是字符串“caaadc”的a后面是a,所以失败,回溯到之前的选择,悬着匹配,获得“aa”,然后又一次遇到同样的问题,引擎选择忽略匹配,发现后面又是a,不能匹配d,再次回溯,选择匹配,得到“aaa”,这一次忽略匹配后发现后匹配成功了d,那么上报成功,得到“aaad”。
希望这几个例子能够大概讲解清楚这两种不同的情况吧!
4.2回溯 固化分组
有些时候我们并不希望引擎去尝试某些回溯,这时候我们可以通过固化分组来解决问题——`(?>...)`。就是一旦括号内的子表达式匹配之后,匹配的内容就会固定下来(固化(atomic)下来无法改变),在接下来的匹配过程中不会变化,除非整个固化分组的括号都被弃用,在外部回溯中重新应用。下面这个简单的例子能够帮助我们理解这种匹配的“固化”性质。
`!.*!`能够匹配"!Hello!",但是如果`.*`在固化分组里面`!(?>.*)!`就不能匹配,在这两种情况下`.*`都会选择尽可能多的字符,都会包含最后的'!',但是固化分组不会“交还”自己已经匹配了的字符,所以出现了不同的结果。
尽管这个例子没有什么实际价值,固化分组还是有很重要的用途。尤其是它能够提高匹配的效率,而且能够对什么能匹配,什么不能匹配进行准确的控制。但是js这门语言不支持。汗!
4.3回溯 占有优先量词
所谓的占有优先量词就是*+、++、?+、{}+这四个,这些量词目前只有java.util.regex和PCRE(以及PHP)提供,但是很可能会流行开来,占有优先量词类似普通的匹配优先量词,不过他们一旦匹配某些内容,就不会“交还”。它们类似固化分组,从某种意义上来说,占有优先量词只是些表面功夫,因为它们可以用固化分组来模拟。`.++`与`(?>.+)`结果一样,只是足够智能的实现方式能对占有优先量词进行更多的优化。
4.4回溯 环视
环视结构不匹配任何字符,只匹配文本中的特定位置,这一点和单词分界符`\b`、`^`、`$`相似。
`(?=)`称作肯定顺序环视,如`x(?=y)`是指匹配x,仅当后面紧跟y时,如果符合匹配,则只有x会被记住,y不会被记住。
`(?!)`称作否定顺序环视,如`x(?!y)`是指匹配x,仅当后面不紧跟y时,如果符合匹配,则只有x会被记住,y不会被记住。
在环视内部的备用状态一旦退出环视范围后立即清除,外部回溯不能回溯到环视内部的备用状态。使用`ab\w+c`和`ab(?=\w+)c`来匹配字符串“abbbbc”,第一个表达式会成功,而第二个表达式会失败。
例子1:
var reg=/ab(?=c)/; var result1=reg.exec("abcd"); var result2=reg.exec("abbc"); document.write(result1+" "+result2);
结果:
例子2:
var reg=/ab(?!c)/; var result1=reg.exec("abdc"); var result2=reg.exec("abcd"); document.write(result1+" "+result2);
结果:
例子3:
var reg1=/ab\w+bc/; var reg2=/ab(?=\w+)c/; var result1=reg1.exec("abbbbbcb"); var result2=reg2.exec("abbbbbbc"); document.write(result1+" "+result2);
结果:
明显自己都觉得环视没讲解好(找时间再修改一下),还有肯定逆序环视和否定逆序环视、占有优先量词以及固化分组这些都是在解决回溯的问题(不过js现在不支持这些,真要将估计得换语言了),回溯算是影响表达式的罪魁祸首吧!这几个内容看啥时候有时间在细讲吧!写着写着才发现想让人看懂不是那么容易的!体谅一下哦!
5、打造高效正则表达式
Perl、Java、.NET、Python和PHP,以及我们熟悉的JS使用的都是表达式主导的NFA引擎,细微的改变就可能对匹配的结果产生重大的影响。DFA中不存在的问题对NFA来说却很重要。因为NFA引擎允许用户进行精确控制,所以我们可以用心打造正则表达式。
5.1先迈好使的腿
对于一般的文本来说,字母和数字比较多,而一些特殊字符很少,一个简单的改动就是调换两个多选分支的顺序,也许会达到不错的效果。如使用`(:|\w)*`和`(\w|:)*`来匹配字符串“ab13_b:bbbb:c34d”,一般说来冒号在文本中出现的次数少于字母数字,此例中第一个表达式效率低于第二个。
例子:
var reg1=/(:|\w)*/; var reg2=/(\w|:)*/; var result1=reg1.exec("ab13_b:bbbb:c34d"); var result2=reg2.exec("ab13_b:bbbb:c34d"); document.write(result1+" "+result2);
5.2无法匹配时
对于无法匹配的文本,可能它在匹配过程中任然会进行许多次工作,我们可以通过某种方式提高报错的速度。如使用`”.*”!`和`”[^”]*”!`去匹配字符串“The name “McDonald’s” is said “makudonarudo” in Japanese”。我们可以看出第一种回溯的次数明显多于第二种。
5.3多选结构代价高
多选结构是回溯的主要原因之一。例如使用`u|v|w|x|y|z`和`[uvwxyz]`去匹配字符串“The name “McDonald’s” is said “makudonarudo” in Japanese”。最终`[uvwxyz]`只需要34次尝试就能够成功,而如果使用`u|v|w|x|y|z`则需要在每个位置进行6次回溯,在得到同样结果前总共有198次回溯。
少用多选结构。
5.4消除无必要的括号
如果某种实现方式认为`(?:.)*`与`.*`是完全等价的,那么请使用后者替换前者,`.*`实际上更快一些。
5.5消除不需要的字符组
只包含单个字符的字符组有点多余,因为它要按照字符组来处理,而这么做完全没有必要。所以例如`[.]`可以写成`\.`。
5.6量词等价转换
有人习惯用`\d\d\d\d`,也有人习惯使用量词`\d{4}`。对于NFA来说效率上时有差别的,但工具不同结果也不同。如果对量词做了优化,则`\d{4}`会更快一些,除非未使用量词的正则表达式能够进行更多的优化。
5.7使用非捕获型括号
如果不需要引用括号内的文本,请使用非捕获型括号`(?:)`。这样不但能够节省捕获的时间,而且会减少回溯使用的状态的数量。由于捕获需要使用内存,所以也减少了内存的占用。
5.8提取必须的元素
由于很多正则引擎存在着局部优化,主要是依靠正则引擎的能力来识别出匹配成功必须的一些文本,所以我们手动的将这些文本“暴露”出来可以提高引擎识别的可能性。 `xx*`替代`x+`能够暴露必须的‘x’。`-{2,4}`可以写作`--{0,2}`。用`th(?:is|at)`代替`(?:this|that)`就能暴露必须的`th`。
5.9忽略优先和匹配优先
通常,使用忽略优先量词还是匹配优先量词取决于正则表达式的具体需求。例如`^.*:`完全不同于`^.*?:`,因为前者匹配到最后的冒号,而后者匹配到第一个冒号。但是如果目标数据中只包含一个冒号,两个表达式就没有区别了。不过并不是任何时候优劣都如此分明,大的原则是:如果目标字符串很长,而你认为冒号会比较接近字符串的开头,就使用忽略优先量词;如果你认为冒号在接近字符串的末尾位置,你就使用匹配优先。如果数据是随机的,又不知道冒号在哪头,就使用匹配优先量词,因为它们的优化一般来说都要比其他量词要好一些。
5.10拆分正则表达式
有时候,应用多个小正则表达式的速度比一个大正则表达式要快得多。比如你希望检查一个长字符串中是否包含月份的名字,依次检查`January`、`February`、`March`之类的速度要比`January|..|….`快得多。
还有很多优化的方法见《精通正则表达式》,我在这里只是列举了部分容易理解的方式。其实只要理解正则引擎室如何匹配的,理解回溯的逻辑,你就可以对自己写的表达式进行相应的优化了!