zoukankan      html  css  js  c++  java
  • 深入理解正则表达式环视的概念与用法

    在《深入理解正则表达式高级教程-环视》中已经对环视做了简单的介绍,但是,可能还有一些读者比较迷惑,今天特意以专题的形式,深入探讨一下正则表达式的环视的概念与用法。

     

     

    一、环视的概念

    环视,在不同的地方又称之为零宽断言,简称断言。 
    环视强调的是它所在的位置,前面或者后面,必须满足环视表达式中的匹配情况,才能匹配成功。 
    环视可以认为是虚拟加入到它所在位置的附加判断条件,并不消耗正则的匹配字符。

     

    (一)环视概念与匹配过程示例

     

    示例一:简单环视匹配过程

    例如,对于源字符串ABC,正则(?=A)[A-Z]匹配的是: 
    1. (?=A)所在的位置,后面是A 
    2. 表达式[A-Z]匹配A-Z中任意一个字母 
    根据两个的先后位置关系,组合在一起,那就是: 
    (?=A)所在的位置,后面是A,而且是A-Z中任意一个字母,因此,上面正则表达式匹配一个大写字母A

    从例子可以看出,从左到右,正则分别匹配了环视(?=A)[A-Z],由于环视不消耗正则的匹配字符,因此,[A-Z]还能对A进行匹配,并得到结果。

     

    (二)什么是消耗正则的匹配字符?

    在《深入理解正则表达式高级教程》里我们已经讲过,正则是按照单个字符来进行匹配的,一般情况下是从左到右,逐个匹配源字符串中的内容。

     

    示例二:一次匹配消耗匹配字符匹配过程

    例如,对于源字符串ABCAD,正则A[A-Z]匹配的过程是: 
    1. 正则A:因为没有位置限定,因此是从源字符串开始位置开始,也就是正则里的^,这个^是虚拟字符,表示匹配字符串开始位置,也就是源字符串ABCAD里的A前面的位置,因为正则A能够匹配源字符串A,匹配成功,匹配位置从源字符串^的位置后移一位,到达A后面,即此时源字符串ABCADA这个字符已经被消耗,接下来的正则匹配从A后面开始。 
    2. 正则[A-Z]:当前匹配位置为第一个A字母后面位置,正则[A-Z]对源字符串ABCAD里的B字母进行匹配,匹配成功,位置后移到B字母后面的位置。至此,由于正则已经匹配完成,因此,正则A[A-Z]匹配结果是AB

    我们知道,有些语言如js支持g模式修饰符,也就是全局匹配,那么,上面例子中,正则匹配1次成功之后,将会从匹配成功位置(B字母后面位置)开始,再从头进行匹配一次正则,直到源字符串全部消耗完为止。

     

    示例三:多次匹配消耗匹配字符匹配过程

    因此,全局匹配的过程补充如下: 
    3. 正则A:当前匹配位置为B字母后面位置,正则A去匹配源字符串中的C,匹配失败,匹配位置后移一位,此时C被消耗了。 
    4. 正则A:当前匹配位置为C字母后面位置,正则A去匹配源字符串中的第二个A字母,匹配成功,匹配位置后移一位,此时A被消耗了。 
    5. 正则[A-Z]:当前匹配位置为第二个A字母后面位置,正则[A-Z]对源字符串ABCAD里的D字母进行匹配,匹配成功,位置后移到D字母后面的位置,此时D被消耗了。 
    6. 由于正则里还有个源字符串结束位置,也就是正则里的$,这个$也是虚拟字符,因此,还要继续进行匹配: 
    正则A:当前匹配位置为D字母后面的位置,正则A去匹配源字符串的结束位置,匹配失败,匹配结束。

    最终匹配结果是ABAD

     

    二、环视的类型

    环视的类型有两类:

     

    (一)肯定和否定

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

     

    (二)顺序和逆序

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

     

    · 两种类型名称组合

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

     

    · 四种组合的用法

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

     

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

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

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

     

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

     
    (1)常规用法

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

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

    源字符串:

     
    1. notexefile1.txt
    2. exefile1.exe
    3. exefile2.exe
    4. exefile3.exe
    5. notexefile2.php
    6. notexefile3.sh

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

     
    1. exefile1
    2. exefile2
    3. exefile3

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

     
    (2)变种用法

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

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

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

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

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

     

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

     
    示例六:否定顺序环视

    源字符串:

     
    1. notexefile1.txt
    2. exefile1.exe
    3. exefile2.exe
    4. exefile3.exe
    5. notexefile2.php
    6. notexefile3.sh

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

     
    1. notexefile1
    2. notexefile2

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

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

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

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

    当然也可以用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的范围值。 
    因此,大家使用过程中可以根据自己使用语言的差异,测试使用。

     
    示例七:肯定逆序环视

    源字符串:

     
    1. name=Zjmainstay
    2. age=26

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

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

     

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

     
    示例八:否逆序环视

    源字符串:

     
    1. name=Zjmainstay
    2. age=26

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

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

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

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

     

    三、环视的应用

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

     

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

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

     

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

    正则:</?(?!p|a|img)([^> /]+)[^>]*/?> 
    解析: 
    常见的标签格式有:

     
    1. <p>...</p> //无属性值
    2. <p class="t"....>...</p> //有属性值
    3. <img ..../> //有属性值自闭合
    4. <br/> //无属性值自闭合

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

     
    1. 无属性值:<p> <([^>]+)
    2. 有属性值:<p class <([^ ]+)
    3. 无属性值自闭合:<br/> <([^/]+)
    4. 闭合标签:</p> </([^>]+)>

    得到正则:

     
    1. </?([^> /]+)

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

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

     

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

    源字符串:

     
    1. <ul>
    2. <li class="item">item1</li>
    3. <li class="item">item2</li>
    4. <li class="item bug">item3</li>
    5. <li class="item">item4</li>
    6. <li class="item">item5</li>
    7. </ul>

    正则:<li class="item">(.*?)</li> 
    减除排错过程: 
    例子比较简单,主要演示思路过程。 
    用上面的正则去匹配源字符串,我们发现,明明预期5个结果,但是却得到了4个,因此,我们开始进行减除正则排错。 
    1. 减除右侧</li>,此时正则<li class="item">(.*?) 匹配4个 
    2. 减除右侧(.*?),此时正则<li class="item">,匹配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[^"]*">(.*?)</li>,匹配5个 
    问题解决!

  • 相关阅读:
    逆元
    C++快读
    最长单调上升子序列(LIS) O(nlogn)求法
    【简●解】巴厘岛的雕塑
    【简●解】学校食堂
    【简●解】[HNOI2005]星际贸易
    差分约束系统小结
    【简•解】花园
    最小生成树小结
    概率及期望DP小结
  • 原文地址:https://www.cnblogs.com/tsql/p/5860889.html
Copyright © 2011-2022 走看看