8.4 重复匹配

字符类能够精确匹配字符,但是却不能够描述字符匹配的次数。如果要匹配两个连续字符,可以描述为/\w\w/,如果要匹配3个连续字符,还可以进一步描述为/\w\w w/。但是如果要匹配几十、上百个或者任意个字符,仅通过枚举字符类的形式来匹配就显得比较笨拙。为此,正则表达式还定义了重复类匹配模式,它可以允许用户定义字符重复匹配的次数。其中重复匹配的次数包括确数或约数。

8.4.1 简单重复性匹配

JavaScript正则表达式约定了下面这些元字符,如表8-3所示。它们分别定义了字符重复匹配的确数或约数。在一些专业书籍中也称之为数量词,或者量词。不过它们的语义都是相同的,即设置匹配项可能重复显示的次数。

表8-3 简单重复类

【示例】下面的示例演示了如何进行重复性匹配的操作。

下面结合一个具体的示例进行讲解。设计下面一组字符串:

      var s = "ggle gogle google gooogle goooogle gooooogle goooooogle
      gooooooogle goooooooogle"

如果仅匹配单词ggle和gogle,可以设计:

      var r=/go? gle/g;                         //匹配前一项字符o0次或1次
      var a=s.match(r);                         //返回数组["ggle", "gogle"]

在这里元字符?表示前一项(单个字符或者子表达式)为可选项,可有可无,也就是说前面项没有也能够正确匹配,如果有,则只能够匹配一次。对于这样的匹配结果,还可以按如下正则表达式设计:

      var r=/go{0,1}gle/g;                      //匹配前一项字符o0次或1次
      var a=s.match(r);                         //返回数组["ggle", "gogle"]

大括号中第一个数值设置前一项最小重复次数,0表示它可以不出现,1表示它仅能够显示自身,不能够重复显示。

如果仅匹配第4个单词gooogle,可以设计:

      var r=/go{3}gle/g;                       //匹配前一项字符o重复显示3次
      var a=s.match(r);                        //返回数组["gooogle"]

也可以通过简单的字符类来匹配:

      var r=/gooogle/g;                        //匹配字符gooogle
      var a=s.match(r);                        //返回数组["gooogle"]

如果希望匹配第4个~第6个之间的单词,可以设计:

      var r=/go{3,5}gle/g;                      //匹配第4个~第6个之间的单词
      var a=s.match(r);                         //返回数组["gooogle", "goooogle", "gooooogle"]

{3,5}分别指定了前一项字符o最少重复次数为3,最多重复次数为5,从而实现匹配第4个~第6个之间的3个单词。

如果希望匹配所有单词,可以设计:

      var r=/go*gle/g;                             //匹配所有的单词
      var a = s.match(r);
          //返回数组["ggle", "gogle", "google", "gooogle", "goooogle", "gooooogle", "goooooogle", "goooooooogle","gooooooooogle"]

其中元字符"*"表示前一项字符"o"可以不出现,或者重复出现任意多次。还可以按如下方式进行设计:

      var r=/go{0, }gle/g;                           //匹配所有的单词
      var a = s.match(r);
          //返回数组["ggle", "gogle", "google", "gooogle", "goooogle", "gooooogle", "goooooogle", "goooooooogle","gooooooooogle"]

{0, }指定前一项字符"o"可以出现0次,或者任意多次。当{}中第二个参数值为空,则表示任意值的意思。

如果希望匹配包含字符"o"的所有单词,可以设计:

      var r=/go+gle/g;                             //匹配的单词中字符"o"至少出现1次
      var a = s.match(r);
          //返回数组["gogle", "google", "gooogle", "goooogle", "gooooogle", "goooooogle", "goooooooogle","gooooooooogle"]

其中元字符+表示前一项字符"o"至少出现1次,最多重复次数不限。还可以按如下方式进行设计:

      var r=/go{1, }gle/g;                           //匹配的单词中字符"o"至少出现1次
      var a = s.match(r);
          //返回数组["gogle", "google", "gooogle", "goooogle", "gooooogle", "goooooogle", "goooooooogle","gooooooooogle"]

重复类元字符总是出现在它们所作用的模式之后。使用重复类元字符"*"和"? "时要注意,由于这些字符可能匹配前面字符或子表达式0次,所们它们允许什么都不匹配。例如,正则表达式/a*/实际上与字符串“bcd”匹配,因为该字符串含有0个字符"a"。

