JavaScript 正则表达式(3)
在JavaScript正则表达式(2)中,我们一起学习了正则表达式的入门进阶功能,比如反向引用,分组匹配,环视,一丢丢引擎的概念,NFA,DFA,以及2个基本原则。下面我们将更加深入了解正则表达式
NFA,“表达式主导”引擎###
2种引擎的根本差异来自于他们各自有着不同的应用算法。NFA
被称为“表达式主导”引擎,而DFA
被成为“文本主导引擎”。什么叫表达式主导呢?请看如下代码:
var reg = /a(cat|dog|deer|lion)/;
var text = '1234adog';
当reg
去匹配text
的时候,正则表达式从a
开始,每次只检查一部分(由引擎查看表达式的一部分),同时检查text
是否匹配表达式的当前部分。如果是,则继续表达式的下一部分,如此继续下去,一直到匹配完所有的正则表达式,那么整个表达式就算是匹配成功。
上述正则的匹配过程是,正则从a
开始,去匹配text
里的1
,失败,然后text
往后移,匹配2
······一直到匹配到a
,满足条件,然后是cat|dog|deer|lion
,它也是会一个一个尝试,先是cat
中的c
,发现不匹配,跳到下一种可能,dog
中的d
,匹配,o
匹配,g
匹配。匹配完成,退出匹配并返回结果。像这种,正则表达式的控制权在不同元素之间来回转换,我们称之为“表达式主导”。因为控制权在正则表达式本身,而不是文本,所以,可以通过不同的写法,让匹配的过程变得更简洁,更快捷。正因为如此,NFA
的这种特性给我们提供了丰富的创造性思维空间。一个好的正则表达式能带来许多收益,而一个不好的,可能会带来严重的后果。
回溯###
回溯是NFC
一个相当重要的概念,它的作用是当引擎在处理各个子表达式或者元素时,遇到需要在2个或者多个之间选择一个的时候,会选择一个,并记录下另外一个。你可以理解为游戏存档,在有多个选择的时候,存一下档,进入其中一个,如果失败之后,选择最近的一次回档,并选择另外一个。
需要作出选择的情形包括量词(决定是否尝试另一个匹配)和多选结构(决定选择哪一个多选分支)
下面我们将讨论有多种选择时,哪种选择优先,匹配失败时,应该回溯到什么状态。
如果需要在进行尝试和跳过尝试之间选择,对于匹配优先量词,引擎会选择尝试匹配,而忽略优先量词会选择跳过匹配。
回溯到哪里?
距离当前最近存储的选项就是当本地失败强制回溯时返回的。使用的原则是后进先出(LIFO)。
总结正则中一些朴实而实用的技巧#####
前略,相信大家已经了解并掌握的正则的基本知识,下面让我们带着这些知识,在实战中来处理更加复杂的问题。正则中的平衡法则:
- 只匹配我们期望的,不匹配我们不期望的文本。
- 易于控制和理解。
- 要保证效率,如果能匹配,必须很快返回结果,如果不能匹配,应该尽快报告匹配失败。
先看下面一个例子:
var text = 'myName=moonburn . \moonburn';//需要匹配这个字符串
var reg = /^\w+=.*\\\w*/gi;
reg.test(text)//false
不要吃惊,是的,匹配失败了,但是讲道理不应该失败的,对吗?让我们仔细分析一波,这个正则的问题在于\,很突兀,对不对。不信?我们来证明一下:
var text = 'myName=moonburn . moonburn';//去掉了\
var reg = /^\w+=.*\w*/gi;
reg.test(text)//true
匹配成功,果然是\的问题。
下面来说一下\的坑....
在JavaScript中,\和其他的语言是不太一样的,举个例子:
var text = '\abc';
console.log(text)//abc
\不见了!再看下一个例子:
'\a' === 'a'//true
解析的时候,就把\自动忽略了?不太完整。再看下面一个例子:
'\n' === 'n'//false
说明并不是忽略,是能转义的时候转义,不能转义的时候忽略!。
所以,回到之前的例子:
var text = 'myName=moonburn . \moonburn';//需要匹配这个字符串
var reg = /^\w+=.*(\\)\w*/gi;
RegExp.$1//''
括号捕获失败,因为根本就没有\,被忽略掉了。所以,应该这样:
var text = 'myName=moonburn . \\moonburn';//需要匹配这个字符串
var reg = /^\w+=.*\\\w*/gi;
reg.test(text)//true
当然,我们发现,在使用.*
去匹配的时候,因为是匹配优先,会匹配全部,然后在通过回溯,退回到\,在进行下一部分的匹配。这样明显不太符合我们说的第三点,没有保证效率。所以我们可以用[^\\\]*
去代替.*
,这样,一旦匹配到了\,就会停下来,进行下一部匹配,没有回溯,效率自然高了。代码如下:
var text = 'myName=moonburn . \\moonburn';//需要匹配这个字符串
var reg = /^\w+=[^\\]*\\\w*/gi;
reg.test(text)//true
最后,得出第一条结论:尽量不要用.*
去匹配,而是用[^···]
去替换,如果选择项比较少,也可以使用(··|··)
的形式,选择项太多使用(··|··)
就得不偿失了。
下面再看一个例子,如何匹配一个IP地址:
一般情况下,IP地址都是由小于3位的数字加上.
号组合而成,就像这样000.001.002.003
。首先我们想到的,应该是这样的形式进行匹配^[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]$
:
var ip = '000.001.002.003';
var reg =/^[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]\.[0-9][0-9][0-9]$/;
reg.test(ip)//true
觉得写法太臃肿?可以把[0-9]
替换成\d
,虽然对于引擎来说本质上没有区别。而且,像这样的写法一定要求3位,显得太死板了,一些ip不一定满足3位,我们也应该匹配通过,所以我们通过如下去匹配:
var ip = '1.21.34.211';
var reg =/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
reg.test(ip)//true
嗯,差不多已经很接近了,不过稍微了解一点IP常识的话,就会知道,最大3位数不会超过255
,而我们这样的匹配法,可以一直匹配到999
,不符合第一条规定,所以我们要缩小范围。应该如何缩小范围呢?首先想到了使用(··|··)
的形式,当然,不会是(0|1|2···|225)
这样子,这样太慢了。下面我们在仔细分析一下这样的结构,首先可以使单个的数字所以(\d|···)
,然后可以是2位数,也是没有限制的,所以变成(\d|\d\d|···)
,只有3位数的时候,会有限制,255,所以当第一位数是0,1的时候,也是没有任何限制的,也就是(\d|\d\d|[01]\d\d|···)
,最后,我们只剩下,3位数,并且第一位数是2的情况,继续分析第二位数,只要比5小,都是没有限制的,所以可以分成(\d|\d\d|[01]\d\d|2[0-4]\d|···)
分析到了这里,最后的情况也明朗了,最终的版本,也就是(\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])
。具体代码如下:
var ip = '1.21.34.211';
var reg =/^((\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])\.){3}(\d|\d\d|[01]\d\d|2[0-4]\d|25[0-5])$/;
reg.test(ip);//true
还能再简单点吗?###
能...
如果使用?
,上述的正则表达式还能更加简略一点,变为([01]?\d?\d|2[0-4]\d|25[0-5])
。代码如下:
var ip = '1.21.34.241';
var reg =/^(([01]?\d?\d|2[0-4]\d|25[0-5])\.){3}([01]?\d?\d|2[0-4]\d|25[0-5])$/;
reg.test(ip);//true
这个例子本身不难,需要掌握的是分析的过程,一层一层分析,最后在修改,优化。
当然,有人会问,为什么不使用环视呢?只需要环视.
之后是否满足条件就ok啊,我也想过,首先JavaScript没有反向环视,只能lookahead,所以第一个.
之前的数字要自己判断,环视的写法也是类似与(?=([01]?\d?\d|2[0-4]\d|25[0-5]))
并没有优化,而且环视的价值在于判断字符不用占位符,这里明显是不需要这样做的。所以不考虑了。
JavaScript 正则表达式(1)
JavaScript 正则表达式(2)
JavaScript 正则表达式(3)
JavaScript 正则表达式(4)