正则运行测试网址 https://regex101.com/

最开始学环视,是通过看《正则表达式必知必会》这本小册子,学完后感觉很模糊,好像会了,又好像不清楚,直到在网上看到下面这张图,才算是真正搞懂环视是干嘛的了。

对于字符串"abc",正则表达式可以匹配的元素有三个字符与四个位置,分别是字符“a”,“b”,“c”与位置0,1,2,3。正则中类似 a \d \w . * 之类的,都是用来匹配的字符的,而 环视 是用来匹配位置的。

环视(lookaround)有多种翻译,零宽断言,前瞻后顾等等,都是从不同的方面来描述环视。零宽断言重点是说它是来匹配位置, 不占有字符 。前瞻后顾主要是说它对匹配的位置的前后字符进行约束。本文采用环视来作为lookaround的翻译。

环视分为四种,分别是 顺序肯定环视 顺序否定环视 逆序肯定环视 逆序否定环视 ,具体如下表所示。

环视类型 表达式
顺序肯定环视 (?=expression)
顺序否定环视 (?!expression)
逆序肯定环视 (?<=expression)
逆序否定环视 (?<!expression)

所谓顺序、逆序,说的其实就是对此位置的左侧还是右侧进行约束。因为正则表达式的匹配顺序是从左至右的,所以顺序环视就是对匹配位置的右侧进行约束,逆序环视就是对匹配位置的左侧进行约束。肯定说的是需要此位置的左侧或者右侧是什么,否定说的是需要此位置的左侧或者右侧不是什么。

顺序肯定环视

顺序肯定环视的表达式为 (?=expression) ,要求位置右侧要符合expression,这个位置才能成功匹配整个的环视表达式。 (?=b) 即要求匹配位置的右侧为b。对于字符串abb,共有四个位置可供匹配。

(?=b) 的简要匹配过程如下:

  1. (?=b) 先去尝试匹配位置0,发现位置0的右侧是a,不是b,不符合 (?=b)
  2. 字符a不是位置,不进行匹配;
  3. (?=b) 尝试匹配位置1,位置1的右侧是b,符合 (?=b) ,保存匹配结果;
  4. 字符b不是位置,不进行匹配;
  5. (?=b) 尝试匹配位置2,位置2的右侧是b,符合 (?=b) ,保存匹配结果;
  6. 字符b不是位置,不进行匹配;
  7. (?=b) 尝试匹配位置3,位置3的右侧没有字符,不是b,不符合 (?=b)

最终匹配结果,只有位置1与位置2匹配成功,如下图所示。

环视单独使用作用不明显,因为它是零宽度的,不占有字符,匹配成功也不会返回字符。环视与字符配合使用,才能发挥它的最大威力。

要求匹配字符a,并且a的右侧必须是b 。相应的正则表达式为 a(?=b) 。准备匹配的字符串为abb、ada、acb。

正则表达式 a ,abb、ada、acb中的所有a都会被匹配到,如下图。

当正则表达式改为 a(?=b) ,就只有一个a符合要求。

下面解释一下 a(?=b) 。为了方便理解,将正则表达式 a(?=b) 的匹配分为两步。

先用a去匹配字符串中的a,这时会有4个字符符合要求。分别是abb中的a,ada中两个a,acb中的a。表达式中 (?=b) 用来匹配位置,因为 (?=b) 在表达式 a(?=b) 中位于a的右侧,意味着它对匹配到的a的右侧位置提出了要求。如下图所示,有四个位置需要匹配。

(?=b) 是顺序肯定环视,即需要匹配的位置的右侧是表达式b。用 (?=b) 去匹配位置1,位置1的右侧是字符b,符合要求,所以位置1符合要求。用 (?=b) 匹配位置2,位置2的右侧是字符d,所以位置2不符合要求。用 (?=b) 匹配位置3,位置3的右侧是换行符或是没有字符(这取决于ada是不是字符串结尾),不符合 (?=b) 。位置4也不符合 (?=b) 。最终只有abb中的a符合整体正则表达式 a(?=b)

