首页 > 其他分享 >正则表达式,你不知道的先行断言与后行断言

正则表达式,你不知道的先行断言与后行断言

时间:2024-06-13 16:57:33浏览次数:15  
标签:字符 匹配 断言 正则表达式 后行 文本 前瞻

正则表达式可以分为两个主要部分:标记和修饰符:

  • 标记(Token):是正则表达式的基本构建块,它们表示具体的字符文字、元字符、字符类别、重复限定符、边界匹配、分组和捕获等;

  • 修饰符(Modifier):是用来修改正则表达式的匹配行为的标志,例如忽略大小写、全局匹配、多行模式等。修饰符可以影响整个正则表达式的匹配结果;

例如. 正则表达式 \b\d/i,由 2 个标记(分别是 \b\d)与 1 个修饰符(i)组成。

以上内容仅用作名词介绍,基础内容请自行 Google。

前瞻断言与后顾断言

前瞻断言(Lookahead)也称先行断言,后顾断言(Lookbehind)也称后行断言。

两者统称为环顾断言(Lookaround),都是零长度(Zero-length Assertions)断言。

什么是零长度断言

很多地方称为零宽度断言(Zero-width Assertions),我更倾向于使用零长度断言。

首先理解下什么是字符消费(Consume Character):在正则表达式匹配的过程中,正则表达式引擎对输入文本进行逐个字符匹配,当匹配到一个字符时,它会将该字符视为已经“消费”了,因此该字符不会再被用于匹配其他部分。

而环顾断言与输入文本进行匹配后,会放弃匹配结果,不消费输入文本中的字符,只返回输入文本是否匹配的结果:是与否。

所以:

  • 零长度:是指对输入文本的字符消费长度是零;

  • 断言:是指仅对是否匹配进行判断,是一种断言操作;

没有什么是举个例子说明不了的,常规正则表达式 /f(o)o/(表达式里面的括号仅仅为了美观)与前瞻断言正则表达式 /f(?=o)o/ 进行对比:

输入文本 foo/f(o)o/

  1. 对正则表达式第一个标识 f 与输入文本第一个字符 f 进行匹配,匹配成功,消费掉输入文本第一个字符 f
  2. 对正则表达式第二个标识 o 与输入文本第二个字符 o 进行匹配,匹配成功,消费掉输入文本第二个字符 o
  3. 对正则表达式第三个标识 o 与输入文本第三个字符 o 进行匹配,匹配成功,消费掉输入文本第三个字符 o
  4. 正则表达式与输入文本都结束,匹配结果:foo

输入文本 foo/f(?=o)o/

  1. 对正则表达式第一个标识 f 与输入文本第一个字符 f 进行匹配,匹配成功,消费掉输入文本第一个字符 f
  2. 正则表达式第二个标识为前瞻断言,匹配标识 o 与输入文本第二个字符 o 进行匹配,匹配成功,但不消费输入文本的字符 o
  3. 由于输入文本并未消耗掉第二个字符,此时,对正则表达式第三个字符 o 与输入文本第二个字符 o 进行匹配,匹配成功,消费掉输入文本第二个字符 o
  4. 输入文本还剩下一个字符 o,但正则表达式结束,匹配结果:fo

前瞻,先行断言

前瞻断言用于在匹配模式中检查一个子字符串是否紧跟在另一个子字符串的前面,分为正向前瞻与负向前瞻。

正向前瞻语法(?=pattern)

负向前瞻语法(?!pattern)

正向(Positive)与负向(Nagetive)完全可以理解为是(True)与非(False)的逻辑判定。例如:

  • f(?=o),只有字符 o 前面的 f 会被匹配;
  • f(?!o)o,只有不是(非)字符 o 前面的 f 会被匹配;

前瞻断言本身的括号为非捕获组,如果希望对前瞻断言中的匹配内容进行捕获,需要在前瞻断言中使用括号,例如 (?=(pattern)) 的形式。

任何有效的正则表达式都可以在前瞻中使用(但是后顾不可以,稍后讲解)。

q(?!u)q[^u] 的区别

同样是非字符 u 的匹配操作,有什么区别?

