美文网首页
JS正则表达式之先行零宽断言

JS正则表达式之先行零宽断言

作者: monvhh | 来源:发表于2018-10-19 12:58 被阅读0次

    曾自己借助阿里云和hexo搭了个站点,现已废弃,过往写的博客暂挪到此处。


    title: JS正则表达式之先行零宽断言
    subtitle: 只匹配,不返回,不消费
    date: 2016-12-13 17:23:03
    tags:
    - 技术
    - Javascript
    - 正则
    - TODO


    点击这里可以直接看我的解析,跳过啰嗦的自我探索过程

    JS的正则表达式,关于零宽断言,只有先行断言。

    什么叫先行断言呢?

    JS犀牛书 里是这样介绍的

    字符 含义
    (?=p) 零宽正向先行断言,要求接下来的字符都与p匹配,但不能包括匹配p的那些字符
    (?!p) 零宽负向先行断言,要求接下来的字符不与p匹配

    例子如下:

    var str1 = "bedroom";
    var str2 = "bedding";
    var reBed = /(bed(?=room))///在我们捕获bed这个字符串时,抢先去看接下来的字符串是不是room
    alert(reBed.test(str1));//true
    alert(reBed.test(str2))//false
    
    var str1 = "bedroom";
    var str2 = "bedding";
    var reBed = /(bed(?!room))/  //要来它后面不能是room
    alert(reBed.test(str1))//false
    alert(reBed.test(str2))//true
    

    看起来很简单是不?
    如果你想做一个匹配,字符串 http://www.baidu.com?from=monvhh=endbalabla,我要得到 ?from=monvhh=end中的monvhh,当然这个monvhh可能是任意的字符串

    想通过一次正则匹配就得到?from==end中间的字符串,正则如此强大,一定可以。
    那么我需要去匹配?from==end,并且我还不要它们。只有零宽断言可以做到了!

    var reg = /(?=\?from=)\w+(?=end)/
    

    好简单,好方便是不是!
    然而失败了。。。

    WHY?!

    谷歌良久,得到的结果就是

    • 1)先行断言嘛,只能匹配结尾(网上的例子都是匹配结尾!会去匹配前置的,都是零宽负向先行断言(?!p))。
    • 2)后行断言可以实现我这个思路。
    • 3)零宽断言只占位,不消费。

    只能匹配结尾,为什么?根据它的定义,不至于啊。
    是因为“都”字的原因么?
    但是

    var reg = /Java(?=Script)/;
    var str1 = 'JavaScript';
    var str2 = 'JavaScripter';
    reg.test(str1);//true
    reg.test(str2);//true
    

    str2的结尾除了Script还有er啊,并不是结尾完全匹配Script,那我在匹配这个之后再继续匹配别的,怎么就不行了呢?

    3)解释了这个问题。但我当时一直没想通,跟我这个有什么关系,直到做了大量的测试。。。

    var reg = /(?=\?from=)\w+(?=end)/
    

    这个例子,我一直以为是(?=\?from=)这部分用错了,是先行零宽断言的错。
    确实是它的错,但并不是在解析到它这里出错的。
    而是在它之后的\w+,如果换成\S就能test成功了。(仅仅是test成功了)

    为什么?monvhh符合\w啊。

    原因是:零宽断言只占位,不消费。即
    匹配完(?=\?from=)之后的字符串是?from=monvhh=endbalabla

    注意:此字符串的意思是:待匹配字符串。
    如果你拿(?=\?from=)单独去test字符串http://www.baidu.com?from=monvhh=endbalabla,是true,如果用该字符串去match该正则,只能得到一个空字符串,对,还不是null。
    因为零宽断言就是只匹配,但并不得到这个匹配的结果。这就是我为什么用它的原因嘛,我需要判断字符串里有你,但是我不要你。
    也就是只占位,不消费。它验证过,但是并没有把它消费掉。

    所以,此时继续匹配\w+就会报错,匹配\S+却可以,应为此时的字符串里还有?和=。

    这就是“先行断言”这个词的关键了。
    它就像一个if语句,它只判断,在我当前位置之后,有某个字符串。但并不改变这个字符串,也不对结果有任何影响。
    而其他正则匹配,是一个处理原始字符串的过程(不改变这个字符串,但是改变匹配之后的结果result)。

    这是我理解的,正则匹配就像是一个匹配字符串返回result的结果,这个result在一开始等于这个字符串,在匹配的过程中从左至右削减:匹配正则中第一个位置成功之前的,削掉;然后在目前的result中,就继续匹配正则中下一个位置;若没匹配上,就把目前的result中削减至result中当前正在匹配的位置(即之前匹配该正则前部分成功的那些字符串也抛弃掉),在剩下的result中继续匹配。
    (以上理解仅为只匹配一次的情况,分组和全局模式应当是在此基础上组合)
    如果削减到最后都没有匹配上这个正则,那么test的话就是false了。

    而断言却不同,它匹配上,却不削减。

    它只做了if这一步,其他什么都没做(相较于以上处理result的过程,它对result不作任何处理)。

    在我之前的理解中,只知道它匹配上,却不返回。
    对,它既不返回,也不削减。

    如果先行断言用在正则的最后,那么它不返回。
    如果先行断言,并不是在正则的尾部,那么,虽然它不返回了,但是因为剩下部分正则还在继续匹配,它没有削减result,所以接下来的正则,还得继续匹配该先行断言明明已经匹配上的部分。相当于只卡了个关口(if语句),嘛都没做。当然这就是它的魅力所在,我是说定义。。

    这是正向先行断言。
    负向先行断言的话,就没有这个削减的问题,因为它本来就被期待匹配不上,当然不会削减。但用它的时候也要小心,毕竟跟[^...]的功能不一样,因为[^...]虽然是期待匹配不上,但是它却占了一个位置,在它之后部分的正则,从它这占了一个位置的字符串之后开始匹配。

    <span id="analysis">用我的方式理解正则匹配(包括零宽断言)</span>:

    只是简单的匹配逻辑,不考虑什么分组、匹配位置、修饰符等;

    var reg,str;
    //正则reg,待匹配字符串str;
    //reg是一串针对连续位置的匹配规则,我在强调连续两个字,断言也不能;
    //str是一串连续的字符串;
    
    var reg_pointer = 0,str_pointer = 0;
    //指针reg_pointer;指针str_pointer;
    //分别为正则reg,待匹配字符串str的指针,初始为0,即首位;
    var str_length = str.length,reg_length = reg.length;
    //此处reg.length仅表示正则reg的占位,先把零宽断言也算,方便对比;
    
    var result;
    //result = str.substring(str_pointer,str_length);所以str_pointer可以表达我说的削减的意思;
    //了解正则表达式中的贪婪、惰性、支配性之后,知道我所说的削减,其实就是贪婪性匹配的特质!!
    
    for( str_pointer < str.length && reg_pointer < reg_length ){
        var bool = str[str_pointer]匹配reg[reg_pointer];
        //reg[reg_pointer]仅表示正则中的第reg_pointer的占位,先把零宽断言也算,方便对比;
    
        
        if( reg[reg_pointer] != 先行零宽断言 && bool === false ){
            //重新再来,该轮匹配失败,从接下来的字符串再继续匹配整条正则
            reg_pointer=0;
            str_pointer++;
            //与先行零宽断言的一致,匹配失败都是一样的
        }
    
        if( reg[reg_pointer] != 先行零宽断言 && bool === true ){
            //继续匹配,削减
            reg_pointer++;
            str_pointer++;
        }
    
        if( reg[reg_pointer] == 先行零宽断言 && bool === false ){
            //重新再来,该轮匹配失败,从接下来的字符串再继续匹配整条正则
            reg_pointer=0;
            str_pointer++;
        }
        if( reg[reg_pointer] == 先行零宽断言 && bool === true ){
            //继续匹配,但不削减
            reg_pointer++;
            //所以字符串的指针不变,但是正则进行到下一个占位了。
            //这就是零宽断言不占位的真谛了。
        }
        result = str.substring(str_pointer,str_length);
    }
    
    //匹配失败
    //没有reg_length-1是因为如果最后一次还是匹配成功,还会再做一次reg_pointer++;
    if( reg_pointer < reg_length ){
        result = undefined;
    }
    return result;
    

    结果

    所以它实现不了我的想法。

    最后我通过分组的方式,然后match数组中的第一个分组得到的结果。如下:

    var reg = /\?from=(\w+)=end/;
    var result = 'http://www.baidu.com?from=monvhh=endbalabla'.match(reg);
    var whatiwant = result[1];//monvhh
    

    TODO 如何在正则中达到跳过的效果。

    意即,破坏我在解析正则匹配实现过程中指出的,

    reg是一串针对连续位置的匹配规则,我在强调连续两个字,断言也不能;

    比如,我想跳过几个字符,再继续匹配,或者我想跳过某几个特定的字符,再继续匹配。怎么搞?

    我是说一次性的,不要跟我说分组之后继续。。。或者substring。。。

    如果是跳过前面和后面的,可以用后行断言匹配前面,匹配成功但不返回,先行断言匹配后面的,匹配成功但不返回。成功实现要匹配但返回结果却跳过的需求。

    然后如果要跳过的是中间的呢?

    本文参考
    http://www.cnblogs.com/rubylouvre/archive/2010/03/09/1681222.html
    http://fxck.it/post/50558232873

    相关文章

      网友评论

          本文标题:JS正则表达式之先行零宽断言

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