PHP 正则表达式:《环视/预查询/断言/零宽断言》好文推荐

阅读数: 1261 2018年11月27日

推荐一篇不错的文章,值得收藏:http://www.zjmainstay.cn/regexp-lookaround

以下内容来自转载】个人理解部分做个红色笔记,用于复习查阅。文章尾部的方法值得学习借鉴。

环视,在不同的地方又称之为零宽断言,简称断言。 
用一句通俗的话解释: 
环视,就是先从全局环顾一遍正则,(然后断定结果,)再做进一步匹配处理(基于正则本身,环视前面或后面) 
断言,就是先从全局环顾一遍正则,然后断定结果,再做进一步匹配处理。

两个虽然字面不一样,意思却是同一个,都是做全局观望,再做进一步处理。

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

环视主要有以下4个用法: 
(?<=exp) 匹配前面是exp的数据 
(?<!exp) 匹配前面不是exp的数据 
(?=exp) 匹配后面是exp的数据 
(?!exp) 匹配后面不是exp的数据

示例四: 
(?<=B)AAA 匹配前面是B的数据,即BAAA匹配,而CAAA不匹配 
(?<!B)AAA 匹配前面不是B的数据,即CAAA匹配,而BAAA不匹配 
AAA(?=B) 匹配后面是B的数据,即AAAB匹配,而AAAC不匹配 
AAA(?!B) 匹配后面不是B的数据,即AAAC能匹配,而AAAB不能匹配

另外,还会看到(?!B)[A-Z]这种写法,其实它是[A-Z]范围里,排除B的意思,前置的(?!B)只是对后面数据的一个限定,从而达到过滤匹配的效果。

因此,环视做排除处理是比较实用的,比如,示例五:

需求:字母、数字组合,不区分大小写,不能纯数字或者纯字母,6-16个字符。
通用正则:^[a-z0-9]{6,16}$    字母数字组合,6-16个字符
    排除纯字母:(?!^[a-z]+$)
排除纯数字:(?!^[0-9]+$)
    组合起来:(?!^[a-z]+$)(?!^[0-9]+$)^[a-z0-9]{6,16}$

注意,环视部分是不占宽度的,所以有零宽断言的叫法。 
所谓不占宽度,可以分成两部分理解: 
1、环视的匹配结果不纳入数据结果 
2、环视它匹配过的地方,下次还能用它继续匹配。

如果不是环视,则匹配过的地方,不能再匹配第二次了。

上面示例四体现了:环视的匹配结果不纳入数据结果,它的结果:

(?<=B)AAA     源串:BAAA  结果:AAA(?<!B)AAA     源串:CAAA  结果:AAAAAA(?=B)      源串:AAAB  结果:AAAAAA(?!B)      源串:AAAC  结果:AAA

而示例五体现了:环视它匹配过的地方,下次还能用它继续匹配 
因为,整个匹配过程中,正则表达式一共走了3次字符串匹配,第一次匹配不全部是字母,第二次匹配不全部是数字,第三次匹配全部是字母数字组合,6-16个字符。

扩展部分:`[A-Z](?<=B)`   [A-Z]范围等于B`[A-Z](?<!B)`   [A-Z]范围排除B`(?!B)[A-Z]`    [A-Z]范围排除B

附: js不支持(?<=exp)  (?<!exp) 语法


二、环视的类型

环视的类型有两类:

(一)肯定和否定

1、肯定:(?=exp)  (?<=exp) 
2、否定:(?!exp)  (?<!exp)

(二)顺序和逆序

1、顺序:(?=exp)  (?!exp) 
2、逆序:(?<=exp)  (?<!exp)

· 两种类型名称组合

1、肯定顺序:(?=exp) 
2、否定顺序:(?!exp) 
3、肯定逆序:(?<=exp) 
4、否定逆序:(?<!exp)

· 四种组合的用法

