Linux正则表达式的子模式(subpatterns),逆向引用(Back references)上篇文章应经详细介绍了,还有一部分内容介绍了量词(quantifiers),量词匹配时的greedy与ungreedy。这里加已详细介绍。
        Linux正则表达式:命名子模式(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?)*/,在有的正则表达式工具里。这会形成一个编译错,不过有的工具却允许这种结构,但不能保证各种工具都可以很好的处理这种结构。
量词匹配的“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。
Linux正则表达式:一次性子模式(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+>)*[!?]/,你就可以很快得到运算的结果。
 
Linux正则表达式断言可能开始理解起来有些困难,我们应该加以重点分析。
断言(Assertions)
断言(Assertions)是在目标字符串的当前匹配位置进行的一种测试但这种测试并不占用目标字符串,也即不会移动模式在目标字符串中的当前匹配位置。
读起来似乎有点拗口,我们还是举几个简单的例子。
两个最常见的断言是元字符“^”和“$”,它们检查匹配模式是否出现在行首或行尾。
我们来看这个模式/^\d\d\d$/,试着用它来匹配目标字符串“123”。“\d\d\d”表示三个数字字符,匹配了目标字符串的三个字符,而模式中的^和$分别表示这三个字符同时出现在行首和行尾,而它们本身并不与目标字符串中的任何字符相对应。
其它还有一些简单的断言\b, \B, \A, \Z, \z,它们都以反斜线开头,前面我们已经介绍过反斜线的这个用法。这几个断言的含义如下表。
断言 含义
\b 字分界线
\B 非字分界线
\A 目标的开头(独立于多行模式)
\Z 目标的结尾或位于结尾的换行符前(独立于多行模式)
\z 目标的结尾(独立于多行模式)
\G 目标中的第一个匹配位置

注意这些断言不能出现在字符类中,如果出现了也是其它的含义,例如\b在字符类中表示反斜线字?
         一个学习正则表达式的便捷工具
学习正则表达式最好的方法当然是实践,不过支持正则表达式的工具虽多,但如果仅仅用来做练习却不是很方便。
        这里我向一家推荐一款专门的正则表达式编写测试工具,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”一书,里面有很多例子。不过我觉得在了解正则表达式基本概念后,还是仔细读一下自己经常使用的相关工具里的正则表达式相关部分更实用一些,最后,还是那句话,实践出真知,希望大家在不断实践中更好的掌握正则表达式的使用