匹配过程不是上文说的那样,上述说法只是为了方便理解。实际上,正则引擎会先用正则表达式 a(?=b) 中最左侧的字符a去尝试匹配到abb中的第一个a,匹配成功,再用 (?=b) 去匹配a右侧的位置,符合要求,所以abb中的a符合正则表达式 a(?=b) 的整体要求,匹配成功。然后正则引擎会用a去尝试匹配abb中的第一个b,不符合要求,舍弃,继续用a尝试匹配abb中的第二个b,仍然不符合要求。正则引擎继续用正则表达式中的a匹配ada中的第一个a,符合要求,再用 (?=b) 尝试匹配此a的右侧位置,此位置的右侧不是b,不符合要求。ada中的第一个a不符合正则表达式 a(?=b) 的整体要求,舍弃,继续向右匹配,直到所有的字符都被匹配一遍。

需要注意的是,正则表达式 a(?=b) 最终匹配的是abb中的 a ,而不是abb中的 ab ,时刻牢记,环视匹配的是位置,不占有字符,所以也被称为零宽断言。

顺序否定环视

顺序否定环视的表达式为 (?!expression) ,要求匹配的位置右侧不能是expression。 (?!b) 即要求匹配位置的右侧不能是b。

正则表达式 a ,abb、ada、acb中的所有a都会被匹配到,如下图。

当正则表达式改为 a(?!b) ,则abb中的a会不符合正则表达式。

匹配过程简述如下:

  1. 正则引擎先尝试用a匹配abb中的a,符合要求;
  2. 引擎继续用 (?!b) 匹配a的右侧位置,要求此位置的右侧不能是b,匹配后发现位置不符合此要求,则abb中的字符a不符合整个正则表达式 a(?!b)
  3. 正则引擎舍弃abb中的a,继续向右匹配;
  4. 匹配到ada中的第一个a,符合正则表达式中的a;
  5. 正则引擎继续用 (?!b) 匹配a的右侧位置,要求此位置的右侧不能是b,符合要求,所以ada中的第一个a符合正则表达式 a(?!b) 的整体要求,匹配成功;
  6. 剩下的两个a也是同样是匹配过程。

就上面的问题而言,使用 a[^b] 同样可以只匹配到ada、acb中的a,而不去匹配abb中的a。但是这样有两个问题,一是 a[^b] 不只是匹配了a一个字符,实际匹配到了a与a后面的字符;二是若是想要匹配abb、abd、ab中的a,并且排除abc中的a,上述正则表达式就会失效。这时环视就显示出优势了,通过 a(?!bc) 就可以区分。

所以,环视在针对表达式进行匹配时,会很方便。

逆序肯定环视

逆序肯定环视的表达式为 (?<=expression) ,要求匹配位置的左侧必须是expression,例如 (?<=b) 即要求匹配位置的左侧必须是b。

正则表达式 a ,abc、bab、dad中的所有a都会被匹配到,如下图。

当正则表达式改为 (?<=b)a ,则只有bab中的a符合正则表达式。

匹配过程简述如下:

  1. 表达式 (?<=b)a 会先用 (?<=b) 对字符串abc中的位置0进行匹配,也就是abc中a的左侧位置,此位置的左侧无字符,自然也不是b,不符合 (?<=b)
  2. 继续使用 (?<=b) 匹配abc中的位置1,也就是b的左侧位置,此位置左侧为a,不符合 (?<=b)
  3. 继续向左匹配,直到匹配到bab中的位置1,也就是a的左侧位置,位置1的左侧为b,符合 (?<=b)
  4. 然后用正则表达式 (?<=b)a 中的a继续进行匹配,因为a位于 (?<=b) 的右侧,所以用a去匹配bab中位置1的右侧字符,匹配成功;
  5. 至此,bab中的位置1与位置1右侧的字符a对表达式 (?<=b)a 全部匹配成功,将匹配结果保存;
  6. 正则表达式继续向左匹配,直到全部匹配结束。

可以看到,对正则表达式进行匹配时,也是先对表达式中的左侧元素匹配,成功后才会匹配表达式中的下一个元素。

逆序否定环视

逆序否定环视的表达式为 (?<!expression) ,要求匹配位置的左侧不是expression,例如(?<!b)即要求匹配位置的左侧不能是b。

正则表达式 a ,abc、bab、dad中的所有a都会被匹配到,如下图。

当正则表达式改为 (?<!b)a ,则只有abc、dad中的a符合正则表达式。