四种组合,根据正则与环视位置的不同,又可以组合出来8种不同的摆放方式。 
一般来说,顺序的环视,放在正则后面,认为是常规用法,而放在正则前面,对正则本身的匹配起到了限制,则认为是变种的用法 
而逆序的环视,常规用法是环视放在正则前面,变种用法是放在正则后面。 
总结一句话就是:常规用法,环视不对正则本身做限制;变种用法会限制正则本身,而不能当做环视了
但是,无论常规和变种,都是非常常见的用法。


四种组合正则与环视的摆放位置

1、肯定顺序常规: [a-z]+(?=;)           字母序列后面跟着;2、肯定顺序变种: (?=[a-z]+$).+$        字母序列3、肯定逆序常规: (?<=:)[0-9]+          :后面的数字4、肯定逆序变种: \b[0-9]\b(?<=[13579]) 0-9中的奇数5、否定顺序常规: [a-z]+\b(?!;)     不以;结尾的字母序列6、否定顺序变种: (?!.*?[lo0])\b[a-z0-9]+\b  不包含l/o/0的字母数字系列7、否定逆序常规: (?<!age)=([0-9]+) 参数名不为age的数据8、否定逆序变种: \b[a-z]+(?<!z)\b  不以z结尾的单词

下面示例,仅对肯定顺序环视进行两种用法的讲解,其他组合都有类似用法,读者参考上面列举8种位置用法自行测试。

1、肯定顺序:(?=exp)

(1)常规用法

所谓常规用法,主要指正则匹配部分位于肯定顺序环视左侧,如:test(?=\.php),用于匹配后缀是.php的test文件。

示例四:肯定顺序环视常规用法

源字符串:

notexefile1.txtexefile1.exeexefile2.exeexefile3.exenotexefile2.phpnotexefile3.sh

需求:获取.exe后缀文件不含后缀的文件名 
正则:.+(?=\.exe) 
结果:

exefile1exefile2exefile3

示例中,因为要获取.exe后缀不含后缀的文件名,因此,在不使用分组进行捕获的时候,我们利用了肯定顺序型环视的限定,达到了既限定为.exe后缀又不被捕获进匹配结果的效果,充分展示了环视不占位的特性。

(2)变种用法

所谓变种用法,主要指正则匹配部分位于肯定顺序环视右侧,匹配内容收到环视条件的限定,如:^(?=[a-z]+$).+,虽然后面用的是.+.除了不能匹配换行,能匹配任意字符),但是,这个表达式只能匹配一个以上的a-z字母组合,因为它被前面的环视限制了匹配范围。

示例五:肯定顺序环视变种用法

需求:必须包含字母(不区分大小写)、数字,6-16位密码 
正则:^(?=.*?[a-zA-Z])(?=.*?[0-9])[a-zA-Z0-9]{6,16}$ 
测试用例:

#量词条件:1. 小于62. 6-16(关注边界值)3. 大于16#字符条件:1. 纯数字2. 纯英文3. 数字+英文4. 英文+数字5. 英文数字乱序混合注:每类字符条件都要考虑量词条件

示例中,使用(?=.*?[a-zA-Z])限定后面的字符中至少有一个字母(此时不能当做环视来使用),使用(?=.*?[0-9])限定后面的字符中至少有一个数字,最后通过实际匹配正则[a-zA-Z0-9]{6,16}限定量词。此示例,同样提现了环视不占位的特性,否则的话,第一个环视消耗完字符,会导致后面匹配失败,而实际并没有,因为环视不消耗匹配字符。

2、否定顺序:(?!exp)

示例六:否定顺序环视

源字符串:

notexefile1.txtexefile1.exeexefile2.exeexefile3.exenotexefile2.phpnotexefile3.sh

需求:获取不是.exe后缀文件不含后缀的文件名 
正则:(.+)(?!\.exe)\.[^.]+$ 
结果:

notexefile1notexefile2notexefile3

