文章目录

  • 一、环视基础
  • 二、顺序环视匹配过程
    • (一)顺序肯定环视匹配过程
    • (二)顺序否定环视匹配过程
  • 三、逆序环视匹配过程
    • (一)逆序环视基础
    • (二)逆序肯定环视匹配过程
      • 1. 逆序表达式的长度固定,如何匹配
      • 2. 逆序表达式的长度不固定,如何匹配
        • (1)匹配开始位置不确定,匹配结束位置确定
        • (2)匹配开始位置确定,匹配结束位置不确定
    • (三)逆序否定环视匹配过程
      • 1. 逆序表达式的长度固定,如何匹配
        • (1)匹配起始位置不确定,匹配结束位置确定
        • (2)匹配起始位置确定,匹配结束位置不确定
      • 2. 逆序表达式的长度不固定,如何匹配

一、环视基础

环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。环视匹配的最终结果就是一个位置。

环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。

环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件。

表达式 说明
(?<=Expression) 逆序肯定环视,表示所在位置左侧能够匹配 Expression
(?<!Expression) 逆序否定环视,表示所在位置左侧不能匹配 Expression
(?=Expression) 顺序肯定环视,表示所在位置右侧能够匹配 Expression
(?!Expression) 顺序否定环视,表示所在位置右侧不能匹配 Expression

环视是正则中的一个难点,对于环视的理解,可以从应用和原理两个角度理解,如果想理解得更清晰、深入一些,还是从原理的角度理解好一些,正则匹配基本原理参考《NFA引擎匹配原理》。

上面提到环视相当于对“所在位置”附加了一个条件,环视的难点在于找到这个“位置”,这一点解决了,环视也就没什么秘密可言了。

对于顺序肯定环视(?=Expression)来说,当子表达式Expression匹配成功时,(?=Expression)匹配成功,并报告(?=Expression)匹配当前位置成功。

对于顺序否定环视(?!Expression)来说,当子表达式Expression匹配成功时,(?!Expression)匹配失败;当子表达式Expression匹配失败时,(?!Expression)匹配成功,并报告(?!Expression)匹配当前位置成功。

二、顺序环视匹配过程

(一)顺序肯定环视匹配过程

顺序肯定环视的例子已在《NFA引擎匹配原理》中讲解过了,请移步参考。

(二)顺序否定环视匹配过程

源字符串:aa<p>one</p>bb<div>two</div>cc

正则表达式:<(?!/?p\b)[^>]+>

这个正则的意义就是匹配除 <p> 或 </p>之外的其余标签。/? 表示匹配正斜杠 0 次或 1 次;\b 表示匹配字符边界。

首先由表达式的字符 < 取得控制权,从源字符串位置 0 开始匹配,由于 < 匹配 a 失败,在位置 0 处整个表达式匹配失败,第一次迭代匹配失败,正则引擎向前传动,由位置 1 处开始尝试第二次迭代匹配。

重复以上过程,直到位置 2,表达式的字符 < 匹配源字符串的字符 < 成功,控制权交给 (?!/?p\b);(?!/?p\b) 子表达式取得控制权后,进行内部子表达式的匹配。首先由 /? 取得控制权,尝试匹配 p 失败,进行回溯,不匹配,控制权交给 p;由 p 来尝试匹配 p,匹配成功,控制权交给 \b;由 \b 来尝试匹配位置 4,匹配成功。此时子表达式匹配完成,/?p\b 匹配成功,那么环视表达式 (?!/?p\b) 就匹配失败。在位置 2 处整个表达式匹配失败,新一轮迭代匹配失败,正则引擎向前传动,由位置 3 处开始尝试下一轮迭代匹配。

在位置 8 处也会遇到一轮 /?p\b 匹配 /p 成功,而导致环视表达式 (?!/?p\b) 匹配失败,从而导致整个表达式匹配失败的过程。