例如. 输入文本为 quit

q(?!a):仅仅会匹配字符 q,它的含义是仅仅期望匹配到那些后面没有跟着字符 aq

q[^a]:会匹配到 qu,它的含义是匹配字符 q 与后面非字符 a 的内容。

引擎匹配逻辑

首先,让我们来看一下引擎是如何将 q(?!u) 应用于文本 Iraq

  1. 正则表达式第一个标记是 q,引擎会遍历文本,直到匹配到最后的 q
  2. 下一个标记是前瞻断言,进入前瞻内部匹配标记 u,内部标记失败,前瞻断言结束。
  3. 但引擎注意到是负向前瞻操作,所以整个正则表达式的匹配却是成功的。
  4. 正则表达式与文本都结束,最后,正则表达式 q(?!u) 匹配成功并返回匹配项 q

其次,还是这个正则表达式 q(?!u),看一下引擎是如何应用于文本 quit

  1. 同样 q 匹配 q 字符。
  2. 下一个标记是前瞻断言,进入前瞻内部匹配标记 u,内部的标记与文本第二个字符 u 匹配成功,前瞻断言结束。
  3. 常规正则表达式下,引擎会匹配文本第三个字符 i,但由于是前瞻断言,所以只记录成功与否并放弃匹配项,导致引擎回退到字符 u
  4. 由于是负向前瞻,前瞻内部正则表达式匹配成功意味着整个正则表达式匹配失败,即该字符 q 不匹配,正则表达式将重新进行匹配。
  5. 直到文本结尾并未匹配到字符 q,最后,正则表达式 q(?!u) 匹配失败。

最后,我们使用正则表达式 q(?=u)i 观察下引擎是如何应用于文本 quit

  1. 同样 q 匹配 q 字符。
  2. 正向前瞻内部标识 u 匹配文本第二个字符 u
  3. 由于前瞻断言,只记录匹配成功并放弃匹配项,导致退出前瞻断言后引擎将从字符 i 回退到字符 u 进行下一个标识的匹配。
  4. 正则表达式下一个标记为 i,但当前匹配文本字符是 u,匹配失败,正则表达式将重新进行匹配。
  5. 直到文本结尾并未匹配到字符 q,最后,正则表达式 q(?=u)i 匹配失败。

后顾,后行断言

后顾断言用于在匹配模式中检查一个子字符串是否紧跟在另一个子字符串的后面,分为正向后顾与负向后顾。

正向后顾语法(?<=pattern)

负向后顾语法(?<!pattern)

例如:

  • (?<!a)b:会匹配非字符 a 后面的 b,它不会匹配 cab,但会匹配 beddebt 中的 b(仅仅是字符 b);

  • (?<=a)b:会匹配 cab 中的 b(仅仅是字符 b),而不会匹配 beddebt

\b\w+(?<!s)\b\b\w+[^s]\b 的区别

如果希望找到所有不以字符 s 结尾的单词,需要使用 \b\w+(?<!s)\b,而不可以使用 \b\w+[^s]\b。对于输入文本 John's,前者会匹配 John,而后者则会匹配到 John'(包括单引号)。如果希望后者达到同样的效果,需要改写正则表达式为 \bw\w+[^s\W]\b

引擎匹配逻辑

让我们将 (?<=a)b 应用于 thingamabob

  1. 引擎开始于后顾断言标识和输入文本的第一个字符 t
  2. 后顾断言使引擎对输入文本向前移动一个字符,查看是否匹配字符 a,但字符 t 是第一个字符,无法向前移动,所以后顾断言失败。
  3. 匹配输入文本的下一个字符 h,再次进行后顾断言,再次向前移动一个字符,查看是否匹配字符 a,它找到了字符 t,无法匹配,所以后顾断言再次失败。
  4. 继续匹配输入文本,当位于字符 m 时,后顾断言与字符 m 的前一个字符 a 进行匹配,匹配成功并放弃匹配项,由于是正向,后顾断言成功。
  5. 由于后顾断言是零长度断言,所以当前匹配位置仍位于字符 m 处,下一个匹配标记为 b,与字符 m 不匹配,匹配失败。
  6. 直到匹配到位于输入文本的第一个 b 字符时,正向后顾断言成功,下一个匹配标记 b 与字符 b 匹配,整个正则表达式匹配成功。
  7. 最后,匹配结果为输入文本中第一个 b 字符。