首先,拿到这个需求,看过前面肯定顺序环视例子的写法,我们很可能一下子写出来.+(?!\.exe),但是测试之后却发现,错了!为什么?一万个为什么飘过~~~ 
为什么匹配错误,这涉及到正则匹配的原理,匹配过程如下: 
为了解释方便,这里以多行模式进行讲解。 
正则.+:因为没有指定位置,从每行字符串开始位置开始匹配,.+是贪婪模式,尽可能多匹配,而且是匹配除了换行外的任意字符,因此,以第一行为例,.+匹配到notexefile1.txt,匹配位置移动到字符串最后。 
正则(?!\.exe):匹配字符串结束位置,不是.exe,成功,匹配结束。 
匹配结果得到:notexefile1.txt 
其他几行匹配过程是类似的,我们发现每行它都匹配上了,这不是我们预期的结果。

为了得到预期的结果,我们需要在环视限定的条件下,把后缀部分消耗掉,同时利用否定顺序环视限定其不能是.exe后缀,然后用分组获取文件名,得到表达式:(.+)(?!\.exe)\.[^.]+$。这个表达式的匹配过程,跟上面其实是类似的,只不过因为表达式没有匹配完成,导致了回溯,回溯让出了后缀部分给\.[^.]+去匹配了。

如何写出这个正则:在写这个正则的过程中,我们可以先写出(.+)\.[^.]+$这样的正则,然后在再后缀位置插入环视限定,从而得到目标正则(.+)(?!\.exe)\.[^.]+$

由于回溯过程涉及步骤过多,这里就不做展开,后面有机会再写一个关于正则回溯的文章,现在大家可以打开这个否定顺序匹配与回溯演示页,分别查看3个版本的debug情况。 
选择版本:在正则输入框上面的下拉菜单里 
查看debug:左侧TOOLS区域的Regex Debugger菜单。 
注:由于该站jquery引用自谷歌,因此需要翻墙加载才可以打开

当然也可以用Regexbuddy的Debug功能,这个可以参考《正则表达式工具RegexBuddy使用教程》查看Debug用法。

三个版本的正则都是(.+)(?!\.exe)\.[^.]+$ 
源字符串分别是: 
1. 测试示例六,使用示例六源字符串 
2. 测试匹配成功情况回溯,源字符串

notexefile1.txt

3. 测试匹配失败情况回溯,源字符串

exefile1.exe

3、肯定逆序:(?<=exp)

(1)肯定逆序环视和否定逆序环视在一些语言中是不支持的,如JavaScript就不支持,大家在使用过程中需要注意一下。 
(2)很多语言不支持非确定长度的逆序环视。所谓非确定长度,是指逆序环视部分内容,不是固定长度的,如(?<=.*;)abc,这里用的.*就是不固定的长度。无论是分支情况还是什么,逆序环视部分需要固定长度。 
(3)有些语言里,支持特定范围的非确定长度,这个是指(?<=.{0,100};)abc这种,本来的.*使用0-100这样的限定最大长度为100的范围值。 
因此,大家使用过程中可以根据自己使用语言的差异,测试使用。

示例七:肯定逆序环视

源字符串:

name=Zjmainstayage=26

需求:获取name参数的值 
正则:(?<=name=).+

示例很直白,前面必须是name=,然后获取其后面的数据,由于环视不占位,因此并没有出现在匹配结果中。

4、否定逆序:(?<!exp)

示例八:否逆序环视

源字符串:

name=Zjmainstayage=26

需求:获取不是name参数的值 
正则:^[^=]+=(?<!name=)(.+)

跟否定顺序示例一样,我们不能直接用(?<!name=).+进行匹配,正则做法是先把参数部分匹配出来,再用否定逆序环视对它进行限定,限定它不能是name=,因此实现匹配。

讲到这里,你们是否能想到前面否定顺序示例六中,可以用否定逆序来做? 
正则:(.+)\.[^.]+(?<!\.exe)$

因此,几个环视组合,由于正则所摆放的位置不同,可以产生等价的效果。

附: js不支持(?<=exp)  (?<!exp) 语法

三、环视的应用

环视一直是正则表达式使用过程中的难题,主要体现在它的不占位(不消耗匹配字符)但起限定作用、肯定和否定、顺序和逆序区分、摆放位置不同如何理解等概念上。经过上面的讲解,相信读者已经对这几个概念有了深刻的理解,但是,理解概念跟灵活运用是两码事。 
接下来我们再举几个平时常用的例子,帮助大家理解并掌握,达到灵活运用的程度。