重复以上过程,直到位置 14,< 匹配 < 成功,控制权交给 (?!/?p\b);/? 尝试匹配 d 失败,进行回溯,不匹配,控制权交给 p;由 p 来尝试匹配 d,匹配失败,已经没有备选状态可供回溯,匹配失败。此时子表达式匹配完成,/?p\b 匹配失败,那么环视表达式 (?!/?p\b) 就匹配成功。匹配的结果是位置15,然后控制权交给 [^>]+;由 [^>]+ 从位置 15 进行尝试匹配,可以成功匹配到 div,控制权交给 >;由 >来匹配 >,匹配成功。此时正则表达式匹配完成,报告匹配成功。

匹配结果为 <div>,开始位置为 14,结束位置为 19。其中 < 匹配 <,(?!/?p\b) 匹配位置 15,[^>]+ 匹配字符串 div,> 匹配 >。

三、逆序环视匹配过程

(一)逆序环视基础

对于逆序肯定环视 (?<=Expression) 来说,当子表达式 Expression 匹配成功时,(?<=Expression) 匹配成功,并报告 (?<=Expression) 匹配当前位置成功。

对于逆序否定环视 (?<!Expression) 来说,当子表达式 Expression 匹配成功时,(?<!Expression) 匹配失败;当子表达式 Expression 匹配失败时,(?<!Expression)匹配成功,并报告(?<!Expression)匹配当前位置成功;

顺序环视相当于在当前位置右侧附加一个条件,所以它的匹配尝试是从当前位置开始的,然后向右尝试匹配,直到某一位置使得匹配成功或失败为止。而逆序环视的特殊处在于,它相当于在当前位置左侧附加一个条件,所以它不是在当前位置开始尝试匹配的,而是从当前位置左侧某一位置开始,匹配到当前位置为止,报告匹配成功或失败。

顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。

所以顺序环视相对是简单的,而逆序环视相对是复杂的。这也就是为什么大多数语言和工具都提供了对顺序环视的支持,而只有少数语言提供了对逆序环视支持的原因。

JavaScript 中只支持顺序环视,不支持逆序环视。

Java 中虽然顺序环视和逆序环视都支持,但是逆序环视只支持长度确定的表达式,逆序环视中量词只支持“?”,不支持其它长度不定的量词。长度确定时,引擎可以向左查找固定长度的位置作为起点开始尝试匹配,而如果长度不确定时,就要从当前位置向左逐个位置开始尝试匹配,不成功则回溯,再向左侧位置进行尝试匹配,然后重复以上过程,直到匹配成功,或是尝试到位置0处以后,报告匹配失败,处理的复杂度是显而易见的。

目前只有.NET中支持不确定长度的逆序环视。

(二)逆序肯定环视匹配过程

1. 逆序表达式的长度固定,如何匹配

源字符串:<div>a test</div>

正则表达式:(?<=<div>)[^<]+(?=</div>)

这个正则的意义就是匹配 <div> 和 </div> 标签之间的内容,而不包括 <div> 和 </div>标签本身。


首先由逆序肯定环视表达式 (?<=<div>) 取得控制权,从位置 0 开始匹配,由于逆序肯定环视表达式中的子表达式 <div> 长度是 5,所以正则引擎会从当前位置向左侧查找 5 个字符来匹配,可是当前位置是 0,左侧没有任何内容,所以子表达式 <div> 必然匹配失败,从而逆序肯定环视表达式 (?<=<div>) 匹配失败,则整个正则表达式在字符串的位置 0 处匹配失败,即正则表达式的第 1 轮迭代匹配失败。

正则引擎向前传动,由位置 1 处开始尝试第 2 次迭代匹配,由于位置左侧的字符数量不足,所以也是匹配失败。直到传动到位置 5,正则引擎向左查找到 5 个字符,(?<=<div>) 取得控制权后,由位置 0 开始向右逐个字符匹配,结果子表达式 <div> 匹配字符串 <div> 成功,从而整个逆序肯定环视表达式 (?<=<div>) 匹配成功,匹配成功的位置是 5,控制权交给下一个子表达式 [^<]+;[^<]+ 从位置 5 向右开始逐个字符匹配,匹配字符串 a test 成功,控制权交给顺序肯定环视表达式 (?=</div>);由 </div> 匹配 </div> 成功,从而顺序肯定环视表达式 (?=</div>) 匹配成功,位置 11 匹配成功。