8.4.2 贪婪匹配

正则表达式也具有贪婪性,先从一个简单的示例说起:

      var s ="<html><head><title></title></head><body></body></html>";

在上面的字符串中,如果希望匹配每个标签,最简单的想法是按如下方法设计正则表达式:

      var r = /<.*>/;

检测一下匹配结果:

      var a = s.match(r);
      alert(a);
          //返回单元素数组["<html><head><title></title></head> <body>
                          </body></html>"]

看来正则表达式并没有按我们所想,逐个匹配标签,而是非常贪婪地把所有标签都匹配过来。

原来,星号(*)元字符在执行匹配时,先看整个字符串是否匹配。如果不匹配,则去掉该字符串中最后一个字符,并再次尝试。如果还是没有发现匹配,则再次去掉最后一个字符。如此递归计算,直到发现一个匹配或者字符串不剩任何字符。

所以,在上面示例中的正则表达式会先看整个字符串,检测是否符合匹配标准,如果符合标准则执行匹配,就返回整个字符串。到目前为止,所有讨论的重复类元字符都具有贪婪的特性,但是它们的贪婪表现不同。

1.? 、{n}和{n, m}重复类

?、{n}和{n, m}重复类都具有弱贪婪性,这种贪婪性主要表现为贪婪的有限性。

【示例1】下面的示例使用了预定义符?限制贪婪匹配。

      var s ="<html><head><title></title></head><body></body></html>";
      var r = /<.? /;
      var a=s.match(r);                            //返回单元素数组["<h"]

对于点号(.)元字符来说,在选择匹配还是不匹配时,如果条件允许,它总会选择匹配,而不是不匹配。对于{n}重复类来说,它总是在允许的条件下,匹配指定次数,而不是不匹配或者少匹配。至于{n, m}重复类,它总会在允许的条件下,匹配m次,而不是n次。

【示例2】本示例中,正则表达式将匹配字符串"<html><head>",而不是"<html>":

      var r = /<.{4,10}>/
      var a=s.match(r);                            //返回单元素数组["<html><head>"]

【示例3】如果按照下面模式进行设计,正则表达式将匹配字符串"<html>",而不是"<html><head>":

      var r = /<.{4,9}>/
      var a=s.match(r);                            //返回单元素数组["<html>"]

这说明正则表达式的贪婪性是在遵循匹配条件基础上尽可能占有更多字符,而不是随意占用。由于中间字符最大取9个字符,则不符合匹配模式,于是JavaScript解释器会丢掉一个字符,再进行判断。如果不匹配,继续丢掉最后一个字符,只到符合匹配条件为止。

2.*、+和{n, }重复类

*、+和{n, }重复类都具有强贪婪性,这种贪婪性主要表现为贪婪的无限性。当然,这种无限性也是在遵循匹配条件基础上实现尽可能的占有。不过,这3个类表现也略有不同。

星号(*)重复类的匹配底线是最宽容的,匹配欲望最强烈。不管是否存在指定字符或子表达式都会执行匹配操作。

加号(+)重复类的匹配底线是最少存在一个符合指定条件的字符或子表达式,否则不予执行匹配操作。

{n, }重复类的匹配底线是最灵活的,这种灵活性决定了它可以代替“*”或“+”重复类,执行任意底线和条件的无限贪婪的匹配操作。例如,使用{0, }代替*重复类,使用{1, }代替+重复类,或者自定义底线执行各种复杂的匹配操作。

【示例4】正则表达式有贪婪性,它总是与最长的可能长度匹配,而且越是排在左侧的重复类匹配符优先级越高。

      var s ="<html><head><title></title></head><body></body></html>";
      var r = /(<.*>)(<.*>)/
      var a = s.match(r);
      alert(a[1]);                             //左侧匹配"<html><head><title></title></head><body></body>"
      alert(a[2]);                             //右侧子表达式匹配"</html>"

上面示例说明,当多个重复类并列在一起时,左侧重复类具有较大优先权,并尽可能占有更多的符合条件的字符,但是它会留出最小匹配机会给右侧的重复类。

【示例5】下面的示例演示了多个子表达式的匹配贪婪性。

      var r = /(<.*>)(<.*>)(<.*>)/
      var a = s.match(r);
      alert(a[1]);                             //左侧匹配"<html><head><title></title></head><body>"
      alert(a[2]);                             //右侧子表达式匹配"</body>"
      alert(a[3]);                             //右侧子表达式匹配"</html>"

