JavaScript 网页编程从入门到精通 (清华社"视频大讲堂"大系·网络开发视频大讲堂)
上QQ阅读APP看书,第一时间看更新

8.5 高级匹配

正则表达式的语法约定是非常复杂的,JavaScript语言仅支持正则表达式的基本语法功能。下面介绍JavaScript所支持的高级语法约定。灵活使用高级匹配模式,能够简化和优化程序开发,使字符串处理变得更加随心所欲。

8.5.1 分组

分组就是通过使用小括号语法分隔符来包含一系列字符、字符类,或者重复类量词,以实现处理各种特殊的字符序列。

【示例1】针对下面字符串,希望分拣出每个标签:

      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>"]

如果使用惰性模式进行匹配,但是每次仅能够抓取一个标签:

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

但是,如果利用分组来进行匹配,就可以获取每个标签的名称:

      var r=/(<.*? >)/g; ;                                    //分组模式
      var a=s.match(r);                                       //全局匹配标签,并存储到数组a中
      for(var i=0; i<a.length; i++){                          //遍历数组a,获取每个标签的名称
          alert(a[i]);
      }

在上面示例中,通过小括号分别存储每个被匹配的标签,最后通过这个数组来获取每个标签的名称。注意,对于正则表达式来说,小括号表示一个独立的逻辑域,其匹配的内容将被独立存储,这样可以以数组形式读取每个子表达式所匹配的信息。

【示例2】设计准备匹配下面长字符串:

      var s = "abcdef-abcdef-abcdef-abcdef-abcdef";

如果不使用分组,估计可能实现的正则表达式如下:

      var r = /abcdef-abcdef-abcdef-abcdef-abcdef/;
      var a = s.match(r);
          //返回单元素数组["abcdef-abcdef-abcdef-abcdef-abcdef"]

尽管可以的,但有点麻烦。如果不知道该字符串中到底出现几次重复时该怎么办呢?这时可以使用分组来重写这个表达式:

      var r=/(abcdef-? )*/;                                     //分组模式进行匹配
      var a = s.match(r);
          //返回数组["abcdef-abcdef-abcdef-abcdef-abcdef", "abcdef"]

在小括号内的匹配模式表示正则表达式的子表达式,而跟随在小括号后的重复类数量词将会作用于子表达式,而不是字符“)”。因此,上面的示例中通过小括号把每个标签内容作为匹配对象,然后通过重复类星号进行迭代匹配,最终能够快速实现匹配的目的。

【示例3】并不限制在分组后使用星号,用户可以使用任意重复类数量词。

      var r=/(abcdef-? ){5}/;                                   //连续匹配5次子表达式
      var r=/(abcdef-? ){1,5}/;                                 //最多匹配5次子表达式
      var r=/(abcdef-? ){0, }/;                                 //匹配任意次子表达式
      var r=/(abcdef-? )? /;                                    //最多匹配一次子表达式
      var r=/(abcdef-? )+/;                                     //最少匹配一次子表达式

【示例4】如果混合使用字符、字符类和量词,甚至可以实现一些相当复杂的分组。

      var s = "<html>< html><html >< html ></html>< /html></ html>< / html >";
      var r = /<([\/\s]*? )html(\s)*? >/g;
      var a = s.match(r);
          //返回数组["<html>", "<html>", "<html>", "<  html>", "</html>", "</html>", "</html>", "</html>"]

在上面的正则表达式中,使用了两个分组,第一个分组中包含了一个字符范围类,其中可以任意匹配空格或斜杠,文本范围类附加了一个量词“*”,它表示空格或斜杠可以出现任意次数,为了避免正则表达式的贪婪性,在重复类量词后面附加了问号(?),令其以惰性模式进行匹配。第二个分组是一个简单的任意空格匹配,当然可以不要分组,但是对于第一个分组来说是必需的,因为数量词作用于子表达式,而不是某个特定的字符。通过上面正则表达式,可以匹配任意形式的<html>标签,这样就不用担心空格或斜杠对匹配语义的影响。

8.5.2 案例:分组应用

在正则表达式中,分组具有极高的应用价值,具体说明如下。

【示例1】把单独的项目进行分组,以便合成子表达式,这样就可以像处理一个独立的字符那样,使用|、+、*或?等元字符来处理它们。

      var s ="javascript is not java";
      var r = /java(script)? /g;
      var a=s.match(r);                            //返回数组["javascript", "java"]

