精通正则表达式

13年1月初在公司开了一次正则表达式的讲座,在这里希望把那一个月努力专研的一些东西分享一下,先分享一本大家熟悉的书《精通正则表达式》,的确是好书,不过要不是工作原因,我才不会一个月内看了两遍呢!实在是枯燥的很啊!不过最近发现的确对自己很有帮助,还可以时常在工作中要到,有时候也能帮群里解决一些问题。我这里算是把那本书精简了一下的版本吧!没书上的详细,不过结合了网上一些资料加上自己的试验。

  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 开发者工具包中得到支持和嵌入应用!目前主流的开发语言(PHPC#JavaC++VBJavascriptRuby以及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|..|….`快得多。

   还有很多优化的方法见《精通正则表达式》,我在这里只是列举了部分容易理解的方式。其实只要理解正则引擎室如何匹配的,理解回溯的逻辑,你就可以对自己写的表达式进行相应的优化了!

 

精通正则表达式第三版 搜集于网络 前言..........I 第1章:正则表达式入门.... 1 解决实际问题... 2 作为编程语言的正则表达式... 4 以文件名做类比... 4 以语言做类比... 5 正则表达式的知识框架... 6 对于有部分经验的读者... 6 检索文本文件:Egrep. 6 Egrep元字符... 8 行的起始和结束... 8 字符组... 9 用点号匹配任意字符... 11 多选结构... 13 忽略大小写... 14 单词分界符... 15 小结... 16 可选项元素... 17 其他量词:重复出现... 18 括号及反向引用... 20 神奇的转义... 22 基础知识拓展... 23 语言的差异... 23 正则表达式的目标... 23 更多的例子... 23 正则表达式术语汇总... 27 改进现状... 30 总结... 32 一家之言... 33 第2章:入门示例拓展.... 35 关于这些例子... 36 Perl简短入门... 37 使用正则表达式匹配文本... 38 向更实用的程序前进... 40 成功匹配的副作用... 40 错综复杂的正则表达式... 43 暂停片刻... 49 使用正则表达式修改文本... 50 例子:公函生成程序... 50 举例:修整股票价格... 51 自动的编辑操作... 53 处理邮件的小工具... 53 用环视功能为数值添加逗号... 59 Text-to-HTML转换... 67 到单词重复问题... 77 第3章:正则表达式的特性和流派概览.... 83 在正则的世界中漫步... 85 正则表达式的起源... 85 最初印象... 91 正则表达式的注意事项和处理方式... 93 集成式处理... 94 程序式处理和面向对象式处理... 95 查找和替换... 98 其他语言中的查找和替换... 100 注意事项和处理方式:小结... 101 字符串,字符编码和匹配模式... 101 作为正则表达式的字符串... 101 字符编码... 105 正则模式和匹配模式... 110 常用的元字符和特性... 113 字符表示法... 115 字符组及相关结构... 118 锚点及其他“零长度断言” 129 注释和模式量词... 135 分组,捕获,条件判断和控制... 137 高级话题引导... 142 第4章:表达式的匹配原理.... 143 发动引擎... 143 两类引擎... 144 新的标准... 144 正则引擎的分类... 145 几句题外话... 146 测试引擎的类型... 146 匹配的基础... 147 关于范例... 147 规则1:优先选择最左端的匹配结果... 148 引擎的构造... 149 规则2:标准量词是匹配优先的... 151 表达式主导与文本主导... 153 NFA引擎:表达式主导... 153 DFA引擎:文本主导... 155 第一想法:比较NFA与DFA.. 156 溯... 157 真实世界中的例子:面包屑... 158 溯的两个要点... 159 备用状态... 159 溯与匹配优先... 162 关于匹配优先和溯的更多内容... 163 匹配优先的问题... 164 多字符“引文” 165 使用忽略优先量词... 166 匹配优先和忽略优先都期望获得匹配... 167 匹配优先、忽略优先和溯的要旨... 168 占有优先量词和固化分组... 169 占有优先量词,?+、*+、++和{m,n}+. 172 环视的溯... 173 多选结构也是匹配优先的吗... 174 发掘有序多选结构的价值... 175 NFA、DFA和POSIX.. 177 最左最长规则... 177 POSIX和最左最长规则... 178 速度和效率... 179 小结:NFA与DFA的比较... 180 总结... 183 第5章:正则表达式实用技巧.... 185 正则表达式的平衡法则... 186 若干简单的例子... 186 匹配连续行(续前)... 186 匹配IP地址... 187 处理文件名... 190 匹配对称的括号... 193 防备不期望的匹配... 194 匹配分隔符之内的文本... 196 了解数据,做出假设... 198 去除文本首尾的空白字符... 199 HTML相关范例... 200 匹配HTML Tag. 200 匹配HTML Link. 201 检查HTTP URL. 203 验证主机名... 203 在真实世界中提取URL. 206 扩展的例子... 208 保持数据的协调性... 209 解析CSV文件... 213 第6章:打造高效正则表达式.... 221 典型示例... 222 稍加修改——先迈最好使的腿... 223 效率vs准确性... 223 继续前进——限制匹配优先的作用范围... 225 实测... 226 全面考查溯... 228 POSIX NFA需要更多处理... 229 无法匹配时必须进行的工作... 230 看清楚一点... 231 多选结构的代价可能很高... 231 性能测试... 232 理解测量对象... 234 PHP测试... 234 Java测试... 235 VB.NET测试... 237 Ruby测试... 238 Python测试... 238 Tcl测试... 239 常见优化措施... 240 有得必有失... 240 优化各有不同... 241 正则表达式的应用原理... 241 应用之前的优化措施... 242 通过传动装置进行优化... 246 优化正则表达式本身... 247 提高表达式速度的诀窍... 252 常识性优化... 254 将文字文本独立出来... 255 将锚点独立出来... 256 忽略优先还是匹配优先?具体情况具体分析... 256 拆分正则表达式... 257 模拟开头字符识别... 258 使用固化分组和占有优先量词... 259 主导引擎的匹配... 260 消除循环... 261 方法1:依据经验构建正则表达式... 262 真正的“消除循环”解法... 264 方法2:自顶向下的视角... 266 方法3:匹配主机名... 267 观察... 268 使用固化分组和占有优先量词... 268 简单的消除循环的例子... 270 消除C语言注释匹配的循环... 272 流畅运转的表达式... 277 引导匹配的工具... 277 引导良好的正则表达式速度很快... 279 完工... 281 总结:开动你的大脑... 281 第7章:Perl 283 作为语言组件的正则表达式... 285 Perl的长处... 286 Perl的短处... 286 Perl的正则流派... 286 正则运算符和正则文字... 288 正则文字的解析方式... 292 正则修饰符... 292 正则表达式相关的Perl教义... 293 表达式应用场合... 294 动态作用域及正则匹配效应... 295 匹配修改的特殊变量... 299 qr/…/运算符与regex对象... 303 构建和使用regex对象... 303 探究regex对象... 305 用regex对象提高效率... 306 Match运算符... 306 Match的正则运算元... 307 指定目标运算元... 308 Match运算符的不同用途... 309 迭代匹配:Scalar Context,不使用/g. 312 Match运算符与环境的关系... 316 Substitution运算符... 318 运算元replacement 319 /e修饰符... 319 应用场合与返值... 321 Split运算符... 321 Split基础知识... 322 返空元素... 324 Split中的特殊Regex运算元... 325 Split中带捕获型括号的match运算元... 326 巧用Perl的专有特性... 326 用动态正则表达式结构匹配嵌套结构... 328 使用内嵌代码结构... 331 在内嵌代码结构中使用local函数... 335 关于内嵌代码和my变量的忠告... 338 使用内嵌代码匹配嵌套结构... 340 正则文字重载... 341 正则文字重载的问题... 344 模拟命名捕获... 344 效率... 347 办法不只一种... 348 表达式编译、/o修饰符、qr/···/和效率... 348 理解“原文”副本... 355 Study函数... 359 性能测试... 360 正则表达式调试信息... 361 结语... 363 第8章:Java. 365 Java的正则流派... 366 Java对\p{…}和\P{…}的支持... 369 Unicode行终结符... 370 使用java.util.regex. 371 The Pattern.compile() Factory. 372 Pattern的matcher方法... 373 Matcher对象... 373 应用正则表达式... 375 查询匹配结果... 376 简单查找-替换... 378 高级查找-替换... 380 原地查找-替换... 382 Matcher的检索范围... 384 方法链... 389 构建扫描程序... 389 Matcher的其他方法... 392 Pattern的其他方法... 394 Pattern的split方法,单个参数... 395 Pattern的split方法,两个参数... 396 拓展示例... 397 为Image Tag添加宽度和高度属性... 397 对于每个Matcher,使用多个Pattern校验HTML. 399 解析CSV文档... 401 Java版本差异... 401 1.4.2和1.5.0之间的差异... 402 1.5.0和1.6之间的差异... 403 第9章:.NET. 405 .NET的正则流派... 406 对于流派的补充... 409 使用.NET正则表达式... 413 正则表达式快速入门... 413 包概览... 415 核心对象概览... 416 核心对象详解... 418 创建Regex对象... 419 使用Regex对象... 421 使用Match对象... 427 使用Group对象... 430 静态“便捷”函数... 431 正则表达式缓存... 432 支持函数... 432 .NET高级话题... 434 正则表达式装配件... 434 匹配嵌套结构... 436 Capture对象... 437 第10章:PHP.. 439 PHP的正则流派... 441 Preg函数接口... 443 “Pattern”参数... 444 Preg函数罗列... 449 “缺失”的preg函数... 471 对未知的Pattern参数进行语法检查... 474 对未知正则表达式进行语法检查... 475 递归的正则表达式... 475 匹配嵌套括号内的文本... 475 不能溯到递归调用之内... 477 匹配一组嵌套的括号... 478 PHP效率... 478 模式修饰符S:“研究”. 478 扩展示例... 480 用PHP解析CSV.. 480 检查tagged data的嵌套正确性... 481 索引...... 485
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值