JavaScript 正则表达式(3)

作者: moonburn | 来源:发表于2017-06-06 11:13 被阅读46次

    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)

    相关文章

      网友评论

        本文标题:JavaScript 正则表达式(3)

        本文链接:https://www.haomeiwen.com/subject/wcegfxtx.html