上面的正则表达式可以匹配字符串"javascript",也可以匹配字符串"java",因为在匹配模式中通过分组,使用量词“? ”来修饰该子表达式,这样匹配字符串时,其后既可以有"script",也可以没有。

【示例2】在正则表达式中,通过分组可以在一个完整的模式中定义子模式。当一个正则表达式成功地与目标字符串相匹配时,也可以从目标字符串中抽出与小括号中的子模式相匹配的部分。

      var s ="ab=21, bc=45, cd=43";
      var r = /(\w+)=(\d*)/;
      var a=s.match(r);                            //返回数组["ab=21", "ab", "21"]

在上面的示例中,不仅要匹配出每个变量声明,而且希望知道每个变量的名称及其值。这时如果使用小括号进行分组,把需要独立获取的信息作为子表达式,这样就可以不仅仅抽出声明,而且还可以提取更多的有用的信息。

【示例3】在同一个正则表达式的后部可以引用前面的子表达式。这是通过在字符“\”后加一位或多位数字实现的。数字指定了带括号的子表达式在正则表达式中的位置。如“\1”引用的是第一个带括号的子表达式,“\2”引用的是第二个带小括号的子表达式。

      var s ="<h1>title<h1><p>text<p>";
      var r = /(<\/? \w+>).*\1/g;
      var a=s.match(r);                            //返回数组["<h1>title<h1>", "<p>text<p>"]

在上面的示例中,通过引用前面子表达式匹配的文本,以实现成组匹配字符串。

【示例4】由于子表达式可以嵌套在别的子表达式中,所以它的位置编号是根据左括号的顺序来定的。在下面的正则表达式中,嵌套的子表达式(<\/? \w+>)被指定为“\2”。

      var s ="<h1>title<h1><p>text<p>";
      var r = /((<\/? \w+>).*\2)/g;
      var a=s.match(r);                            //返回数组["<h1>title<h1>", "<p>text<p>"]

【示例5】对正则表达式中前面子表达式的引用,所指的并不是那个子表达式的模式,而是与那个模式相匹配的文本。例如,下面这个字符串就无法实现匹配。

      var s ="<h1>title</h1><p>text</p>";
      var r = /((<\/? \w+>).*\2)/g;
      var a=s.match(r);                        //返回null

