正则表达式的先行断言(lookahead)和后行断言(lookbehind) 负预测 正预测 正向 负向 之 浅显通俗解释
如题,在正则表达式的先行断言(lookahead)和后行断言(lookbehind) 负预测 正预测 正向 负向 应用时查看资料时,经常看的一头雾水,本来比较通俗易懂的事,因为几个翻译或专业术语就变的难以理解,下面以下面的文章链接为基础进行一些通俗易懂的解释。
开始之前说明:
先说明方向:如字符串“abcdefg",从左向右=向前,从右向左=向后。即向右为向前,向左为向后。
正文开始(以下文中红或蓝字为通俗说明):
正则表达式中的零宽断言是一种特殊的结构,它在匹配的时候不会消耗字符,只是对匹配位置进行条件判断。这对于一些复杂的模式匹配非常有用,因为它允许你在匹配位置前面或后面添加条件,从而更精确地控制匹配。
正则表达式的先行断言和后行断言一共有 4 种形式:
【 用bing词典查如下:
positive 有”肯定“这个含义。
negative 有”否定“这个含义。
lookahead 有”预测先行;前瞻“的含义。下面采用”向右“这个意思。
lookbehind 有”回头(看),向后查找;后向匹配“的含义。下面采用”向左“这个意思。
assertion有”断言,声明,主张“的含义。下面采用”断言“这个意思,但感觉”声明“更好一些。
其实到这,大家看下面的文章英文意思,就很清楚了,反而看中文难以理解。
“零宽”看后面的解释。断言就是匹配,我们写的整个正则表达式都是为了匹配。
】
- (?=pattern) 【零宽 向右肯定 断言】零宽正向先行断言(zero-width positive lookahead assertion)
- (?!pattern) 【零宽 向右否定 断言】零宽负向先行断言(zero-width negative lookahead assertion)
- (?<=pattern) 【零宽 向左肯定 断言】零宽正向后行断言(zero-width positive lookbehind assertion)
- (?<!pattern) 【零宽 向左否定 断言】零宽负向后行断言(zero-width negative lookbehind assertion)
【上面表达式中,”=“表示肯定,”!“表示否定,这个天天写代码没有问题吧。”<"左尖括号,那就简单理解为向左(向后),对应没有"<"那就认为是向右(向前)。】
【其实看到这里,基本不用向后看了,可以试着用了。后面文章一些示例和解释当然是有用的。】
这里面的 pattern 是一个正则表达式。
如同 ^ 代表开头,$ 代表结尾,\b 代表单词边界一样,先行断言和后行断言也有类似的作用,它们只匹配某些位置,在匹配过程中,不占用字符,所以被称为"零宽"。所谓位置,是指字符串中(每行)第一个字符的左边、最后一个字符的右边以及相邻字符的中间(假设文字方向是头左尾右)。
概念说明:
- 零宽(Zero-width): 只匹配位置,零宽意味着断言在匹配时不会"消耗"字符串,它只是对位置进行条件判断,不包括匹配位置之前或之后的字符在匹配结果中。
- 先行【向右】(Lookahead): 表示断言发生在匹配位置之前。
- 后行【向左】(Lookbehind): 表示断言发生在匹配位置之后。
- 正向【肯定】(Positive): 匹配括号中的表达式,即断言所作的条件判断是肯定的,即只有当条件成立时,匹配才成功。
- 负向【否定】(Negative): 不匹配括号中的表达式,即断言所作的条件判断是否定的,即只有当条件不成立时,匹配才成功。
下面分别举例来说明这 4 种断言的含义。
(?=pattern) 【 向右肯定】正向先行断言
代表字符串中的一个位置,紧接该位置之后的字符序列能够匹配 pattern。
【代表字符串中的一个匹配A,紧接该匹配A的右边能够匹配 pattern。】
例如对 "a regular expression" 这个字符串,要想匹配 regular 中的 re,但不能匹配 expression 中的 re,可以用 re(?=gular),该表达式限定了 re 右边的位置,这个位置之后是 gular,但并不消耗 gular 这些字符。
将表达式改为 re(?=gular).,将会匹配 reg,元字符 . 匹配了 g,括号这一砣匹配了 e 和 g 之间的位置。
(?!pattern) 【 向右否定 】负向先行断言
代表字符串中的一个位置,紧接该位置之后的字符序列不能匹配 pattern。
【代表字符串中的一个匹配A,紧接该匹配A的右边不得匹配 pattern。】
例如对 "regex represents regular expression" 这个字符串,要想匹配除 regex 和 regular 之外的 re,可以用 re(?!g),该表达式限定了 re 右边的位置,这个位置后面不是字符 g。
负向和正向的区别,就在于该位置之后的字符能否匹配括号中的表达式。
(?<=pattern) 【向左肯定 】正向后行断言
代表字符串中的一个位置,紧接该位置之前的字符序列能够匹配 pattern。
【代表字符串中的一个匹配A,紧接该匹配A的左边能够匹配 pattern。】
例如对 regex represents regular expression 这个字符串,有 4 个单词,要想匹配单词内部的 re,但不匹配单词开头的 re,可以用 (?<=\w)re,单词内部的 re,在 re 前面应该是一个单词字符。
之所以叫后行断言,是因为正则表达式引擎在匹配字符串和表达式时,是从前向后逐个扫描字符串中的字符,并判断是否与表达式符合,当在表达式中遇到该断言时,正则表达式引擎需要往字符串前端检测已扫描过的字符,相对于扫描方向是向后的。
(?<!pattern) 【 向左否定 】负向后行断言
代表字符串中的一个位置,紧接该位置之前的字符序列不能匹配 pattern。
【代表字符串中的一个匹配A,紧接该匹配A的左边不能匹配 pattern。】
例如对 "regex represents regular expression" 这个字符串,要想匹配单词开头的 re,可以用 (?<!\w)re。单词开头的 re,在本例中,也就是指不在单词内部的 re,即 re 前面不是单词字符。当然也可以用 \bre 来匹配。
对于这 4 个断言的理解,可以从两个方面入手:
-
1、关于先行【向右】(lookahead)和后行【向左】(lookbehind):正则表达式引擎在执行字符串和表达式匹配时,会从头到尾(从前到后)连续扫描字符串中的字符,设想有一个扫描指针指向字符边界处并随匹配过程移动。【向右】先行断言,是当扫描指针位于某处时,引擎会尝试匹配指针还未扫过的字符,先于指针到达该字符,故称为先行。【向左】后行断言,引擎会尝试匹配指针已扫过的字符,后于指针到达该字符,故称为后行。
-
2、关于正向【肯定】(positive)和负向【否定】(negative):正向就表示匹配括号中的表达式,负向表示不匹配。
对这 4 个断言形式的记忆:
-
1、先行【向右】和后行【向左】:后行断言 (?<=pattern)、(?<!pattern) 中,有个小于号,同时也是箭头,对于自左至右的文本方向,这个箭头是指向后的,这也比较符合我们的习惯。把小于号去掉,就是先行断言。
-
2、正向【肯定】和负向【否定】:不等于 (!=)、逻辑非 (!) 都是用 !号来表示,所以有 ! 号的形式表示不匹配、负向;将 ! 号换成 = 号,就表示匹配、正向。
我们经常用正则表达式来检测一个字符串中包含某个子串,要表示一个字符串中不包含某个字符或某些字符也很容易,用 [^...] 形式就可以了。要表示一个字符串中不包含某个子串(由字符序列构成)呢?
用 [^...] 这种形式就不行了,这时就要用到(负向【否定】)先行【向右】断言或后行【向左】断言、或同时使用。
例如判断一句话中包含 this,但不包含 that。
包含 this 比较好办,一句话中不包含 that,可以认为这句话中每个字符的前面都不是 that 或每个字符的后面都不是 that。正则表达式如下:
^((?<!that).)*this((?<!that).)*$
或
^(.(?!that))*this(.(?!that))*$
对于 this is runoob test 这句话,两个表达式都能够匹配成功,而 this and that is runoob test 都匹配失败。
在一般情况下,这两个表达式基本上都能够满足要求了。考虑极端情况,如一句话以 that 开头、以 that 结尾、that 和 this 连在一起时,上述表达式就可能不胜任了。 如 runoob thatthis is the case 或者 this is the case, not that 等。
只要灵活运用这几个断言,就很容易解决:
^(.(?<!that))*this(.(?<!that))*$
^(.(?<!that))*this((?!that).)*$
^((?!that).)*this(.(?<!that))*$
^((?!that).)*this((?!that).)*$
这 4 个正则表达式测试上述的几句话,结果都能够满足要求。
上述 4 种断言,括号里的 pattern 本身是一个正则表达式。但对 2 种后行【向左】断言有所限制,在 Perl 和 Python 中,这个表达式必须是定长(fixed length)的,即不能使用 *、+、? 等元字符,如 (?<=abc) 没有问题,但 (?<=a*bc) 是不被支持的,特别是当表达式中含有|连接的分支时,各个分支的长度必须相同。之所以不支持变长表达式,是因为当引擎检查后行断言时,无法确定要回溯多少步。Java 支持 ?、{m}、{n,m} 等符号,但同样不支持 *、+ 字符。Javascript 干脆不支持后行断言,不过一般来说,这不是太大的问题。
先行【向右】断言和后行【向左】断言某种程度上就好比使用 if 语句对匹配的字符前后做判断验证。
以下列出 ?=、?<=、?!、?<!= 的使用
exp1(?=exp2):查找 exp2 前面的 exp1。
(?<=exp2)exp1:查找 exp2 后面的 exp1。
exp1(?!exp2):查找后面不是 exp2 的 exp1。
(?<!=exp2)exp1:查找前面不是 exp2 的 exp1。
以上解释仅给与本人类似的理解能力的人参考。错误之处请指正。
原文链接:https://www.runoob.com/w3cnote/reg-lookahead-lookbehind.html
原文参考链接:https://blog.51cto.com/cnn237111/749047