JavaScript精粹读书笔记(7)

来源:互联网 发布:网络搜索引擎的文章 编辑:程序博客网 时间:2024/05/29 07:30

第7章  正则表达式

JavaScript的许多特性都借鉴自其他语言。语法借鉴自Java,函数借鉴自Scheme,原型继承借鉴自Self。而JavaScript的正则表达式特性则借鉴自Perl

正则表达式起源于对形式语言的数学研究。

JavaScript中,正则表达式的语法是对Perl版的改进和发展,它非常接近源自贝尔实验室的原始形式。正则表达式的书写规则出奇的复杂,因为它们把某些位置上的字符串解析为运算符,而把仅在位置上稍微不同的相同字符串又当作字符串本身。比不易书写更糟糕的是,这使得正则表达式不仅难以阅读,而且修改时充满危险。要想正确地阅读它们,就必须对正则表达式的整个复杂性有相当彻底的理解。为了缓解这个问题,我对其规则进行了些许简化。这里所展示的正则表达式可能稍微有些不够简洁,但也会让正确地使用它们变得稍微容易一点。

有点让人感到费解的是,JavaScript的正则表达式难以分段阅读,因为它们不支持注释和空白。正则表达式的所有部分都被紧密排列在一起,使得它们几乎无法被辨认。当它们在安全应用中进行扫描和验证时,这点就须要特别地留意。如果你不能阅读和理解一个正则表达式,你如何能确保它对所有的输入都能正确地工作呢?然而,尽管有这些明显的缺点,但正则表达式还是被广泛地使用着。

7.1   一个例子

这里有一个例子。它是一个用来匹配URL的正则表达式。在JavaScript程序中,正则表达式必须写在一行中。正则表达式中的空白是至关重要的:

