正则表达式快速入门(二)
【导读】在本文里,我们主要介绍子模式(subpatterns),逆向引用(Back references)和量词(quantifiers)
在上篇文章里,我们介绍了正则表达式的模式修正符与元字符,细心的读者也许会发现,这部分介绍的非常简略,而且很少有实际的例子的讲解。这主要是因为网上现有的正则表达式资料都对这部分都有详细的介绍和众多的例子,如果觉得对前一部分缺乏了解可以参看这些资料。本文希望可以尽可能多涉及一些较高级的正则表达式特性。
在本文里,我们主要介绍子模式(subpatterns),逆向引用(Back references)和量词(quantifiers),其中重点介绍对这些概念的一些扩展应用,例如子模式中的非捕获子模式,量词匹配时的greedy与ungreedy。
子模式(subpatterns)与逆向引用(Back references)
正则表达式可以包含多个字模式,子模式由圆括号定界,可以嵌套。这也是两个元字符“(”和“)”的作用。子模式可以有以下作用:
1. 将多选一的分支局部化。
例如,模式: cat(aract|erpillar|)匹配了 "cat","cataract" 或 "caterpillar" 之一,没有圆括号的话将匹配 "cataract","erpillar" 或空字符串。
2. 将子模式设定为捕获子模式(例如上面这个例子)。当整个模式匹配时,目标字符串中匹配了子模式的部分可以通过逆向引用进行调用。左圆括号从左到右计数(从 1 开始)以取得捕获子模式的数。
注意,子模式是可以嵌套的,例如,如果将字符串 "the red king" 来和模式 /the ((red|white) (king|queen))/进行匹配,捕获的子串为 "red king","red" 以及 "king",并被计为 1,2 和 3 ,可以通过“1”,“2”,“3”来分别引用它们,“1”包含了“2”和“3”,它们的序号是由左括号的顺序决定的。
在一些老的linux/unux工具里,子模式使用的圆括号需要用反斜线转义,向这种(subpattern),但现代的工具已经不需要了,本文中使用的例子都不进行转义。
非捕获子模式(non-capturing subpatterns)
用一对括号同时完成上面提到的子模式的两个功能有时会出现一些问题,例如,由于逆向引用的数目是有限的(通常最大不超过9),而且经常会遇到无需捕获的子模式定义。这时,可以在开始的括号后加上问号和冒号来表示这个子模式无需捕获,就向下面这样(?:red|white) (king|queen))。
如果将“the white queen”作为模式匹配的目标字符串,则捕获的字串有“white queen”和“queen”,分别作为“1”和“2”,white虽然符合子模式“(?:red|white)”,但并不被捕获。
我们前面已经介绍过用括号与问号表示模式修正符的方法,为方便起见,如果需要在非捕获子模式中插入模式修正符,可以把它直接放在问号和冒号之间,例如,下面两个模式是等效的。
/(?i:saturday|sunday)/和/(??i)saturday|sunday)/。
逆向引用(Back references)
前面介绍反斜线作用时,已经提到它的一个作用就是表示逆向引用,当字符类之外的反斜线后跟一个大于0的十进制数时,它很有可能是一个逆向引用。它的含义正如它的名称如言,它表示对它出现之前已经捕获的子模式的引用。这个数字代表了它引用的左括号在模式中出现的次序,我们在介绍子模式时已经看到过逆向引用的一个例子,那里的过“1”,“2”,“3”分别表示所捕获的第一,第二,和第三个小括号定义的子模式的内容。
值得注意的是,当反斜线后的数字小于10时,可以确定此为一个逆向引用,这样,这个逆向引用就可以出现在之前有相应数目的左圆括号被捕获前而不会出现混淆,只有整个模式能提供那么多的捕获子模式,就不会报错。说起来似乎很混乱,还是让我们来看下面这个例子。把介绍子模子时举的例子拿来修改一下,前面讲过字符串 "the red king" 来和模式 /the ((red|white) (king|queen))/匹配,捕获的子串为 "red king","red" 以及 "king",并被计为 1,2 和 3 ,现在把字符串,修改为" king,the red king",模式改为/3,the ((red|white) (king|queen))/,这个模式应该也是可以匹配的。不过,并非所有的正则表达式工具都支持这种用法,安全的做法是在相应序号的左括号之后使用与之相关的逆向引用。
需要注意的另一点是逆向引用的值是在目标字符串中实际捕获的符合子模式的字符串片段而非该子模式本本身。例如/ (sens|respons)e and 1ibility/会匹配“sense and sensibility” 和 “response and responsibility”,但不会是 "sense and responsibility"。当被逆向引用的子模式后面有量词从而被重复匹配了多次,逆向引用的值会以最后一次匹配的值为准。例如/([abc]){3}/匹配字符串“abc”时,逆向引用“1”的值将是最后一次匹配的结果“c”。
命名子模式(named subpattern)
一些工具(例如Python)可以为逆向引用命名,从而定义出命名子模式。在Python中对正则表达式的使用是以函数或方法调用的格式,语法与这里举的例子有较大差别。有兴趣的朋友可以参看一下自己使用的工具来看看是否支持命名子模式。
非捕获子模式(non-capturing subpatterns)
用一对括号同时完成上面提到的子模式的两个功能有时会出现一些问题,例如,由于逆向引用的数目是有限的(通常最大不超过9),而且经常会遇到无需捕获的子模式定义。这时,可以在开始的括号后加上问号和冒号来表示这个子模式无需捕获,就向下面这样(?:red|white) (king|queen))。
如果将“the white queen”作为模式匹配的目标字符串,则捕获的字串有“white queen”和“queen”,分别作为“1”和“2”,white虽然符合子模式“(?:red|white)”,但并不被捕获。
我们前面已经介绍过用括号与问号表示模式修正符的方法,为方便起见,如果需要在非捕获子模式中插入模式修正符,可以把它直接放在问号和冒号之间,例如,下面两个模式是等效的。
/(?i:saturday|sunday)/和/(?:(?i)saturday|sunday)/。
逆向引用(Back references)
前面介绍反斜线作用时,已经提到它的一个作用就是表示逆向引用,当字符类之外的反斜线后跟一个大于0的十进制数时,它很有可能是一个逆向引用。它的含义正如它的名称如言,它表示对它出现之前已经捕获的子模式的引用。这个数字代表了它引用的左括号在模式中出现的次序,我们在介绍子模式时已经看到过逆向引用的一个例子,那里的过“1”,“2”,“3”分别表示所捕获的第一,第二,和第三个小括号定义的子模式的内容。
值得注意的是,当反斜线后的数字小于10时,可以确定此为一个逆向引用,这样,这个逆向引用就可以出现在之前有相应数目的左圆括号被捕获前而不会出现混淆,只有整个模式能提供那么多的捕获子模式,就不会报错。说起来似乎很混乱,还是让我们来看下面这个例子。把介绍子模子时举的例子拿来修改一下,前面讲过字符串 "the red king" 来和模式 /the ((red|white) (king|queen))/匹配,捕获的子串为 "red king","red" 以及 "king",并被计为 1,2 和 3 ,现在把字符串,修改为" king,the red king",模式改为/3,the ((red|white) (king|queen))/,这个模式应该也是可以匹配的。不过,并非所有的正则表达式工具都支持这种用法,安全的做法是在相应序号的左括号之后使用与之相关的逆向引用。
需要注意的另一点是逆向引用的值是在目标字符串中实际捕获的符合子模式的字符串片段而非该子模式本本身。例如/ (sens|respons)e and 1ibility/会匹配“sense and sensibility” 和 “response and responsibility”,但不会是 "sense and responsibility"。当被逆向引用的子模式后面有量词从而被重复匹配了多次,逆向引用的值会以最后一次匹配的值为准。例如/([abc]){3}/匹配字符串“abc”时,逆向引用“1”的值将是最后一次匹配的结果“c”。
命名子模式(named subpattern)
一些工具(例如Python)可以为逆向引用命名,从而定义出命名子模式。在Python中对正则表达式的使用是以函数或方法调用的格式,语法与这里举的例子有较大差别。有兴趣的朋友可以参看一下自己使用的工具来看看是否支持命名子模式。
重复(Repetition)和量词(quantifiers)
在前面介绍逆向引用的部分里我们已经接触到了量词(quantifiers)的概念,例如前面的例子/([abc]){3}/表示三个连续的字符,每个字符都必然是 “abc”这三个字符中的一个。在这个模式里,{3}就属于量词。它表示一个模式需要重复匹配(repetition)的数目。
量词可以放在下面这些项目之后:
?●单个字符(有可能是被转义的单个字符,如xhh)
?●“.”元字符
?● 由方括号表示的字符类
?● 逆向引用
?●由小括号定义的子模式(除非它是个断言,我们会在以后介绍)
最通用的量词使用形式是用花括号括起的两个由逗号分隔的数字,如这样的格式{min,max},例如,/z{2,4}/ 可以匹配 "zz", "zzz", 或者 "zzzz",花括号中的最大值以及前面的逗号可以省略,例如/d{3,}/可以匹配三个以上的数字,数字的数目没有上限,而/d{3}/(注意,没有逗号)则精确的匹配3个数字。当花括号出现在不允许量词的位置或者语法与前面提到的不符时,这里它仅仅代表花括号字符本身而不再具有特殊的含义。例如{,6}不是量词,它仅仅代表这四个字符本身的含义。
为了方便,三个最常用的量词有它们的单字符缩写形式,它们的的含义如下表:
* 相当于 {0,}
+ 相当于 {1,}
? 相当于 {0,1}
这也是以上三个元字符做为量词使用含义。
在使用量词特别是没有上限限制的量词时,应该特别注意不要构成无限循环,例如/(a?)*/,在有的正则表达式工具里。这会形成一个编译错,不过有的工具却允许这种结构,但不能保证各种工具都可以很好的处理这种结构。
重复(Repetition)和量词(quantifiers)
在前面介绍逆向引用的部分里我们已经接触到了量词(quantifiers)的概念,例如前面的例子/([abc]){3}/表示三个连续的字符,每个字符都必然是 “abc”这三个字符中的一个。在这个模式里,{3}就属于量词。它表示一个模式需要重复匹配(repetition)的数目。
量词可以放在下面这些项目之后:
?●单个字符(有可能是被转义的单个字符,如xhh)
?●“.”元字符
?● 由方括号表示的字符类
?● 逆向引用
?●由小括号定义的子模式(除非它是个断言,我们会在以后介绍)
最通用的量词使用形式是用花括号括起的两个由逗号分隔的数字,如这样的格式{min,max},例如,/z{2,4}/ 可以匹配 "zz", "zzz", 或者 "zzzz",花括号中的最大值以及前面的逗号可以省略,例如/d{3,}/可以匹配三个以上的数字,数字的数目没有上限,而/d{3}/(注意,没有逗号)则精确的匹配3个数字。当花括号出现在不允许量词的位置或者语法与前面提到的不符时,这里它仅仅代表花括号字符本身而不再具有特殊的含义。例如{,6}不是量词,它仅仅代表这四个字符本身的含义。
为了方便,三个最常用的量词有它们的单字符缩写形式,它们的的含义如下表:
* 相当于 {0,}
+ 相当于 {1,}
? 相当于 {0,1}
这也是以上三个元字符做为量词使用含义。
在使用量词特别是没有上限限制的量词时,应该特别注意不要构成无限循环,例如/(a?)*/,在有的正则表达式工具里。这会形成一个编译错,不过有的工具却允许这种结构,但不能保证各种工具都可以很好的处理这种结构。
量词匹配的“greedy”与“ungreedy”
在使用带量词的模式时,我们常会发现对同一模式而言,同一个目标字符串可以有多种匹配方式。例如/d{0,1}d/,可以匹配两个或三个十进制数字,如果目标字符串是123,当量词取下限0里,它匹配“12”,当量词取上限1里,它匹配“123”整个字符。这两种匹配结果都是正确的,如果我们取它的子模式/(d{0,1}d)/,则匹配的结果1到底是“12”还是“123”?
实际的运行结果一般会是后者,因为默认情况下,大多数正则表达式工具的匹配是按“greedy”原则匹配的。“greedy”单词的中的含义是“贪吃的, 贪婪的”的意思,它的行为也如此单词的含义,所谓greedy匹配意指在量词限制范围内,只要能保持后续模式的匹配,匹配总是尽可能的重复下去,直到不匹配的情况发生为止。为便于理解,我们看下面这个简单的例子。
/(d{1,5})d/匹配“12345”这个字符串,这个模式表示在1到5个数字后面跟上一个数字,量词范围从1到5,当它的值在1-4时,整个模式都是匹配的,1的值可以是“1”,“12”,“123”,“1234”,而在greedy匹配的情况下,它取匹配时的量词最大值,因此最终匹配的结果是”1234”。
在大多数情况下,这就是我们想要的结果,但情况并不总这样。例如,我们希望用下面这个模式提取出c语言的注释部分(在c语言中,注释语句放在字符串/*和*/之间)。我们使用的正则表达式是/*.**/,但匹配的结果却完全和需要的不同。当正则表达式解析到“/*”这后的“.*”时,因为“.”可以代表任意字符,这也包含了其后需要匹配的“*/”,在量词的作用下,这个匹配将一直进行下去,超过下一个“*”/直到文本的结束,这显然不是我们需要的结果。
为了完成如上例我们想要的那种匹配,正则表达式引入了ungreedy匹配方法,与greedy匹配相反,在满足整个模式匹配的前提下,它总是取最小的量词数目结果。Ungreedy匹配用在量词后面加上问号“?”来表示。例如在匹配C语言的注释时,我们把正则表达式写成如下形式:/*.*?*/,在量词“*”后加上问号就可以达成想要的结果。还有前面那个例子用/(d{1,5})d/匹配“12345”这个字符串,如果改写为ungreedy模式向这样/(d{1,5}?)d/,、1的值将为1。
上面的解释也许有些不准确,量词后的问号的作用实际上是反转当前的正则表达式的greedy与ungreedy行为。你可以通过模式修正符“U”将正则表达式设成ungreedy模式然后在模式中通过量词后的问号将之反转为greedy。
一次性子模式(Once-only subpatterns)
关于量词的另一个有趣的话题是一次性子模式(Once-only subpatterns)。要理解它的概念需要先了解一下含有量词的正则表达式的匹配过程。我们这里举个例子。
现在,让我们用模式/d+foo/来匹配字符串“123456bar”,当然,它的结果是没有匹配。但正则表达式引擎是如何工作的呢?它先分析前面的d+,这代表一个以上的数字,然后检查目标字符串的对应位置的第一个字符“1”,符合模式,然后根据量词重复这个模式对字符串进行匹配直到“123456”始终符合“d+”模式,接着它在目标字符串中遇到字符“b”无法与“d+”匹配,于是查看“d+”的后续模式“foo”,与目标字符串的后续部分“bar”无法匹配,这时,有趣的事情出现了,解释引擎会对前面已经解析过的“d+”模式进行回溯,将量词数目减少一,看剩余部分能否匹配,此时“d+”的值改为“12345”,然后解释引擎看目标字符串剩余的部分“6bar”能否与剩余的模式“foo”相匹配,如果不行,就把量词数再减一,直到达到最小的量词限制,如果仍无法匹配,则表明目标字符串无法匹配,返回无法匹配的结果。
现在,我们就可以来接触一次性子模式了。所谓一次性子模式就是定义在正则表达式解析时不需要上述回溯过程的子模式。它用左圆括号后面的问号和小于号来表示,向这样(?>)。如果将上面提到的例子改为一次性子模式,可以这样书写:
/(?>d)+foo/,这时,当解析器遇到后面不匹配的bar时,会立即返回不匹配的结果,而不会进行前面提到的回溯过程。
需要了解的是,一次性子模式属于非捕获子模式,它的匹配结果不能被逆向引用。
当一个没有设定重复上限的子模式中包含了同样没有设定重复上限的模式时,使用一次性子模式是唯一可以避免让你的程序陷入长时间等待的方法。例如你用“/(D+|<d+>)*[!?]/”这个模式去匹配一长串的a字符,向这样“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa”,在返回最终无匹配的结果前,你会等待很长的一段时间。这个模式表示一串非数字字符或者用尖括号括着的一串数字后跟随着叹号或者问号,把这段字符串分成两个重复的部分会有很多种分法,而无论是子模式本身还是子模式之内的量词的各可能值都要经过逐一测试,这将使最终的运算量达到一个很大的程度。这样,你将在电脑前等待相当长的时间才会看到结果。而如果用一次性子模式来改写刚才的模式,改成这样/ ((?>D+)|<d+>)*[!?]/,你就可以很快得到运算的结果。
正则表达式快速入门(三)
在上文里,我们介绍了正则表达式的子模式,逆向引用和量词,在这篇文章里,我们将重点介绍正则表达式中的断言(Assertions)。
断言(Assertions)
断言(Assertions)是在目标字符串的当前匹配位置进行的一种测试但这种测试并不占用目标字符串,也即不会移动模式在目标字符串中的当前匹配位置。
读起来似乎有点拗口,我们还是举几个简单的例子。
两个最常见的断言是元字符“^”和“$”,它们检查匹配模式是否出现在行首或行尾。
我们来看这个模式/^ddd$/,试着用它来匹配目标字符串“123”。“ddd”表示三个数字字符,匹配了目标字符串的三个字符,而模式中的^和$分别表示这三个字符同时出现在行首和行尾,而它们本身并不与目标字符串中的任何字符相对应。
其它还有一些简单的断言b, B, A, Z, z,它们都以反斜线开头,前面我们已经介绍过反斜线的这个用法。这几个断言的含义如下表。
断言 含义
b 字分界线
B 非字分界线
A 目标的开头(独立于多行模式)
Z 目标的结尾或位于结尾的换行符前(独立于多行模式)
z 目标的结尾(独立于多行模式)
G 目标中的第一个匹配位置
注意这些断言不能出现在字符类中,如果出现了也是其它的含义,例如b在字符类中表示反斜线字符0x08。
前面介绍的这些断言的测试都是一些基于当前位置的测试,断言还支持更多复杂的测试条件。更复杂的断言以子模式方式来表示,它包括前向断言(Lookahead assertions)和后向断言(Lookbehind assertions)。
前向断言(Lookahead assertions)
前向断言从目标字符串的当前位置向前测试断言条件是否成立。前向断言又可分为前向肯定断言和前向否定断言,分别用(?=和{?!表示。例如模式/ w+(?=;)/用来表示一串文本字符后面会有一个分号,但是这个分号并不包括在匹配结果中。一件有趣的事看起来差不多的模式/ (?=;)w+/并不是表示一串前面不是分号的alpha字符串,事实上,不论这串alpha字符的前面是否是一个分号它总是匹配的,要完成这个功能需要我们下面提到的后向断言(Lookbehind assertions)。
后向断言(Lookbehind assertions)
后向断言分别用(?<=和(?<!表示肯定的后向断言与否定后向断言。例如,/ (?<!foo)bar/将寻找一个前面不是foo的bar字符串。一般而言,后向断言使用的子模式需要有确定的长度值,否则会产生一个编译错误。
使用后向断言与一次性子模式搭配使用可以有效的文本的结束部分进行匹配,这里来看一下例子。
考虑一下如果用/abcd$/这样一个简单的模式来匹配一长段以abcd结尾的文本,因为模式的匹配过程是从左向右进行的,正则表达式引擎将在文本中寻找每一个a字符并尝试匹配剩余的模式,如果在这长段文本里仅好有不少的a字符,这样做明显是非常低效的,而如果把以上模式换成为样/^.*abcd$/,这时前面的“^.*”部分将匹配整个文本,然后它发现下一个模式a无法匹配,这时会发生前面提到过的回溯过程,解析器会逐次缩短“^.*”匹配的字符长度从右向左逐次查找剩余的子模式,也要产生多次的尝试过程。现在,我们用一次性子模式与后向断言重写所用的模式,改为/^(?>.*)(?<=abcd)/,这时,一次性子模式一次匹配了整段文本,然后用后向断言检查前面四个字符是否为abcd,只需要一次检测就可以立刻确定整个模式是否匹配。在遇到需要匹配一个很长的文本时,这种方法可以非常显著的提高处理效率。
一个模式中可以包含多个相继的断言,断言也可以嵌套。另外,断言使用的子模式也是非捕获的,不能被逆向引用。
断言的一个重要应用领域就是做为条件子模式的条件。那什么是条件子模式呢?
条件子模式(Conditional subpatterns)
正则表达式允许在模式中根据不同的条件使用不同的匹配子模式。也就是条件子模式(Conditional subpatterns)。它的格式如下?(condition)yes-pattern)或者 (?(condition)yes-pattern|no-pattern)。如果条件满足,采用yes-pattern,否则,采用no-pattern(如果在模式中提供了话)。
条件子模式中的条件有两种,一种是断言结果,另一种是看是否捕获一个前面提供的子模式。
如果在表示条件的圆括号里的内容是一个数字,它表示当此数字代表的子模式被成功匹配时条件为真。看看下面这个例子,/( ( )? [^()]+ (?(1) ) )/x,(注意“x”模式修正符表示忽略字符类外的空白字符和#符号之后的内容)。
这个模式的第一部分“( ( )?”匹配了一个可选的左图括号“(”,第二部分“[^()]+”匹配了一个以上的非圆括号字符,最后一部分“(?(1) ) )”是个条件子模式,表示如果捕获到1也即那个可选的左圆括号,第三部分应该会出现一个右圆括号“)”。
如果在表示条件的圆括号内是一个“R”字符,表示在这个模式或子模式被递归调用时条件为真,在递归调用的顶层,这个条件为假。关于正则表达式中的递归,我们会在后面的部分专题介绍。
如果条件不是一个数字或R字符,则它必需是一个断言。断言可以是肯定或否定的前身或后向断言。让我们看下面这个例子。
/(?(?=[^a-z]*[a-z])
d{2}-[a-z]{3}-d{2} | d{2}-d{2}-d{2} )/x
为了让这个正则表达式更容易阅读,我们特意采用了x模式修正符,这样我们可以在用模式中加入空格对符式进行格式上的分隔并分行表示而不影响模式的解析。
第一行的条件子模式使用了一个肯定的前向断言,表示一串可选的非小写字母后面跟随着一个小写字母。换句话说,它查看目标字符串是否至少包含一个小写字母,如果是,它用“|”前的模式对目标进行匹配,看目标是否为看目标是否为两个数字-三个小写字母-两个数字这种格式,否则,用“|”来匹配目标,看目标字符串是否为由“-”分隔的三段二位十进制数字。
正则表达式中的注释
为了让正则表达式更容易阅读,可以在其中加入注释语句。通常注释由左圆括号和井号——“(#“开始,当遇到下一个右圆括号”)“结束。注释是禁止嵌套的。
如果设定了“x”模式修正符,任何字符类之外(也即[]之外)的井号(#)和下一个新行标记之间的部分也被作为注释看待。
正则表达式快速入门(四)
在上一篇文章里,我们介绍了正则表达式中断言相关的一些概念,在本文里,我们会介绍正则表达式中递归的运用与利用正则表达式修改目标字符串。
正则表达式中的递归
接触过程序的朋友可能都遇到过成对的各种括号吧,这些括号常常相互嵌套,而且嵌套的层次数目无法确定。试想一下如果想提取一段程序里用括号括起的一段代码,这里面很可能包含了层次数目不定的其它括号对,用正则表达式该如何完成?
在Perl 5.6之前这的确有点困难,不过从Perl 5.6之后,引入了递归正则表达式,这个问题得到了解决。通常在正则表达式里用“(?R)”表示一个对自己的引用,下面让我们看看用什么正则表达式来解决刚才提出的问题。
/( ( (?>[^()]+) | (?R) )* )/x
现在让我们来分析这个模式的含义,这里使用了“x”模式修正符,以便可以在模式中加入空格以方便阅读。
模式的开头是匹配第一个左圆括号,然后我们需要捕获的子模式,注意,字模式后面跟了量词“*”,表示此模式可以重复0到多次。最后是一个结束圆括号。现在我们分析子模式( (?>[^()]+) | (?R) )的内容。这是一个分支子模式,表示模式可以有两种情况,第一种是(?>[^()]+),这是一个一次性子模式,代表一个以上的非括号字符,另一种情况是| (?R),也即对正则表达式自己的递归调用——( ( (?>[^()]+) | (?R) )* ),又寻找一个左圆括号,开始查找一对嵌套的圆括号包含的内容。
分析到这里,这个正则表达式的含义已经基本清楚,但你注意到没有,这里为什么要使用一次性子模式(?>[^()]+)来查找非括号字符串?
事实上,由于递归的层次是无限的,这种处理非常必要,特别是遇到不匹配的字符串时,它不会让你陷入长时间的等待。考虑一下下面这个目标字符串,
(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa()
在得出不匹配的最终结果前,如果不使用一次性子模式,解析器将尝试每一种可能的方法来分割目标字符串,这将浪费大量的时间。
用正则表达式修改目标
并非所有的正则表达式工具都允许你修改目标字符串,它们中的一些仅仅使用正则表达式来查找匹配指定模式的字符串,在Linux中,最为广泛使用的支持正则表达式的工具就是grep命令,这是一个专门用来查找的工具,再就是一些文本编辑器工具,它们有的允许使用正则表达式替换,有的则不允许,这需要查看你使用的工具的在线手册。
对于那些允许你使用正则表达式来修改目标字符串的工具中,它们之间的一些不同你必然放在心上:
这些不同首先表现在替换的具体形式上,有的是以对话框的形式分别让你输入需要查找的模式和被替换的内容,有些则使用命令使界面通过定界符来分隔匹配的模式与需要替换的内容,对于一些编程语言工具,它们通常通过函数的不同参数来分别定义需要匹配的模式与替换的内容。
另一个需要注意的不同是这些工具具体修改的对象。大多数基于Linux的命令行工具一般是通过标准输出或者管道来修改缓存的内容而非直接修改磁盘上存储的文件,而文本编辑器工具或编程语言通常会直接修改目标文件。
我们下面用Linux下sed命令的格式来举几个正则表达式的例子:
模式:s/cat/dog/g
输入:wild dogs, bobcats, lions, and other wild cats
输出:wild dogs, bobdogs, lions, and other wild dogs
模式:s/[a-z]+i[a-z]*/nice/g
输入:wild dogs, bobcats, lions, and other wild cats
输出: nice dogs, bobcats, nice, and other nice cats
当我们使用模式进行替换操作时,目标字符串中所有匹配模式的字符串都将被替换。
下面再举一个使用逆向引用进行替换的例子:
模式:s/([A-Z])([0-9]{2,4}) /2:1 /g
输入: A37 B4 C107 D54112 E1103 XXX
输出: 37:A B4 107:C D54112 1103:E XXX
前面已经介绍过默认情况下的匹配一般是greedy的,这常会使实际匹配的部分大于你希望匹配的内容,特别是在进行替换操作时这将更加危险,因为如果你在错误匹配的情况下执行了一次替换操作,实际上你是删除了目标中的有效内容,特别是当这种操作面向文件时造成的危害就更大了。因此,牢记一个不严格的字符类加上一个不严格的量词足以造成不可挽回的后果,执行类似操作前一定要多测试一下不同的目标字符串,尽可能避免这种情况的发生。
在本教程的下一篇文章里,我们会介绍一款可以方便进行正则表达式学习的工具和一些正则表达式编写的思路。
正则表达式快速入门(五)
在上一篇文章里,我们介绍了正则表达式中的递归与替换,现在让我们接触一个学习正则表达式时方便测试使用的工具,并介绍一些正则表达式的编写思路。
一个学习正则表达式的便捷工具
学习正则表达式最好的方法当然是实践,不过支持正则表达式的工具虽多,但如果仅仅用来做练习却不是很方便。
这里我向一家推荐一款专门的正则表达式编写测试工具,PHPEdit公司的Regular Expression Editor工具。这是一个免费软件,主要用来调试PHP使用的Perl兼容正则表达式函数。使用它可以方便的输入目标字符串和正则表达式,并实时看到匹配的结果。可以到它的下载网页去下载这个工具。
程序的界面非常简明,不过使用中发现,它的一些功能使用起来好像有问题,只有preg_match_all和preg_replace功能正常,另外在匹配模式输入框中,不要加模式定界符,程序好像把该输入框中的全部内容都作为模式来解析。
好在做为一个正则表达式的练习工具,它的功能是足够了,下面是它的运行界面。
程序运行界面
文中提到的各个例子都可以在里面进行测试,在最上面的框里输入模式,把目标字符串写进中间的输入框,点击“run the regxwp”按钮可以在下面得到匹配结果。
正则表达式的编写思路
一个避免过多匹配的小技巧
前面我们已经多此谈到书写不合理的正则表达式引起过多匹配的问题,现在的问题是,如何可以尽量避免类似的情况发生。这里有个小小的技巧。
如果你发现你定制模式匹配了过多的结果,一个好的方法是换个思路,与其考虑我的模式下一步需要匹配什么,不如考虑我的模式下一步需要避免匹配什么。我们可以用元字答“^”和字符类很容易的达成这种效果,这常常可以得到更精确的匹配。
为了说明这种思路的好处我们先来举一个与正则表达式无关的例子,考虑这样一个问题,你把一个骰子一次抛出6的概率是六分之一,如果让你掷六次,掷出一个6的概率是多少呢?
可能有人会这么算,一次的概率是1/6,六次是就是6个1/6,加起来等于1。这个结果明显是错的,虽然你掷了六次,但肯定不能保证必然会掷出一个6。从正向的思路解这道题看上去有点难。
如果我们换个思路,解决的方法就明确多了。我们可以把这个题的问法改成这样,如果让你掷6次骰子,每一次都掷不出6的概率是多少?这个问题就好解多了,根据概率的乘法原理,每一次掷出不是6的点数的概率是5/6,而6次中每一次都不是6的概率是5/6的6次方,大概等于33%的样子,然后用1减去这个数字就可以得到我们需要的答案。
你可以把模式中每部分的匹配看作掷一次骰子的过程,每一部分的匹配概率与总匹配概率的情况与我们上面这个例子非常相似。
如何提高正则表达式的解析效率
对同样匹配内容的正则表达式而言,一些模式往往比另外一些模式更有效率。举一个简单的例子,使用字符类“[aeiou]”会比使用分支选择型模式“(a|e|i|o|u).”更有效,一般而言,使用尽可能简单和基本的模式通过会得到更高的效率。
应该尽可能的慎用相互嵌套的无限重复量词,当遇到不匹配的目标字符串时,对字符串的解析有可能花掉很可观的时间。比如下面这个模式片断“(a+)*”,当遇到不匹配的目标字符串“aaaa”时,解析器会对它尝试33种不同的匹配方法,这个数目会随不匹配字符串长度的增加而极快的增长。
一些正则表达式工具对一些特定的模式匹配进行了优化以提高效率,了解你使用的正则表达式工作做过些什么优化并尽可能利用经过优化的模式可以大大提高你的正则表达式执行效率。例如,PHP对形如/a+)*b /这样的模式的解析进行了优化,当模式结尾是一个确定的字符时,解析器会先查找目标的结尾是否符合模式,如果否则立刻返回失败的匹配结果并停止解析。如果将上面的样式改为“(a+)*d”时,因为结尾不再是一个确定的字符,此模式会按正常的过程解析。如果你想看一下两者效果的差异,你在我们前面提到的工具中,把目标字符串设置成25个小写的a字符,然后分别测试两个模式,前者立刻就结束了,而后者需要等待约一秒(笔者使用的是XP1700+处理器)。
除了尽可能利用经过优化的模式,对一些模式进行重新构造也可以大大提高效率。我们在介绍后向断言时介绍过的那个利用后向断言结合一次性子模式匹配结尾的字符的方法就是一个很好的例子。
这里我们准备结束这个教程,由于篇幅和本人水平的限制文中可能会有很多疏漏,还要请求大家谅解。对正则表达式介绍最全面的可能还是Perl相关的一些文档和著作,如果想对正则表达式进行更深入的了解可以参看Jeffrey Friedl 写的“Mastering Regular Expressions”一书,里面有很多例子。不过我觉得在了解正则表达式基本概念后,还是仔细读一下自己经常使用的相关工具里的正则表达式相关部分更实用一些,最后,还是那句话,实践出真知,希望大家在不断实践中更好的掌握正则表达式的使用。
正则表达式中的特殊字符
转自:http://www.phpe.net/articles/151.shtml
字符 描述
将下一个字符标记为一个特殊字符、或一个原义字符、或一个后向引用、或一个八进制转义符。例如,'n' 匹配字符 "n"。'n' 匹配一个换行符。序列 '' 匹配 "" 而 "(" 则匹配 "("。
^
匹配输入字符串的开始位置。如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 'n' 或 'r' 之后的位置。
$
匹配输入字符串的结束位置。如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 'n' 或 'r' 之前的位置。
*
匹配前面的子表达式零次或多次。例如,zo* 能匹配 "z" 以及 "zoo"。 * 等价于{0,}。
+ 匹配前面的子表达式一次或多次。例如,'zo+' 能匹配 "zo" 以及 "zoo",但不能匹配 "z"。+ 等价于 {1,}。
?
匹配前面的子表达式零次或一次。例如,"do(es)?" 可以匹配 "do" 或 "does" 中的"do" 。? 等价于 {0,1}。
{n}
n 是一个非负整数。匹配确定的 n 次。例如,'o{2}' 不能匹配 "Bob" 中的 'o',但是能匹配 "food" 中的两个 o。
{n,}
n 是一个非负整数。至少匹配n 次。例如,'o{2,}' 不能匹配 "Bob" 中的 'o',但能匹配 "foooood" 中的所有 o。'o{1,}' 等价于 'o+'。'o{0,}' 则等价于 'o*'。
{n,m}
m 和 n 均为非负整数,其中n <= m。最少匹配 n 次且最多匹配 m 次。刘, "o{1,3}" 将匹配 "fooooood" 中的前三个 o。'o{0,1}' 等价于 'o?'。请注意在逗号和两个数之间不能有空格。
?
当该字符紧跟在任何一个其他限制符 (*, +, ?, {n}, {n,}, {n,m}) 后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串 "oooo",'o+?' 将匹配单个 "o",而 'o+' 将匹配所有 'o'。
.
匹配除 "n" 之外的任何单个字符。要匹配包括 'n' 在内的任何字符,请使用象 '[.n]' 的模式。
(pattern)
匹配pattern 并获取这一匹配。所获取的匹配可以从产生的 Matches 集合得到,在VBScript 中使用 SubMatches 集合,在JScript 中则使用 {CONTENT}… 属性。要匹配圆括号字符,请使用 '(' 或 ')'。
(?:pattern)
匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 "或" 字符 (|) 来组合一个模式的各个部分是很有用。例如, 'industr(?:y|ies) 就是一个比 'industry|industries' 更简略的表达式。
(?=pattern)
正向预查,在任何匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如, 'Windows (?=95|98|NT|2000)' 能匹配 "Windows 2000" 中的 "Windows" ,但不能匹配 "Windows 3.1" 中的 "Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。
(?!pattern)
负向预查,在任何不匹配Negative lookahead matches the search string at any point where a string not matching pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如'Windows (?!95|98|NT|2000)' 能匹配 "Windows 3.1" 中的 "Windows",但不能匹配 "Windows 2000" 中的 "Windows"。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始
x|y
匹配 x 或 y。例如,'z|food' 能匹配 "z" 或 "food"。'(z|f)ood' 则匹配 "zood" 或 "food"。
[xyz]
字符集合。匹配所包含的任意一个字符。例如, '[abc]' 可以匹配 "plain" 中的 'a'。
[^xyz]
负值字符集合。匹配未包含的任意字符。例如, '[^abc]' 可以匹配 "plain" 中的'p'。
[a-z]
字符范围。匹配指定范围内的任意字符。例如,'[a-z]' 可以匹配 'a' 到 'z' 范围内的任意小写字母字符。
[^a-z]
负值字符范围。匹配任何不在指定范围内的任意字符。例如,'[^a-z]' 可以匹配任何不在 'a' 到 'z' 范围内的任意字符。
匹配一个单词边界,也就是指单词和空格间的位置。例如, 'erb' 可以匹配"never" 中的 'er',但不能匹配 "verb" 中的 'er'。
匹配非单词边界。'erB' 能匹配 "verb" 中的 'er',但不能匹配 "never" 中的 'er'。
cx
匹配由x指明的控制字符。例如, cM 匹配一个 Control-M 或回车符。 x 的值必须为 A-Z 或 a-z 之一。否则,将 c 视为一个原义的 'c' 字符。
d
匹配一个数字字符。等价于 [0-9]。
D
匹配一个非数字字符。等价于 [^0-9]。
f
匹配一个换页符。等价于 x0c 和 cL。
匹配一个换行符。等价于 x0a 和 cJ。
r
匹配一个回车符。等价于 x0d 和 cM。
匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ fnrtv]。
匹配任何非空白字符。等价于 [^ fnrtv]。
t
匹配一个制表符。等价于 x09 和 cI。
v
匹配一个垂直制表符。等价于 x0b 和 cK。
w
匹配包括下划线的任何单词字符。等价于'[A-Za-z0-9_]'。
W
匹配任何非单词字符。等价于 '[^A-Za-z0-9_]'。
xn
匹配 n,其中 n 为十六进制转义值。十六进制转义值必须为确定的两个数字长。例如, 'x41' 匹配 "A"。'x041' 则等价于 'x04' & "1"。正则表达式中可以使用 ASCII 编码。.
num
匹配 num,其中 num 是一个正整数。对所获取的匹配的引用。例如,'(.)' 匹配两个连续的相同字符。
标识一个八进制转义值或一个后向引用。如果 n 之前至少 n 个获取的子表达式,则 n 为后向引用。否则,如果 n 为八进制数字 (0-7),则 n 为一个八进制转义值。
nm
标识一个八进制转义值或一个后向引用。如果 nm 之前至少有is preceded by at least nm 个获取得子表达式,则 nm 为后向引用。如果 nm 之前至少有 n 个获取,则 n 为一个后跟文字 m 的后向引用。如果前面的条件都不满足,若 n 和 m 均为八进制数字 (0-7),则 nm 将匹配八进制转义值 nm。
nml
如果 n 为八进制数字 (0-3),且 m 和 l 均为八进制数字 (0-7),则匹配八进制转义值 nml。
un
匹配 n,其中 n 是一个用四个十六进制数字表示的 Unicode 字符。例如,u00A9 匹配版权符号 (?)。