注意

在很多正则表达式方言(flavor)中,后顾断言的内容不可以使用正则表达式(与前瞻断言不同)。

由上述内部匹配逻辑可知,后顾断言会让正则表达式引擎临时对前面的输入文本与后顾的内容进行匹配检查,所以引擎需要明确知道临时回查多少个字节,当匹配完需要回到当前位置。所以,后顾断言的内容在大多数正则表达式方言中只允许为固定长度的字符串

使用前瞻断言进行数据校验

由于前瞻断言中可以使用正则表达式,所以通常使用前瞻断言对数据格式进行校验。例如密码,用户名等。

举个简单的示例,正则表达式 ^(?=.*[a-z])(?=.*[A-Z])[a-zA-Z]{8,16}$ 的规则:

  • 输入文本只能包含英文字母,不可以有其他字符;
  • 输入文本必须包括小写字母;
  • 输入文本必须包括大写字母;
  • 输入文本必须是 8 - 16 位;

:为什么前瞻断言可以写在正则表达式一开始的位置,按照常规写法不应该写在要匹配的内容后面么?

为了便于理解,我们将上述正则表达式简化为 ^(?=.*[a-z])[a-z]{4,8}$

分解得:^(?=.*[a-z])[a-z]{4,8}$ = ^ + (?=.*[a-z]) + [a-z]{4,8} + $

这样看起来就清晰很多,是正向前瞻与另一个标识的组合形式,而并非整体是一个前瞻断言语法。

  1. 正则表达式引擎从输入文本起始位置开始,使用前瞻断言内部的正则表达式 .*[a-z] 对后续文本进行匹配。
  2. .*[a-z] 匹配任意数量的任意字符(包括零个字符),但必须以至少一个小写字母结尾。
  3. 如果前瞻断言匹配成功,则由于零长度断言,将会从输入文本开始对 [a-z]{4,8} 进行匹配。
  4. 再加上边界标识限制,所以最后的规则是从输入文本开始到结束,必须存在一个小写字母,且只能存在 4 - 8 的小写字母才能满足匹配。

对于这个简化后的正则表达式,前瞻断言有点多余,完全可以写成 ^[a-z]{4,8}$ 的形式,仅为了解释逻辑而已。

:为什么会有多个连续的前瞻断言,这又是怎么回事儿?

^(?=.*[a-z])(?=.*[A-Z])[a-zA-Z]{8,16}$ 中,由于前瞻断言都是零长度断言,所以每次判定完前瞻后,都会回到起始位置进行下一项判定,所以多个前瞻断言是并列(and)关系,需要同时满足。

原子分组

原子分组(Atomic Group)也是非捕获分组的一种,但具有特殊的回溯行为。原子分组在匹配其内部模式后,不允许回溯,即一旦内部模式匹配成功,整个分组的匹配就固定下来,不再参与后续的回溯匹配。

语法(?>pattern)

示例会让原子分组的行为更清晰,正则表达式 a(bc|b)c(捕获分组)可以匹配 abccabc。正则表达式 a(?>bc|b)c(原子分组)仅可以匹配 abcc,但会不匹配 abc

当应用于 abc 时,两者都会匹配标记 a 与字符 a,标记 bc 与字符串 bc。最后的标记 c 将会失败。此时,两者有所不同:

  • 捕获分组的正则表达式会记住可选标记 | 的回溯位置,放弃之前匹配的 bc 尝试匹配 b,然后匹配 c,最后匹配成功;
  • 原子分组的正则表达式一旦 bc 匹配成功退出分组,将会丢弃所有分组内部标记的回溯位置,不会回溯重新匹配;

环顾断言的原子性

环顾断言零长度断言的事实自身就说明了其是原子性的,一旦满足环顾断言的条件,正则表达式引擎就会放弃匹配,仅保留是否匹配的结果。所以在环顾断言中,一旦匹配成功,将不会回溯尝试其他可选排列。