var parse_url=/^(?:([A-Za-z]+):)?(//{0,3})([0-9./-A-Za-z]+)(?::(/d+))?(?://([^?#]*))?(?:/?([^#]*))?(?:#(.*))?$/;

   var url="http://www.ora.com:80/goodparts?q#fragment";

让我们来调用parse_urlexec方法。如果能成功地匹配我们传给它的字符串,它将会返回一个数组,该数组包含了从这个url中提取出来的片段:

   var result=parse_url.exec(url);

   var names=['url','scheme','slash','host','port','path','query','hash'];

   var blanks='      ';

   var i;

   for(i=0;i<names.length;i+=1){

      document.writeln('<br />'+names[i]+':'+blanks.substring(names[i].length),result[i]);

   }

这段代码产生的结果如下:

url: http://www.ora.com:80/goodparts?q#fragment
scheme:http
slash: //
host: www.ora.com
port: 80
path: goodparts
query: q
hash: fragment

让我们来看看parse_url的每部分的因子是如何工作的:

^

^字符表示这个字符串的开始。它是一个标记,用来防止exec跳过不像URL(non-URL-like)的前缀:

(?:([A-Za-z]+):)?

这个因子匹配一个协议名,但仅当它之后跟随一个:(冒号)的时候才匹配。(?...)表示一个非捕获型分组(noncapturing group)。后缀?表示这个分组是可选的。

它表示重复01次。()表示一个捕获型分组(capturing group)。一个捕获型分组将复制它所匹配的文本,并将其放入result数组中。每个捕获型分组都将被指定一个编号。第一个捕获型分组的编号是1,所以该分组所匹配的文本拷贝将出现在result[1]中。[…]表示一个字符类。这个字符类A-Za-z包含26个大写字母和26个小写字母。连字符(-)表示范围从AZ。后缀+表示这个字符类将被匹配1次或多次。这个组后面跟着字符:,它将按字面进行匹配:

//{0,3}

下一个因子是捕获型分组2.//表示一个应该被匹配的/(斜杠)。它用/(反斜杠)来进行转义,这样它就不会被错误地解释为这个正则表达式的结束符。后缀{0,3}表示/将被匹配0次,或者13次之间:

[0-9./-A-Za-z]+

下一个因子是捕获型分组3.它将匹配一个主机名,由1个或多个数字、字母或.-组成。-将被转义为/-以防止与表示范围的连字符相混淆:

(?::(/d+))?

下一个可选的因子将匹配端口号,它是由一个前置:加上1个或多个数字而组成的序列。/d表示一个数字字符。1个或多个数字组成的数字串将被捕获型分组4捕获:

(?://([^?#]*))?

我们有另一个可选的分组。该分组以一个/开始。之后的字符类[^?#]以一个^开始,它表示这个类包含除?#之外的所有字符。*表示这个字符类将被匹配0次或多次。

注意我在此是不严谨的。这个类匹配除?#之外的所有字符,其中包括了行结束符、控制字符,以及其他大量不应在此被匹配的字符。大多数情况下,它会照我们希望的去做,但某些不好的文本可能会有漏进来的风险。不严谨的正则表达式是一个常见的安全漏洞发源地。写不严谨的正则表达式比写严谨的正则表达式要容易很多:

(?:/?([^#]*))?

接下来,我们还有一个以一个?开始的可选分组。它包含捕获型分组6,这个分组包含0个或多个非#字符:

(?:#(.*))?

我们的最后一个可选分组是以#开始的。.将匹配除行结束符以外的所有字符:

$

$表示这个字符串的结束。它让我们确信在这个URL的尾部没有其他更多内容。

以上便是正则表达式parse_url的所有因子。

parse_url的正则表达式还可以编写得更复杂,但我不建议这样做。短小并简单的正则表达式是最好的。惟有如此,我们才有信心让它们正确地工作并在需要时能成功地修改它们。

JavaScript的语言处理程序之间兼容性非常高。这门语言中最没有移植性的部分就是对正则表达式的实现。结构复杂或令人费解的正则表达式很有可能导致移植性问题。在执行某些匹配时,嵌套的正则表达式也能导致极恶劣的性能问题。因此简单是最好的策略。

让我们来看另一个例子:一个匹配数字的正则表达式。数字可能由一个整数部分加上一个可选的减号、一个可选的小数部分和一个可选的指数部分组成:

var parse_number=/^-?/d+(?:/./d*)?(?:e[+/-]?/d+)?$/i;

   var test=function(num){

      document.writeln("测试"+num+":")

         document.writeln(parse_number.test(num));

         document.writeln("<br />")

   };

   test('1');                                //true

   test('number');                            //false

   test('98.6');                          //true

   test('123.21.86.100');             //false

   test('123.45E-67');                //true

   test('123.45d-67');                //false

parse_number成功地从这些字符串中检验出哪些符合我们的规范,而哪些不符合,但对那些不符合的字符串,它并没有告诉我们这些数字测试失败的缘由和位置。

们来分解parse_number:

/^    $/i

我们又用^$来框定这个正则表达式。它将导致文本中的所有字符都要针对这个正则表达式进行匹配。如果我们省略了这些标识,那么只要一个字符串包含一个数字,这个正则表达式就会告诉我们。但有了这些标识,只有当一个字符串的内容仅为一个数字时,它才会告诉我们。如果我们仅包含^,它将匹配以一个数字开头的字符串。如果我们仅包含$,则匹配以一个数字结尾的字符串。

i标识规定当匹配字母时忽略大小写。在我们的模式中唯一可能出现的字母是e。我们希望e也能匹配E。我们可以将e因子写成[Ee](?:E|e),但不必这么麻烦,因为我们使用了标识符i

-?

减号后面的?后缀表示这个减号是可选的:

/d+

/d的含义和[0-9]一样。它匹配一个数字。后缀+规定它可以匹配1个或多个数字:

(?:/./d*)?

(?: . . .)?表示一个可选的非捕获型分组。通常用非捕获型分组来替代少量不优美的捕获型分组是很好的方法,因为捕获会有性能上的损失。这个分组将匹配后面跟随0个或多个数字的小数点:

(?:e[+/-]?/d+)?

这是另外一个可选的非捕获型分组。它将匹配一个e(或E)、一个可选的正负号及一个或多个数字。

7.2   结构

有两个方法来创建一个RegExp对象。优先的方法是直接声明正则表达式。

正则表达式被包围在一对斜杠中。这有点令人难以捉摸,因为斜杠也被用作除法运算符和注释符。

3个标志能在RegExp中设置。它们分别由字母gim来标示(见表7-1)。这些标志被直接添加在正则表达式的末尾:

//构造一个匹配JavaScript字符串的正则表达式对象

var my_regexp=/"(?://.|[^///"])"/g;

7-1       正则表达式标志

标志

含义

g

全局的(匹配多次;准确含义随方法而变)

i

大小写不敏感(忽略字符大小写)

m

多行(^$能匹配行结束符)

默然说话:原书中以上标志均写为大写字母,这在JavaScript中是错误的,三个标志均只能写成小写字母。

创建一个正则表达式的另一个方法是使用RegExp构造器。这个构造器接收一个字符串,并把它编译为一个RegExp对象。创建这个字符串时请多加小心,因为反斜杠在正则表达式和在字符串中有一些不同的含义。通常需要双写反斜杠及对引号进行转义:

//使用RegExp构造一个正则表达式

var my_regexp=new RegExp("/"(?://.|[^///////"])*/"",'g');

第二个参数是一个指定标志的字符串。RegExp构造器适用于正则表达式必须在运行时动态生成的情况。

7-2RegExp对象的属性

属性

用法

global

如果标志g被使用,值为true

ignoreCase

如果标志i被使用,值true

lastIndex

下一次exec匹配开始听索引。初始值为0

multiline

如果标志m被使用,值为true

source

正则表达式源代码文本

7.3   元素

7.3.1   选择

一个选择包含1个或多个正则表达式序列。这些序列被|(竖线)字符分隔。如果这些序列中的任何一项符合匹配条件,那么这个选择就被匹配。它尝试按顺序依次匹配这些序列项。所以:

"into".match(/in|int/)

将在into中匹配in。而不是int,因为in已被匹配成功了。

7.3.2   序列

一个序列包含1个或多个正则表达式因子。每个因子能选择是否跟随一个量词,这个量词决定着这个因子被允许出现的次数。如果没有指定这个量词,那么该因子将被匹配一次。

7.3.3   因子

一个因子可以是一个字符、一个由圆括号包围的组、一个字符类,或者是一个转义序列。除了控制字符和特殊字符以外,所有的字符都将被按照字面的意思进行处理:

/  / [ ] ( ) { } ? + * | ^ $

如果你希望上面列出的字符都按字面的本意去匹配,那么必须要用一个/来进行转义。如果你记不清哪些要转义,那你可以给任何特殊字符都添加一个/前缀来使其字面化。/前缀不能使字母或数字字面化(默然说话:即所以的字母和数字不应该进行转义)。

一个未被转义的.(点)将匹配除行结束符以外的任何字符。

lastIndex属性值为0时,一个未转义的^将匹配该文本的开始。当指定了m标识时,它也能匹配行结束符。

一个未转义的$将匹配该文本的结束。当指定了m标志时,它也能匹配行结束符。

7.3.4   转义

反斜杠字符在正则表达式因子中与其在字符串中一样均表示转义,但是在正则表达式因子中,它稍有一点不同。

像在字符串中一样,/f是换页符,/n是换行符,/r是回车符,/t是制表符,并且/u允许使用四位的十六进制常量指定一个Unicode字符。但要注意:/b不是退格符。

/d等同于[0-9]。它匹配一个数字。而/D则表示非数字:[^0-9]

/s等同于[/f/n/r/t/u00DB/u0020/u00A0/u2028/u2029]。这是Unicode空白符的一个不完全子集。/S则表示相反的一个子集(即非空白符) [^/f/n/r/t/u00DB/u0020/u00A0/u2028/u2029]

/w等同于[0-9A-Z_a-z](可用于变量命名的部分子集)。/W则表示与其相反:[^0-9A-Z_a-z]。这似乎应该是表示出现在变量中的字符。可实际上它对任何语言来说都是无用的。如果你要匹配一个变量的命名规则,你得自己指定规则。

/b被指定为一个字边界标志,这将方便于对文本的字边界进行匹配。不幸的是,它使用/w去寻找字边界,所以它对多语言应该来说是完全无用的。

/1是指向分组1所捕获到的文本的一个引用,所以它能被再次匹配。例如,你能用下面的正则表达式来搜索文本中的所有单词:

var doubled_words=/[A-Za-z/u00C0-/u1fff/u2800-/ufffd'/-]+/s+/1/gi;

doubled_words将寻找出现重复的单词(包含1个或多个字母的字符串),该单词的后面跟着1个或多个空白,然后再跟着与它相同的单词。

/2是指向分组2的引用,/3是指向分组3的引用,依此类推。

7.3.5   分组

分组共有4种。

捕获型

一个捕获型分组是一个被包围在圆括号中的正则表达式选择。任何匹配这个分组的字符将被捕获。每个捕获型分组都被指定了一个数字。在正则表达式中第一个捕获(的是分组1.第二个(是分组2

非捕获型

非捕获型分组有一个(?:前缀。非捕获型分组仅做简单的匹配;并不会捕获所匹配文本。这会有微弱的性能优势。非捕获型分组不会干扰捕获型分组的编号。

向前正向匹配

向前正向匹配组有一个(?=前缀。它类似于非捕获型分组,但在这个组匹配后,文本将倒回到它开始的地方,实际上并不匹配任何东西。这不是一个好的特性。

向前负向匹配

向前负向匹配分组有一个(?!前缀。它类似于向前正向匹配分组,但只有匹配失败时它才进行匹配。这不是一个好的特性。

7.3.6  

正则表达式类是一种指定一组字符的便利方式。例如,如果想匹配一个元音字母,我们可以写作(?:a|e|i|o|u),但它可以被更方便地写成一个类[aeiou]

类提供另外两个方便性。第一个是能够指定字符范围。所以,一组由32ASCII的特殊字符组成的集合:

! ” # $ % & ‘ ( ) * + , - . / :

; < = > ? @ [ / ] ^ _ ` { | } ~

可以被写为:

(?:!|"|#|/$|%|&|'|/(|/)|/*|/+|,|-|/.|//|:|;|<|=|>|@|/[|//|]|/^|_|` |/{|/||/}|~)

稍微更好看一些的写法是:

[!-//:-@/[-`{-~]

它包括从!/、从:到@、从[`和从{~r的字符。但它看起来依旧相当难以阅读(默然说话:我觉得写成[!”#$% &‘()*+,/-./ /:;<=>?@/[///]/^_`{|}~]也是可以的)

另一个方便之处是类的求反。如果[后的第一个字符是^,那么 个类将排除这些特殊字符。所以

[^!-//:-@/[-`{-~]

将匹配任何一个非ASCII特殊字符的字符。

7.3.7   类转义

字符类内部的转义规则和正则表达式因子的相比稍有不同。[/b]是退格符。下面是在字符类中需要被转义的特殊字符:

- / [ / ] ^

7.3.8   量词

因子可以用一个正则表达式量词后缀来决定这个因子应该被匹配的次数。包围在一对花括号中的一个数字表示这个因子应该被匹配的次数。所以,/www/匹配的/w{3}/一样。{3,6}将匹配3456次。{3,}将匹配3次或更多次。

?等同于{0,1}*等同于{0,}+则等同于{1,}

如果只有一个量词,则趋向于进行贪婪性匹配,即匹配尽可能多的重复直至达到上限。如果这个量词还有一个额外的后缀?,那么则趋向于进行懒惰性匹配,即试图匹配尽可能少的必要重复。一般情况下最好坚持使用贪婪性匹配。

原创粉丝点击