此时正则表达式匹配完成,报告匹配成功。匹配到的字符串为 a test,匹配开始位置为 5,匹配结束位置为 11。其中 (?<=<div>) 匹配位置 5,[^<]+ 匹配字符串 a test,(?=</div>) 匹配位置 11

疑问:
逆序环视表达式的匹配是如何确定匹配开始位置的?如果是按照表达式的长度向左查找对应数量的字符数,从而确定匹配起点,那么当前位置左侧的字符数量不足时,匹配起点位置就无法确定,也就不会逐个字符去匹配了,因为长度都不同,匹配结果肯定是失败的。猜测,大概率是按逆序环视子表达式的长度(或者最小长度)来确定起点,如果字符数不足,就没有必要逐个字符去匹配,因为这是多余的,匹配结果肯定是失败的。

2. 逆序表达式的长度不固定,如何匹配

源字符串:<div id=“test1”>a test</div>

正则表达式:(?<=<div[^>]*>)[^<]+(?=</div>)

(1)匹配开始位置不确定,匹配结束位置确定

注:我不认可这样的匹配逻辑。

首先由“(?<=<div[^>]*>)”取得控制权,由位置 0 开始匹配,由于“<div[^>]*>”的长度不固定,可能会由逆序环视表达式的第 1 个字符从当前位置向左逐字符查找(这个可能性不大,因为太傻了!);有可能是先计算逆序表达式最小长度,然后在当前位置向前查找初始的匹配起点位置。在这里“<div[^>]*>”至少需要 5 个字符,所以由当前位置向左查找 5 个字符,然后再从左到右的方向,从这 5 个字符的第 1 个字符开始尝试匹配,但是由于此时位于位置 0处,前面没有任何字符,所以尝试匹配失败。

正则引擎传动装置向右传动,由位置 1 处开始尝试匹配,同样因为左侧的字符数不足,所以直接匹配失败,直到位置 5 处,向左查找 5 个字符,满足条件,此时把控制权交给“(?<=<div[^>]*>)”中的子表达式“<div[^>]*>”。“<div[^>]*>”取得控制权后,由位置 0 处开始向右尝试匹配,由于正则都是逐字符进行匹配的,所以这时会把控制权交给“<div[^>]*>”中的“<”,由“<”尝试匹配字符串中的“<”,匹配成功,接下来由“d”尝试匹配字符串中的“d”,匹配成功,同样的过程,由“<div[^>]*”匹配位置 0 到位置 5 之间的“<div ”成功,其中“[^>]*”在匹配“<div ”中的空格时会记录可供回溯的状态的,此时控制权交给“>”,由于已没有任何字符可供匹配,所以“>”匹配失败,此时进行回溯,由“[^>]*”让出已匹配的空格给“>”进行匹配,同样匹配失败,此时已没有可供回溯的状态,所以这一轮迭代匹配失败。

正则引擎传动装置向右传动,由位置 6 处开始尝试匹配,同样匹配失败,直到位置 16 处,此时的当前位置指的就是位置 16,向左查找到 5 个字符,把控制权交给“(?<=<div[^>]*>)”中的子表达式“<div[^>]*>”。“<div[^>]*>”取得控制权后,由位置 11 处开始向右尝试匹配, “<div[^>]*>”中的“<”尝试匹配字符串中的“s”,匹配失败;继续向左尝试,在位置 10 处由“<”尝试匹配字符串中的“e”,也匹配失败。同样的过程,直到尝试到位置 0 处,最后“<div[^>]*>”以位置 0 作为匹配起点,向右匹配,结果成功匹配到“<div id=“test1”>”,此时“(?<=<div[^>]*>)”匹配成功,控制权交给“[^>]+”,继续进行下面的匹配…