【示例6】虽然子表达式(<\/? \w+>)可以匹配“<h1>”,也可以匹配“</h1>”,但是对于“\2”来说,它引用的是前面子表达式匹配的文本,而不是它的匹配模式。如果要引用前面子表达式的匹配模式,则必须使用下面正则表达式:

      var r = /((<\/? \w+>).*((<\/? \w+>))/g;
      var a=s.match(r);                        //返回数组["<h1>title</h1>", "<p>text</p>"]

8.5.3 引用

在正则表达式执行匹配运算时,表达式计算会自动把每个分组(子表达式)匹配的文本临时存储起来以备将来使用。这些存储在分组中的特殊值,被称之为反向引用。反向引用将遵循从左到右的顺序,根据表达式中的左括号字符的顺序进行创建和编号。

【示例1】下面的示例定义了匹配模式包含多个子表达式。

      var s = "abcdefghijklmn";
      var r = /(a(b(c)))/;
      var a=s.match(r);                        //返回数组["abc", "abc", "bc", "c"]

在这个分组匹配模式中,共产生了3个反向引用,第一个是“(a(b(c)))”,第二个是“(b(c))”,第三个是“(c)”。它们引用的匹配文本分别是字符串"abc"、"bc"和"c"。

反向引用在应用开发中主要包含以下几种常规用法。

【示例2】在正则表达式对象的test()方法,以及字符串对象的match()和search()等方法中使用。在这些方法中,反向引用的值可以从RegExp()构造函数中获得。

      var s = "abcdefghijklmn";
      var r = /(\w)(\w)(\w)/;
      r.test(s);
      alert(RegExp.$1);                         //返回第一个子表达式匹配的字符a
      alert(RegExp.$2);                         //返回第二个子表达式匹配的字符b
      alert(RegExp.$3);                         //返回第三个子表达式匹配的字符c

通过上面示例可以看到,正则表达式执行匹配测试后,所有子表达式匹配的文本都被分组存储在RegExp()构造函数的属性内,通过前缀符号$与正则表达式中子表达式的编号来引用这些临时属性。其中属性$1标识符指向第一个值引用,属性$2标识符指向第二个值引用,依此类推。

【示例3】可以直接在定义分组的表达式中包含反向引用。这可以通过使用特殊转义序列(如\l、\2等)来实现(详细内容可以参阅8.5.2节内容)。

      var s = "abcbcacba";
      var r = /(\w)(\w)(\w)\2\3\1\3\2\1/;
      var b=r.test(s);                          //验证正则表达式是否匹配该字符串
      alert(b);                                 //返回true

在上面示例的正则表达式中,“\1”表示对第一个反向引用(\w)所匹配的字符a引用,“\2”表示对第二个反向引用(\w)所匹配的字符b引用,“\3”表示对第二个反向引用(\w)所匹配的字符c引用。

【示例4】可以在字符串对象的replace()方法中使用。通过使用特殊字符序列$1、$2、$3等来实现。例如,在下面的示例中将颠倒相邻字母和数字的位置:

      var s = "aa11bb22c3d4e5f6";
      var r = /(\w+? )(\d+)/g;
      var b = s.replace(r, "$2$1");
      alert(b);                                    //返回字符串"11aa22bb3c 4d5e6f"

在上面例子中,正则表达式包括两个分组,第一个分组匹配任意连续的字母,第二个分组匹配任意连续的数字。在replace()方法的第二个参数中,$1表示对正则表达式中第一个子表达式匹配文本的引用,而$2表示对正则表达式中第二个子表达式匹配文本的引用,通过颠倒$1和$2标识符的位置,即可实现字符串的颠倒替换原字符串。

8.5.4 案例:非引用型分组

正则表达式分组会占用一定的系统资源,在较长的正则表达式中,存储反向引用会降低匹配速度。但是很多时候使用分组仅是为了设置操作单元,而不是为了引用,这时建议选用一种非引用型分组,它不会创建反向引用。

【示例】通过使用非引用型分组,既可以拥有与匹配字符串序列同样的能力,又不用存储匹配文本的开销。创建非引用型分组的方法是,在左括号的后面分别加上一个问号和冒号。

      var s1 = "abc";
      var s2 = "123";
      var r=/(? :\w*? )|(? :\d*? )/;                       //非引用型分组
      var a=r.test(s1);                                    //返回true
      var b=r.test(s2);                                    //返回true

此时如果调用RegExp对象的$1标识符来引用分组匹配的文本信息,结果会返回一个空字串,因为该分组是非引用型的。

      alert(RegExp.$1);                                   //返回""

正因为如此,字符串对象的replace()方法就不能通过RegExp.$1变量来使用任何反向引用,或在正则表达式中使用它。

非引用型分组对于必须使用子表达式,但是又不希望存储无用的匹配信息而浪费系统资源,或者希望提高匹配速度,是非常重要的方法。

8.5.5 选择

当正则表达式执行匹配操作时,经常会遇到选择性问题。

【示例1】下面的示例演示了选择匹配模式的设计方法。

如果希望匹配字符串"abc",同时还要匹配字符串"123"。由于这两个字符串完全没有相同的字符,按照前面介绍的方法,需要编写两个不同的正则表达式,并分别对两个字符串进行匹配:

      var s1 = "abc";
      var s2 = "123";
      var r1 = /\w+/;
      var r2 = /\d+/;
      var b1 = r1.test(s1);
      var b2 = r2.test(s2);
      alert(b1);                                 //返回true
      alert(b2);                                 //返回true

也可以使用字符范围类,实现相同的匹配选择操作。

      var s1 = "abc";
      var s2 = "123";
      var r=/[(\w+)(\d+)]/;                     //字符范围类正则表达式
      var b1=r.test(s1);                        //返回true
      var b2=r.test(s2);                        //返回true

在正则表达式的字符范围类中,把两个匹配模式分别包含在小括号语法分隔符,组成两个独立的子表达式,并通过范围类进行匹配操作。不过,这种方式的执行效率比较低,正则表达式会为不同的子表达式开辟内存空间,这样就占用了不必要的系统资源。

【示例2】JavaScript正则表达式提供对选择操作符的支持,选择操作符使用管道符(|)表示,它放在两个独立的单元之间,表示对多个单元执行选择匹配操作。

      var r=/a|b/;                             //选择单个字符,匹配a或b
      var r=/\w*|\d+/;                         //选择重复类,匹配任意字符,或者匹配任意数字
      var r=/a|\d+/;                           //选择字符或重复类,匹配字符a,或者匹配任意数字
      var r=/\w*? |\d{1, }? /;                 //选择惰性匹配
      var r=/a|\d{1, }? /;                     //选择字符或惰性模式
      var r=/[abc]|[def]/;                     //选择字符范围类
      var r=/a|[cde]/;                         //选择字符或者字符范围类
      var r=/(abc)|(cdf)/;                     //选择子表达式

针对上面的示例,如果使用选择操作符,则可以这样设计:

      var s1 = "abc";
      var s2 = "123";
      var r=/\w+|\d+/;                          //选择重复字符类
      var b1=r.test(s1);                        //返回true
      var b2=r.test(s2);                        //返回true

【示例3】可以设计多重选择模式,这时只需要在多个连续的单元之间加入选择操作符即可,执行连续选择匹配操作。

      var s1 = "abc";
      var s2 = "efg";
      var s3 = "123";
      var s4 = "456";
      var r=/(abc)|(efg)|(123)|(456)/;          //多重选择匹配
      var b1=r.test(s1);                        //返回true
      var b2=r.test(s2);                        //返回true
      var b3=r.test(s3);                        //返回true
      var b4=r.test(s4);                        //返回true

【示例4】选择操作符在实际开发中经常会被使用。例如,针对提交的表单信息进行敏感字符过滤,这时可以设计一个匹配所有敏感字符的正则表达式,然后使用字符串对象的repalce()方法把所有敏感字符替换为字符编码,并转换为网页显示的编码格式。

      var s="a'b? c&";                            //待过滤的表单提交信息
      var r=/\'|\"|\? |\&/gi;                     //过滤敏感字符的正则表达式
      function f(){                               //替换函数,把敏感字符替换为对应的网页显示的编码格式
          return "&#" + arguments[0].charCodeAt(0) + "; ";
      }
      var a=s.replace(r, f);                      //执行过滤替换
      document.write(a);                          //在网页中显示正常的字符信息
      alert(a);                                   //返回字符串"a&#39; b&#63; c&#38; "

最后,把JavaScript正则表达式的分组、引用和选择元字符进行小结,以方便读者比较参考,如表8-4所示。

表8-4 JavaScript正则表达式的分组、引用和选择元字符

8.5.6 声明

声明包括正前向声明和反前向声明两种模式。

正前向声明:这里的前向是指定匹配模式后面的字符,声明表示条件的意思,也就是指定在接下来的字符必须被匹配,但并不真正进行匹配。正前向声明使用“(? =匹配条件)”来表示。

【示例1】下面的代码定义了一个正前向声明的匹配模式。

      var s = "a:123 b=345";
      var r=/\w*(? ==)/;                        //使用正前向声明,指定执行匹配必须满足的条件
      var a=s.match(r);                         //返回数组["b"]

在上面示例中,通过使用(? ==)锚定条件,指定只有在\w*所能够匹配的字符后面跟随一个等号字符,才能够执行\w*匹配。所以,最后匹配的是字符b,而不是字符a。

反前向声明,与正前向声明匹配相反,指定接下来的字符都不必匹配。反前向声明使用“(? !匹配条件)”来表示。

【示例2】下面的代码定义了一个反前向声明的匹配模式。

      var s = "a:123 b=345";
      var r=/\w*(? ! =)/;                        //使用反前向声明,指定执行匹配不必满足的条件
      var a=s.match(r);                          //返回数组["a"]

在上面示例中,通过使用(? ! =)锚定条件,指定只有在“\w*”所能够匹配的字符后面不跟随一个等号字符,才能够执行\w*匹配。所以,最后匹配的是字符a,而不是字符b。

提示:声明虽然包含在小括号内,但这不是分组。事实上,分组不会考虑声明的存在。对于正则表达式来说,它们仅是一个修饰性限定条件,而不是可引用的单元。

此时如果调用RegExp对象的$1标识符来引用分组匹配的文本信息,结果会返回一个空字串,因为这里的小括号不是分组标志。

      alert(RegExp.$1);                         //返回""

目前,JavaScript仅支持前向声明,而不支持后向声明。即能够根据前面的字符是否匹配某个指定的表达式,来决定执行后面的匹配操作。

8.5.7 边界

JavaScript正则表达式支持定位功能,所谓定位就是能够确定字符在字符串中的具体方位(如字符串的头部或尾部,或者单词的边界),详细说明如表8-5所示。

表8-5 JavaScript正则表达式支持的边界元字符

【示例1】下面的代码分别演示了边界元字符的定界功能。

利用这4个边界元字符,可以很轻松地定位所要匹配的字符在文本行中的位置。

如果匹配文本行中最后一个单词,可以使用如下方法:

      var s = "how are you";
      var r = /(? :\w+)$/;
      var a=s.match(r);                                //返回数组["you"]

如果匹配文本行中开头一个单词,可以使用如下方法:

      var s = "how are you";
      var r = /^(? :\w+)/;
      var a=s.match(r);                                //返回数组["how"]

如果匹配文本行中每一个单词,可以使用如下方法:

      var s = "how are you";
      var r = /(? :\w+)/g;
      var a=s.match(r);                                //返回数组["how", "are", "you"]

如果使用下面的方法,也会匹配每个单词,但是它也会把单词之间的空格作为一个单词进行匹配:

      var r = /\b(? :.+? )\b/g;
      var a=s.match(r);                                //返回数组["how", "", "are", , "",  "you"]

也可以这样设计:

      var r=/\b(\S+? )\b/g;                            //匹配非边界区域的字符
      var a=s.match(r);                                //返回数组["how", "are", "you"]

【示例2】在匹配单个字符串时,可以在匹配模式中添加行边界:

      var s = "javascript";
      var r=/^javascript$/;                              //在匹配模式中添加行边界
      var b=r.test(s);                                   //返回true

【示例3】单词总是以空格进行分隔。从语法角度上分析,这种单词间必须添加空格的格式是正确的,但是在正则表达式中就会引发匹配错误。例如检索单词“javascript”自身,可以使用如下模式:

      var r = /\sjavascript\s/;

也就是说,在单词前后都要有空格,但是这样做存在两个潜在问题:

如果单词出现在字符串的开头或结尾,该匹配模式就会失效。除非在字符串的开头或结尾有一个空格。

当该模式匹配字符串时,在返回的匹配字符串前后都会带有空格。

因此,应该把空格和单词边界区分开来,正确选择单词的边界匹配符\b来代替空格匹配符\s进行匹配。

8.5.8 锚记

正则表达式的锚记与HTML锚记概念有点类似,都具有定位功能。不过正则表达式的锚记可以将匹配模式定位在检索字符串中的一个特定位置上。最常用的锚记是^,它能够使模式定位在字符串的开头,而锚记$能够使模式定位在字符串的末尾。JavaScript支持的锚记如表8-6所示。

表8-6 JavaScript支持的锚记

8.5.9 标志

正则表达式的标志说明了高级模式匹配的规则。与其他的正则表达式语法不同,标志是在模式分隔符“/”之外进行说明的,它不会出现在模式分隔符(即两个斜杠)之间,而是位于第二个斜杠之后。JavaScript正则表达式支持的标志包括3种,说明如表8-7所示。

表8-7 JavaScript正则表达式支持的标志

表8-7中的3个标志可以任意混合使用。

【示例1】如果把标志i与g混合使用,就可以执行一个全局的、大小写不敏感的匹配。如下:

      var r = /javascript/gi;

上面的模式可以匹配指定字符串中所有的“javascript”、“JAVASCRIPT”、“Javascript”、“javaScript”或“JavaScript”等不同大小写字符串。

如果字符串中包含很多行,字符匹配就比较复杂。在JavaScript 1.5版本以前,由于不支持m标志,为了实现多行匹配,需要使用split()方法将字符串分割成行与行的数组,然后再对每一行单独进行正则表达式测试。

【示例2】在本示例中,只能够匹配最后一个字母c,但是前面两个字母没有被匹配,因为只有字母c在字符串的结尾处:

      var s = "a\nb\nc";
      var r=/\w+$/g;                                  //默认为单行匹配
      var a=s.match(r);                               //返回数组["c"]

由于字符串s中有两个换行符,如果匹配每一行结尾的字母,则需要使用多行匹配模式:

      var s = "a\nb\nc";
      var r=/\w+$/gm;                                 //启动多行匹配
      var a=s.match(r);                               //返回数组["a", "b", "c"]

提示:多行模式会改变边界的匹配行为,这时$会匹配换行符之前的位置,而不再是字符串的结尾。