标签:字符,匹配,断言,正则表达式,后行,文本,前瞻
From: https://blog.csdn.net/weixin_44189802/article/details/139632682

相关文章

  • C#实现使用正则表达式验证身份证号 (附完整源码)
    C#实现使用正则表达式验证身份证号代码解释:使用方法:下面是一个使用C#和正则表达式验证中国身份证号码的示例代码。中国的身份证号码通常是18位,前17位是数字,最后一位可以是数字或字母X。这个正则表达式会检查格式是否正确。usingSystem;usingSystem.......
  • c# 正则表达式验证"身份证","手机号","邮箱地址","邮编"
    publicstaticclassVerify{///<summary>///验证手机号码///</summary>///<paramname="str_handset"></param>///<returns></returns>publicstaticboolIsHandset(stringstr_handset)......
  • Linux -- 正则表达式基础
    提示:制作不易,可以点个关注和收藏哦。前言        虽然我们这一节的标题是正则表达式,但实际这一节实验只是介绍grep,sed,awk这三个命令,而正则表达式作为这三个命令的一种使用方式(命令输出中可以包含正则表达式)。正则表达式本身的内容很多,要把它说明清楚需要单独一门......
  • 简单了解java中的正则表达式
    正则表达式1、正则表达式认识正则表达式通常用来校验,检查字符串是否符合规则,由一些特定的字符组成的字符串校验规则,就称之为正则表达式。2、正则表达式能干啥?正则表达式只能针对字符串格式进行校验,所以它的应用场景就是对用户输入的字符串进行校验3、正则表达式使用3.......
  • 27-unittest之断言(assert)
            在测试方法中需要判断结果是pass还是fail,自动化测试脚本里面一般把这种生成测试结果的方法称为断言(assert)。    使用unittest测试框架时,有很多的断言方法,下面介绍几种常用的断言方法:assertEqual、assertIn、assertTrue。一、测试代码importunittes......
  • python---正则表达式
    ==本章目标:1:能够知道在Python中使用正则要导入的模块;[了解]   re模块2:能够使用re模块匹配单个字符;[重点]   \d \w 正则表达式的概述:基本介绍正则表达式,也叫做规则表达式,通常会说成[正则]实际上正则表达式就是指符合一定规则的字符串,同时他能用......
  • python 正则表达式使用简介和实用技巧
    元字符释义.代指任意字符^从字符串开始匹配$匹配字符串的结尾*匹配前面挨着的字符,能匹配0到无穷次+同*,能匹配1到无穷次(最少1个)?匹配前面挨着的字符,匹配0或1次{}自定义匹配次数,{1,6}匹配1到6次,{6}匹配6次(重复匹配前面挨着的字符)......
  • Qt 正则表达式 QRegularExpression
    正则表达式QRegularExpression学习在Qt中有两种和正则相关的类,一种是QRegExp类,今天查资料说的好像是从Qt4传承下来的,BUG相对来说比较多,目前基本已停止维护了,多用于正则匹配,还有一种就是今天要讲的QRegularExpression类,是Qt5新开辟出来的类,相对来说比较完善(网上这......
  • 正则表达式学习(3)——语法
    普通字符[abc]匹配中括号的所有字符[^abc]匹配除了中括号的所有字符[A-Z]匹配A-Z的大写字母区间内的字符[a-z]匹配a-z的小写字母区间内的字符[0-9]匹配0-9的数字.匹配除了换行、回车(\n,\r)的单个字符,等价于[^\n\r]\s是匹配所有空白符,包括换行\S非空白符,不包括换......
  • 正则表达式学习(2)---字符特性
    正则表达式特性字符匹配普通字符:匹配字面值。元字符:元字符具有特殊的含义,例如\d匹配任意数字字符,\w匹配任意字母数字字符,.匹配任意字符(除了换行符)等。量词*:匹配前面的模式零次或多次,例如aa*+:匹配前面的模式一次或多次,例如aa+?:匹配前面的模式零次或一次,例如aa?......