匹配过程简述如下:

  1. 表达式 (?<!b)a 首先用 (?<!b) 去尝试匹配abc中的位置0,也就是abc中的a的左侧位置,位置0的左边无字符,自然也不是字符b,符合 (?<!b)
  2. 然后用 (?<!b)a 中的a继续匹配,因为表达式中a在 (?<!b) 的右侧,所以会尝试匹配位置0的右侧字符,匹配成功;
  3. abc中的位置0与位置0右侧的字符a对整个正则表达式 (?<!b)a 匹配成功;
  4. 表达式 (?<!b)a (?<!b) 去继续尝试匹配abc中的位置1,匹配成功;
  5. 正则中的a继续匹配位置1的右侧字符,匹配失败,
  6. abc中的位置1与位置1右侧的字符b对整个正则表达式 (?<!b)a 匹配失败;
  7. 正则表达式 (?<!b)a 继续向左匹配,直到所有的位置与字符都匹配完毕。

既然是对位置的匹配,那么 \b ^ $ 也可以视为环视。 \b 匹配单词边界或是字符串的起始或结束,要求是此位置的前后,分别是单词字符和不是单词字符,等价于 (?<!\w)(?=\w)|(?<=\w)(?!\w) ,但是不等价于 (?<=\W)(?=\w)|(?<=\w)(?=\W) 。同样的, ^ 也是匹配一个位置,要求是此位置左侧非字符,右侧任意字符,等价于 (?<![\w\W])(?=[\w\W]) $ 匹配一个位置,要求是此位置左侧任意字符,右侧非字符,等价于 (?<=[\w\W])(?![\w\W])

\b的等价表达式

\b 匹配单词边界或是字符串的起始或结束,如下图所示, \b 匹配了word单词前后,on单词前后,off单词前后共6个位置。

(?<!\w)(?=\w)|(?<=\w)(?!\w) 匹配同样的内容,结果如下所示,匹配结果与 \b 一致。

(?<=\W)(?=\w)|(?<=\w)(?=\W) 匹配同样的内容,结果如下所示。

可以看到,字符串内的单词间隔都可以正确匹配,问题出在字符串的起始与结束。表达式的本意是通过 (?<=\W)(?=\w) 匹配单词与字符串的起始位置, (?<=\w)(?=\W) 匹配单词与字符串的终止位置。

(?<=\W)(?=\w) 要求匹配的位置,左侧是非单词字符,但其中隐含要求 左侧需要有字符 ,然后右侧为单词字符。但是对于字符串起始位置, 左侧是没有字符的 ,自然也就就无法匹配 (?<=\W) ,所以字符串起始位置匹配失败。字符串终止位置是同样的问题。

本文主要讨论了环视的四种分类,简要的匹配过程以及\b的等价表达式。

下述两个博客的文章都是以C#为基础写作的,在普通捕获组与命名捕获组的混用上,与Java有所不同

正则大神主页: https://blog.csdn.net/lxcnn?t=1

正则表达式入门教程: https://deerchao.cn/tutorials/regex/regex.htm#top