上面示例显示,当多个重复类同时满足条件时,会在保证右侧重复类最低匹配次数基础上,最左侧的重复类将尽可能占有所有字符。

8.4.3 惰性匹配

与贪婪匹配相反,惰性匹配将遵循另一种算法。它将先查看字符串中的第一个字符是否匹配,如果匹配条件不够,就读入下一个字符。如果还是不能够匹配,惰性匹配会继续从字符串中读取字符直到发现匹配或者整个字符串都检查过也没有匹配为止。

惰性匹配只需要在重复类后面添加问号(?)即可。这是Perl 5语言的一个特性,在JavaScript 1.5和其后版本中获得了支持。

【示例1】下面的代码定义了惰性匹配模式。

      var s ="<html><head><title></title></head><body></body></html>";
      var r = /<.*? >/
      var a=s.match(r);                       //返回单元素数组["<html>"]

提示:问号必须放在重复类字符后面。在上面示例中,JavaScript解释器从字符串左侧开始逐个匹配,经过4次迭代,最终找到了匹配条件的子字符串"<html>",于是就停止迭代匹配了。

如果说贪婪匹配体现了最大化匹配原则,那么惰性匹配则体现最小化匹配原则。从语义角度分析,它们属于反义词,操作相反的两种行为。

不管是贪婪匹配,还是惰性匹配,虽然它们的算法不同,但是它们都遵循这样的一条基本原则:必须保证匹配满足正则表达式基本条件。

例如,在上面的示例中,星号(*)可以不重复匹配任何字符,也就是说,对于正则表达式/<.*? >/来说,它可以返回匹配字符串"<>",但是为了能够确保匹配条件成立,在执行中还是匹配了带有4个字符的字符串"html"。惰性取值不能够以违反基本匹配条件而返回,除非没有找到符合条件的字符串,否则必须满足它。

针对6种重复类的惰性匹配详细说明如下。

{n, m}?正则表达式尽量匹配n次,但是为了满足匹配条件也可能最多重复m次。

{n}?正则表达式尽量匹配n次。

{n, }?正则表达式尽量匹配n次,但是为了满足匹配条件也可能匹配任意次。

? ?正则表达式尽量匹配,但是为了满足匹配条件也可能最多匹配1次,相当于{0, 1}?。

+?正则表达式尽量匹配1次,但是为了满足匹配条件也可能匹配任意次,相当于{1, }?。

*?正则表达式尽量不匹配,但是为了满足匹配条件也可能匹配任意次,相当于{0, }?。

【示例2】当然,使用惰性匹配时,返回的结果会与你期望的一致。

      var s ="<html><head><title></title></head><body></body></html>";
      var r = /<.*>? /
      var a=s.match(r);    //返回单元素数组["<html><head><title></title></head><body></body></html>"]

对于正则表达式/<.*>/来说,考虑到该模式将匹配字符“<”,以及0个或多个字符,然后跟随字符“>”。在应用到字符串时,它将匹配整个字符串。现在使用惰性匹配模式/<.*>? /,应该说它仅能够匹配字符串"<html>"。但是在应用到上面的字符串时,该模式也匹配整个字符串,与贪婪匹配的结果是一样的。

原来这是因为正则表达式的模式匹配是在字符串中寻找第一个可能匹配的位置。惰性匹配在字符串的第一个字符处不匹配,所以该匹配将返回,甚至不考虑对后面的字符进行匹配。

8.4.4 支配匹配

支配匹配是另一种类型的匹配模式,它的算法是:只尝试匹配整个字符串。如果整个字符串不能够匹配,则会自动放弃匹配,不再执行迭代以求进一步尝试。支配匹配只需要在重复类后面添加加号(+)即可。

【示例】下面的示例演示了如何定义支配匹配。

      var s ="<html><head><title></title></head><body></body></html>";
      var r = /<.*+>/
      var a = s.match(r);
          //返回单元素数组["<html><head><title></title></head> <body>
            </body></html>"]

注意:目前浏览器对支配匹配的支持不是很完善,IE和Opera不支持该重复类量词,而FF也仅是把它看作贪婪匹配。考虑到兼容的安全性,一般不建议使用支配匹配模式。