注:我认为这样的匹配规则是错误的,因为“<div[^>]*>”中的“<”匹配失败后往左尝试匹配,这样的做法很不合理,为什么?假设“<”继续向左尝试匹配,最后匹配成功了,控制权交个下个表达式,而该表达式匹配失败了,“<”会继续向左尝试匹配,可能又匹配成功了,但是下个表达式又匹配失败,这样的匹配逻辑肯定不对!!!

(2)匹配开始位置确定,匹配结束位置不确定

注:这个更符合逆序的概念,也更加合理,我认可这种匹配逻辑!

源字符串:<div>a test</div>

正则表达式:(?<=<div>)[^<]+(?=</div>)

“(?<=<div>)”获得控制权,从源字符串位置 0 开始向左匹配,首先“>” 去匹配,但是位置 0 左侧没有字符,所以匹配失败,第 1 次迭代匹配失败;接着正则引擎指针向右移动,“>” 去匹配字符串的字符“<”,匹配失败,第 2 次迭代匹配失败。

重复上述过程,直到位置 5,子表达式“<div>”中的“>” 去匹配位置 5 左边的第 1 个字符“>”,匹配成功;子表达式“<div>”中的“v”去匹配位置 5 左边第 2 个字符“v”,匹配成功…,最后子表达式“<div>”成功匹配位置 5 左边的字符串“<div>”,那么说明逆序肯定环视表达式“(?<=<div>)”匹配成功,即成功匹配位置 5;接着控制权给表达式“[^<]+”,该表达式从位置 5 开始向右逐个字符匹配,最后成功匹配到字符串“a test”,接着把控制权交个子表达式“(?=</div>)”,由它去验证字符串“a test”的结尾位置 11 是否符合正则式的要求,结果“(?=</div>)”成功匹配到了字符串“a test”后面的字符串“</div>”,说明字符串“a test”的结尾位置 11 符合要求,后续没有子表达式了,说明正则表达式迭代匹配成功 1 次,成功匹配到字符串“a test”。接着从位置 11 开始下次迭代匹配…

后面重复上述的过程,直到正则引擎的指针移到字符串的结尾处,则停止迭代匹配。

(三)逆序否定环视匹配过程

源字符串:adf<B>BerBilBlon<B>Ssdfefe</B>dfee
正则表达式:(?<!<B>)B

1. 逆序表达式的长度固定,如何匹配

(1)匹配起始位置不确定,匹配结束位置确定

当前位置是匹配终点,匹配起点在当前位置的左侧,最终的匹配起点是不确定的,初始的匹配起点可以根据逆序表达式的长度来查找。

注:我认为这样的匹配逻辑是错误的,不认可

首先由“(?<!<B>)”的子表达式“<B>”取得控制权,由位置 0 开始尝匹配,由于“<B>”的长度固定为 3,所以会从当前位置向左查找 3个字符,但是由于此时位于位置 0 处,前面没有任何字符,所以直接匹配失败,“<B>”匹配失败,那么整个逆序否定环视表达式“(?<!<B>)”则匹配成功,所以位置 0 满足逆序否定环视表达式“(?<!<B>)”,那么控制权就传给了“B”,由“B”从位置 0 开始向右匹配字符,于是“B”就去匹配字符串中的“a”,结果匹配失败,那么第 1 次迭代匹配失败。

正则引擎传动装置向右传动,你可以理解为有个指针的东西向右移动,此时指针来到位置 1 处,由位置 1 处向左查找 3 个字符,但是前面只有 1 个字符 a,所以同样和“<B>”匹配失败,则整个逆序否定环视表达式“(?<!<B>)”匹配成功,控制权传给“B”,由“B”从位置 1 开始向右匹配字符,于是“B”就去匹配字符串中的“d”,结果匹配失败,那么第 2 次迭代匹配失败。

