零宽断言 python_【正则表达式系列】零宽断言

前言

正则表达式中,有一个绕不过去的坎,那就是零宽断言

零宽断言是一种零宽度的匹配,它匹配的内容不会保存到匹配结果中,也不会占用index宽度,最终匹配的结果只是一个位置

简单的说,它用于查找在某些内容之前或之后的东西(但返回结果并不包括这些内容)

JavaScript中只支持零宽先行断言

简介

零宽断言分为4类

正向零宽先行断言(?=exp)

目标字符出现的位置的右边必须匹配到exp这个表达式

负向零宽先行断言(?!exp)

目标字符出现的位置的右边不能匹配到exp这个表达式

正向零宽后发断言(?<=exp)

目标字符出现的位置的左边必须匹配到exp这个表达式

负向零宽后发断言(?

目标字符出现的位置的左边不能匹配到exp这个表达式

注,关于先行和后发,还有其它称呼,譬如前瞻和后瞻等,本文统一使用先行与后发

JavaScript中的断言

JavaScript语言内只支持零宽先行断言(即只支持?=exp和?!exp)

所以本文中只会介绍零宽先行断言

另外,可以通过RegexBuddy 4等工具分析正则的匹配过程

大纲

正向零宽先行断言

负向零宽先行断言

实战演练

正向零宽先行断言

示例1

var str="abcdefg";

var reg=/ab(?=cd)/;

str.match(reg); // ["ab", index: 0, input: "abcdefg"]

index = 0,a匹配a成功,尝试b匹配b成功

继续尝试匹配,(?=cd)接管控制权

接下来(?=cd)依次尝试匹配c和d成功

此时所有的正则表达式匹配完毕,匹配成功,返回成功结果

示例2

var str="abcdefg";

var reg=/(?=cd)efg/;

str.match(reg); // null

想要达到的效果是匹配在cd后方的efg,但是这是零宽后发断言才有的效果(?<=exp),而JS中并不支持,此时使用先行断言,实际效果为

?=cd获取控制权,一直到index = 2时才匹配成功,接下来e获取控制权

可惜由于?=cd是零宽式的,因此匹配成功后,下一轮匹配依然从index = 2开始尝试,此时c匹配e失败,于是index挪到3

接下来又是d, e, f, g匹配?=cd失败,于是最终匹配失败,返回null

示例3

var str="abcdefg";

var reg=/(?=cd)cdefg/;

str.match(reg); // ["cdefg", index: 2, input: "abcdefg"]

基于示例2的变形

在index = 2时,?=cd匹配成功了,交给cdefg

index = 2,此时刚好c匹配,继续吃进d,e,f,g也都匹配,于是匹配成功,于是返回成功结果

示例4

var str="abcdefg";

var reg=/ab(?=cd)cdefg/;

str.match(reg); // ["abcdefg", index: 0, input: "abcdefg"]

上述示例的综合

index = 0时,左侧的ab匹配成功

继续往后尝试(index = 2处),?=cd也匹配成功

接下来继续从index = 2开始尝试,c, d, e ,f , g依次匹配成功,于是匹配结束,返回成功结果(index = 0,因为没有失败,后续的尝试都成功了)

注,零宽断言返回的是位置而不是字符,零宽断言匹配成功后,其余表达式会基于这个返回的位置继续判断

另外,请不要把先行断言当成后发断言来用

负向零宽先行断言

示例1

var str="abcdefg";

var reg=/ab(?!cd)/;

str.match(reg); // null

在ab匹配成功后,接下来cd匹配?!cd失败

接下来的b, c, d, e, f, g依次都匹配a失败,于是最终匹配失败,返回null

示例2

var str="abcdefg";

var reg=/ab(?!ab)cd/;

str.match(reg); // ["abcd", index: 0, input: "abcdefg"]

在ab匹配成功后,接下来cd匹配?!ab成功

由于?!ab是零宽的,因此接下来仍然从index = 2处尝试(也就是c继续匹配c),因此匹配成功,接下来d也匹配d成功,所有表达式匹配完毕,最终返回成功结果(index = 0,因为没有失败)

实战演练

接下来一些实战练习,加深印象

找到文本中的所有ing单词的前缀

需求说明

例如: I am reading in the dining room的匹配结果应该是read与din

代码

var str="I am reading in the dining room";

var reg=/\w+(?=ing)/g;

str.match(reg); // ["read", "din"]

说明

\w+匹配至少一个以上的单词

?=ing代表右侧必须有ing,但是匹配的结果又不包含ing

g是全局匹配

因此符合了需求,成功匹配

测试一个文件是否是.css后缀,但又不能是.min.css

需求说明

这道题曾多次出现在各大平台,基本都是依靠零宽断言来检测,例如:

test('a.min.css'); // false

test('b.css'); // true

test('c.mining.css'); // true

代码

var reg=/^(?!.*\.min\.css$).+\.css$/;

reg.test('a.min.css'); // false

reg.test('.min.css'); // false

reg.test('.css'); // false

reg.test('min.css'); // true

reg.test('b.css'); // true

reg.test('c.mining.css'); // true

说明

由于只考虑单个文件名的匹配,所以较简单

先用?!.*\.min\.css负向先行断言试探文件名。这一步匹配完后,直接就排除了xxx.min.css了(由于是*,所以.min.css也会匹配失败)

然后继续用\w+.*\.css匹配xxx.css这种情况

于是就符合了要求,只匹配.css但不匹配.min.css

另外,关于回溯步骤,由于较多,这里就不赘述了,感兴趣的可以用RegexBuddy等工具自行检测

找到所有的.min.css文件的文件名

需求说明

例如: a.min.css;.min.css;.css;min.css;b.css;c.min.js;d.css;e.a.min.css(文件以;隔开)的匹配结果应该是a与e.a

代码

var str="a.min.css;.min.css;.css;min.css;b.css;c.min.js;d.css;e.a.min.css";

var reg=/\w+[^;]*(?=\.min\.css)/g;

str.match(reg); // ["a", "e.a"]

说明

这类型表达式回溯次数很多,实际中可以有更好的解决方案,比如先分割,再匹配

左侧的表达式\w+[ ^;]*确保了必须是一个正常的单词开头,并且不能包括;,所以直接排除了名字以.开头或名字中包含;的情况

右侧的零宽断言?=\.min\.css确保名字右侧必须有.min.css

于是最终只剩下了a与e.a符合情况

找到所有.css文件的文件名,需要排除.min.css

需求说明

这道题基于上两题的综合与变形,增加了点难度(不再是单个文件名匹配,而是字符串中的文件名提取)

例如: a.min.css;.min.css;.css;min.css;b.css;c.min.js;d.css;e.a.min.css;f.min.a.css(文件以;分割)的匹配结果应该是min、b、d和f.min.a

代码

var str = "a.min.css;.min.css;.css;min.css;b.css;c.min.js;d.css;e.a.min.css;f.min.a.css";

var reg1 = /[^;]+(?=\.css)/g;

var match1 = str.match(reg1);

var reg2 = /\.min$/;

var match2 = [];

match1 && match1.map(function(item, index) {

!reg2.test(item) && match2.push(item);

});

console.log(match1); // ["a.min", ".min", "min", "b", "d", "e.a.min", "f.min.a"]

console.log(match2); // ["min", "b", "d", "f.min.a"]

说明

好吧,我承认无法只靠一个表达式实现这个功能(不知道在座的各位有谁可以的...)

第一步[ ^;]+(?=\.css)先匹配所有的.css后缀的名字

第二步\.min$剔除以.min结尾的名字

于是就只剩下了正常的结果

或者先分割,然后再匹配(是.css但非.min.css)也行

PS:本来准备一步解决作为压轴的,但是尝试了很久都未果,最终还是拆分来实现的,之所以仍然放在最后,也算是给自己一个警醒

写在最后的话

深入研究后,才发现精通正则表达式真的很难,很多时候,你认为的已经精通了只是一种假象。

因此,还是放下身段,努力学习吧!

附录

博客

初次发布2017.07.26于个人博客

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
正则表达式断言是用来匹配某个位置前后是否满足特定的条件,但不会消耗实际的字符。在Python中,有两种类型的断言:肯定断言和否定断言。 肯定断言分为前向肯定断言和后向肯定断言。前向肯定断言表示匹配的字符串前面是pattern匹配的内容时才匹配,语法为(?<=pattern)。后向肯定断言表示匹配的字符串后面是pattern匹配的内容时才匹配,语法为(?=pattern)。注意,前向肯定断言必须写在要匹配的正则表达式的前面,而后向肯定断言必须写在要匹配的字符串的后面。此外,前向肯定断言中的正则表达式必须是能确定长度的正则表达式,不能是不确定个数的正则模式符(如\w*、\w、\w?等)。 否定断言也分为前向否定断言和后向否定断言。前向否定断言表示不匹配的字符串前面是pattern匹配的内容时才匹配,语法为(?!=pattern)。后向否定断言表示匹配的字符串后面不是pattern匹配的内容时才匹配,语法为(?!pattern)。和前向肯定断言类似,前向否定断言中的正则表达式也必须是能确定长度的正则表达式。 使用断言可以在正则表达式中增加更灵活的匹配条件,提高匹配的准确性。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [python正则表达式断言](https://blog.csdn.net/weixin_37345015/article/details/111996899)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [Python正则中的断言](https://blog.csdn.net/stzhuce/article/details/121376188)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值