示例九:正则分块组合法-必须包含字母、数字、特殊字符

正则:^(?=.*?[a-z])(?=.*?\d)(?![a-z\d]+$).+$ 
解析: 
(?=.*?[a-z])限制必须有字母(限定后面的正则本身必须符合这个规则,限定正则本身)
(?=.*?\d)限制必须有数字 (限定后面的正则本身必须符合这个规则,限定正则本身)
(?![a-z\d]+$)限制从开头到结尾不能全为数字和字母 
.+在没有限定的情况下可以是任意字符 
^$ 限定字符串的开头和结尾 
组合起来就可以得到上面正则。

示例十:正则逐步完善法-排除特定标签p/a/img,匹配html标签

正则: /]+)[^>]*/?> 
解析: 
常见的标签格式有:

      //无属性值 //有属性值[object Object]     //有属性值自闭合
          //无属性值自闭合

首先,从简单标签入手,对于


,写出正则: 
]*/?> 
由于[^>]通配符的匹配访问太大,因此,实际上无论有没有属性值,都被上面表达式给匹配了,这个没关系,我们通过进一步细化匹配通配符,缩小匹配范围。 
我们观察可得,标签名是这样得到的:

无属性值:          ]+)>

得到正则:

 /]+)

用这部分代替前面通配正则的标签名部分,得到: 
/]+)[^>]*/?> 
最后,我们需要排除p/a/img标签,用否定顺序法,在标签名前面加入否定环视: 
/]+)[^>]*/?> 
大功告成,这是我们要的结果!

此示例的正则逐步完善法是正则书写过程中常用方法,倒推回去也是可行的,比如,假如我们拿到一段很长的正则,而它的匹配结果是错误的,我们该怎么做? 
我们可以用逐步截断的方法,一步步的减除掉右侧的一部分,直到它恢复匹配,我们就知道刚刚被减除掉的部分正则是有问题的,观察它为什么导致错误,修改正确,再逐步恢复后面减除的正则即可。

示例十一:正则减除查错法-匹配异常原因查找

源字符串:

    item1    item2    item3    item4    item5

正则:

  • (.*?)

 
减除排错过程: 
例子比较简单,主要演示思路过程。 
用上面的正则去匹配源字符串,我们发现,明明预期5个结果,但是却得到了4个,因此,我们开始进行减除正则排错。 
1. 减除右侧,此时正则

  • (.*?)

 匹配4个 
2. 减除右侧(.*?),此时正则


,匹配4个 
3. 减除"item">,此时正则<li class=匹配5个 
4. 恢复"item">,减除>,此时正则<li class="item",匹配4个 
5. 减除",此时正则<li class="item匹配5个 
至此,观察发现item后面还有其他可能,补充兼容: 
6. 修复得正则<li class="item[^"]*" 
7. 逐步把前面减除的"后面部分补充回来,此时正则<li class="item[^"]*">(.*?)匹配5个 
问题解决!

总结

文章至此,已经完整讲解了正则表达式环视的概念与用法,读者从中能够了解到正则的逐步匹配原理,消耗与不消耗匹配字符原理,环视的不占位概念,环视作为一个虚拟位置限定其前后匹配的概念,环视肯定和否定类型与顺序和逆序类型的概念,以及各种概念原理的运用,最后还附带了正则书写过程中运用的分块组合法、逐步完善法和减除查错法,希望能够帮助广大读者更加深刻地理解正则表达式,达到灵活运用的程度。

更多关于正则表达式入门的内容,请参考本站博客《我眼里的正则表达式入门教程 
更多关于正则表达式高级的内容,请参考本站博客《深入理解正则表达式高级教程 
Windows正则表达式测试工具请从《正则表达式测试工具RegexBuddy-4.1.0》下载 
Mac正则表达式测试工具请从《Mac正则表达式测试工具》下载


参考资料
转载:http://www.zjmainstay.cn/regexp-lookaround
phpriji.cn | 网站地图 | 沪ICP备17015433号-1