直到位置 3 处,向左查找到 3 个字符串“abc”,字符数满足条件,此时“(?<!<B>)”中的子表达式“<B>”获得控制权。“<B>”取得控制权后,由位置 0 处开始向右逐个字符匹配字符串“abc”,既然是逐字符进行匹配的,所以这时会把控制权交给“<B>”中的“<”,由“<”尝试匹配字符串中的“a”,匹配失败,那么“<B>”就和字符串“abc”匹配失败,则整个逆序否定环视表达式“(?<!<B>)”匹配成功,控制权传给“B”,由“B”从位置 3 开始向右匹配字符,于是“B”就去匹配字符串中的“<”,结果匹配失败,那么第 4 次迭代匹配失败。

正则引擎的传动指针继续向右移动,此时来到了位置 4,那么正则引擎向左查找 3 个字符来匹配,查找到的字符串就是“df<”,接着“<B>”获得控制权,从位置 1 开始向右逐个字符匹配,那么首先由“<B>”中的“<”去匹配字符“d”,匹配失败,那么整个逆序否定环视表达式“(?<!<B>)”匹配成功,控制权传给“B”,由“B”从位置 4 开始向右匹配字符,于是“B”就去匹配位置 4 后面的“B”,结果匹配成功。

重复上述的过程直到正则引擎的指针移到字符串结尾才结束迭代匹配。

最后匹配到的“B”,如下所示(高亮部分):

(2)匹配起始位置确定,匹配结束位置不确定

当前位置是匹配起点,逆序环视是从当前位置向左开始匹配的,匹配终点在当前位置的左侧。不少人认为应该是这样的匹配规则,因为更符合逆序的概念。我也支持这个匹配逻辑。

注:需要明确的一点,无论是什么样的正则表达式,都是要从字符串的位置 0 处开始尝试匹配的,这点没有变。

逆序否定环视表达式“(?<!<B>)B”中的“<B>”先获得控制权,因为匹配从右到左,所以子表达式“<B>”中的“>”会先获得控制权,去匹配字符串当前位置左边的第 1 个字符,不过当前位置是 0,所以左侧没有字符,固然匹配失败,既然“<B>”匹配失败,那么整个逆序否定环视表达式“(?<!<B>)B”就匹配成功,也就是说位置 0 是匹配成功的,位置 0 是满足逆序否定环视表达式的,于是控制权交给“B”,由“B”从字符串位置 0 开始向右匹配字符,显然“B”匹配“a”是失败的,因此整个正则表达式的第 1 次迭代匹配失败。

重复上述的过程,直到位置 4,“<B>”从位置 4 开始向左逐个字符匹配,首先由“>”匹配位置 4 左边的第 1 个字符“<”,结果匹配失败,于是整个逆序否定环视表达式“(?<!<B>)B”匹配成功,也就是说位置 4 匹配成功,控制权交个了“B”,由“B”从位置 4 开始向右匹配字符,显示“B”与字符“B”匹配成功。

重复上述过程,直到正则引擎的指针移到位置 6 时,“<B>”逐个字符匹配位置 6 左侧的字符,首先“<B>”中的“>”先去匹配位置 6 左边的第 1 个字符“>”,匹配成功;接着“<B>”中的“B”去匹配位置 6 左边的第 2 个字符“B”,也匹配成功;接着“<B>”中的“<”去匹配位置 6 左边的第 3 个字符“<”,也匹配成功。那么最后“<B>”成功匹配到位置 6 左边的字符串“<B>”,因为是否定环视,所以整个逆序否定环视表达式匹配失败(即位置 6 不符合要求),所以整个正则表达式的迭代匹配失败,正则引擎的指针继续向后移。

重复上述过程,直到正则引擎指针移到字符串结尾处,正则迭代匹配结束。

2. 逆序表达式的长度不固定,如何匹配