正则表达式 环视 (?=pattern)(?!pattern)(?<=pattern)(?<!pattern) 环视 是指 正则表达式 引擎先对字符串前后进行环顾观察,再进一步进行匹配操作。 环视 可分为顺序 环视 和逆序 环视 环视 是不占用宽度的。首先 环视 所匹配的结果不纳入最终结果,其次 环视 匹配时匹配指针不会移动。 顺序 环视 顺序 环视 又称前瞻,即向前查看字符串是否符合相应的条件,若符合才能算匹配成功,否则匹配失败。表达式(?=pattern)和(?!pattern)前者表示肯定性前瞻,后者表示否定性前瞻。 很多文章,把 环视 (又叫零宽断言,或者预搜索,随你怎么叫啦),解释的云里雾里的,看他们的图示, 这里,他们其实是默认你已经知道了用法(这种教程,是他们自己知道,也认为你也知道了),这种解析都是搅合浆糊的, 大多数人,应该是,看了一遍后,该不会!还是不会的!除非,你看了一遍又一遍,才明白过来,要不是他们这些,你可能早就会了,错误引导了你的思维,好多人明白后,会气得xxx,这时要(注意修为),原因:它(教程这),这里少写了一个东西,藏着呢! 我来解析吧, 数据_表达式 环... 环视 ( Look around)1 环视 基础 环视 只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。 环视 匹配的最终结果就是一个位置。 环视 的作用相当于对所在位置加了一个附加条件,只有满足这个条件, 环视 子表达式才能匹配成功。 环视 按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种 环视 。顺序 环视 相当于在当前位置右侧附加一个条件 所谓 环视 就是先看,看到了恰当字符序列的之后再尝试匹配。 比如我现在要尝试匹配字符串&amp;amp;quot;我爱&amp;amp;quot;但是一定要是爱的&amp;amp;quot;编程&amp;amp;quot;而不是&amp;amp;quot;狗狗&amp;amp;quot;等其他东西。 这时就用到 环视 的元字符(?=···)了, 环视 的‘看’虽说也是某种意义上的匹配但是并不会占用字符。 于是我写下了&amp;amp;quot;(?=我爱(编 所有的主流的 正则表达式 实现都支持前者,但支持后者的就没那么多了。一般来说,凡是支持向前查看的 正则表达式 实现也都支持肯定式向前查看和否定式向前查看。类似地,凡是支持向后查看的 正则表达式 实现也都支持肯定式向后查看和否定式向后查看。向前查看和向后查看其实是有返回结果的,只不过结果永远都是零长度字符串。”, 环视 能够向前或者向后查看, 环视 可以去除一些不用被返回的匹配。注意,被匹配到的 66 被没有出现在最终的匹配结果里。,不同之处是匹配到的 66 是否出现在最终的匹配结果之中。开头的子表达式,需要匹配的文本跟在。 环视 look around)分为前瞻( Look ahead )和后顾( look behind),也称为零宽度断言(zero-length assertions )。那什么是零宽度断言呢?正则 环视 实际上是有匹配字符的,但又立即放弃了对字符的占有或消耗,也就是说 环视 不消耗字符串,只表现出是否匹配或存在该模式的子字符串的结果,然后又回到了它开始 环视 的那个位置,所以我们称它为零宽度断言。 肯定式前瞻(... 正则表达式 在匹配文本时,一般都是按照从左到右的顺序进行的,并且会消耗匹配的字符, 环视 ( look around)能够实现在特定位置向左或向右查看(匹配)。 环视 结构不占用(消耗)任何字符,只匹配文本中的特定位置,这一点与单词分界符”\b”,锚点”^”和”$”相似,但是 环视 更加通用。 环视 常见的用途是匹配前缀,匹配后缀和寻找重复的单词。 一, 环视 结构 环视 正则表达式 中的特殊结构, 环视 正则表达式 格式... 环视 ( Look around) 1 环视 基础 环视 只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。 环视 匹配的最终结果就是一个位置。 环视 的作用相当于对所在位置加了一个附加条件,只有满足这个条件, 环视 子表达式才能匹配成功。 环视 按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种 环视 。顺序 环视 相当于在当前位置右侧附加一个条件 小数点可以匹配除了换行符“/n”以外的任意一个字符,如要匹配小数点本身,用“/”进行转义“/.”。 一些细节 对于使用传统NFA引擎的大多数语言和工具,如Java、.NET来说,“.”的匹配范围是匹配除了换行符“/n”以外的任意一个字符。 但是对于javascript来说有些特殊,由于各浏览 首先这个不属于技术文章,因为它是从新手的角度理解,也不苛求用技术术语规范表达,技术大神不要鄙视,可以无视。因为我也是小白,一开始也对断言摸不清头脑。什么正向 环视 ,负向 环视 ,一头雾水。言归正传!断言也叫 环视 ,听上去就是意思是有假设和判断性质。 知道^和$吧?代表位置,行的开头和结尾,不占用空间,就是透明空气。干脆姑且先这样理解: (?<=xx)和(?<!xx)相当于^。 (?=xx)和(?!xx)相当于$。 只是这里的^$是附带条件的。 =============... 正则表达式 环视 功能 正则表达式 ,又称规则表达式。(Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。 正则表达式 通常被用来检索、替换那些符合某个模式(规则)的文本。 环视 ,在不同的地方又称之为零宽断言,简称断言。 环视 强调的是它所在的位置,前面或者后面,必须满足 环视 表达式中的匹配情况,才能匹配成功。 环视 结构不匹配任何字符,只匹配文...