正则表达式的环视深度剖析相关推荐

  1. 深度剖析Zabbix Web scenarios数据表结构

    深度剖析Zabbix Web scenarios数据表结构 前言 因开发需求,需要解析Zabbix web监控数据表结构:因为网上关于Zabbix数据表结构解析的比较少,所以心血来潮写了一篇作为记录. ...

  2. DDOS学习笔记(《破坏之王-DDOS攻击与防范深度剖析》)

           最近花了点时间把<破坏之王-DDOS攻击与防范深度剖析>看了一遍,坦白来说,这本书比较浅显,可以说是入门书,当然对于我这种对DDOS一知半解的人来说,也是一本不错的书,起码我 ...

  3. 《AngularJS深度剖析与最佳实践》一1.4 实现第一个页面:注册

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,第1.4节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 ...

  4. SurroundDepth:自监督多摄像头环视深度估计

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者丨黄浴 来源丨计算机视觉深度学习和自动驾驶 arXiv论文"SurroundDepth: ...

  5. libevent源码深度剖析

    原文地址:http://blog.csdn.net/sparkliang/article/details/4957667 libevent源码深度剖析一 --序幕 张亮 1 前言 Libevent是一 ...

  6. libevent源码深度剖析十一

    libevent源码深度剖析十一 --时间管理 张亮 为了支持定时器,Libevent必须和系统时间打交道,这一部分的内容也比较简单,主要涉及到时间的加减辅助函数.时间缓存.时间校正和定时器堆的时间值 ...

  7. 《AngularJS深度剖析与最佳实践》一第1章 从实战开始

    本节书摘来自华章出版社<AngularJS深度剖析与最佳实践>一书中的第1章,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区"华章计算机"公众号查看 第1章 从 ...

  8. 深度剖析:Redis分布式锁到底安全吗?看完这篇文章彻底懂了!

    ‍‍‍‍‍‍‍‍‍‍‍‍阅读本文大约需要 20 分钟. 大家好,我是 Kaito. 这篇文章我想和你聊一聊,关于 Redis 分布式锁的「安全性」问题. Redis 分布式锁的话题,很多文章已经写烂了 ...

  9. [Android] Toast问题深度剖析(二)

    欢迎大家前往云+社区,获取更多腾讯海量技术实践干货哦~ 作者: QQ音乐技术团队 题记 Toast 作为 Android 系统中最常用的类之一,由于其方便的api设计和简洁的交互体验,被我们所广泛采用 ...

最新文章

  1. 目标检测推理部署:优化和部署
  2. Activiti——流程变量(六)
  3. 【深度学习】深度学习之对抗样本问题和知识蒸馏技术
  4. LeetCode 06Z字形变换07整数反转
  5. AQS理解之四—看看我们写的和 ReentrantLock 的公平不公平锁的区别
  6. js-移动端android浏览器中input框被软键盘遮住的问题解决方案
  7. sqoop从mysql到hive问题
  8. IO多路复用之select
  9. mybatisplus 一次性执行多条SQL语句
  10. Tomcat启动时自动加载Servlet
  11. wordcount C
  12. jdk1.8 ConcurrentHashMap
  13. 为什么建议要延迟macOS升级,小编为你全面分析!
  14. docker centos rpm离线安装1.8.2及pull的正确姿势
  15. 繁凡的 ACM 退役记(一名普通算法竞赛选手与博主的竞赛生涯回忆录)凡是过往,皆为序章
  16. 【Redis总结-基础入门】
  17. 【基础】华为设备RIP协议原理及配置全集
  18. 模拟自动售卖机售卖3、5、8元饮料投币业务
  19. 斯嘉丽约翰逊60张pdf什么时间的?_《超体》的女主角露西,放在漫威宇宙中是什么水平?...
  20. 计算机网络--DHCP分析实验

热门文章

  1. ibm liberty_使用Eclipse和Open Liberty的Java EE 8上的Java 9
  2. java gc cms_Java垃圾收集器:G1GC何时将CMS强制退出?
  3. autovalue_AutoValue:生成的不可变值类
  4. fegin 参数丢失_许多参数和丢失的信息
  5. 部署被测软件应用和中间件_使用FlexDeploy对融合中间件应用程序进行自动化软件测试...
  6. 产品原型示例_原型设计模式示例
  7. 具有InlfuxDB的Spring Boot和Micrometer第2部分:添加InfluxDB
  8. Spring Data JPA教程
  9. websockets_使用Java WebSockets,JSR 356和JSON映射到POJO的
  10. 谁最终需要Java模块?