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

    文章大纲:

        深入理解正则表达式环视的概念与用法
            一、环视的概念
                (一)环视概念与匹配过程示例
                    示例一:简单环视匹配过程
                (二)什么是消耗正则的匹配字符?
                    示例二:一次匹配消耗匹配字符匹配过程
                    示例三:多次匹配消耗匹配字符匹配过程
            二、环视的类型
                (一)肯定和否定
                (二)顺序和逆序
                · 两种类型名称组合
                · 四种组合的用法
                    四种组合正则与环视的摆放位置
                    1、肯定顺序:(?=exp)
                        (1)常规用法
                            示例四:肯定顺序环视常规用法
                        (2)变种用法
                            示例五:肯定顺序环视变种用法
                    2、否定顺序:(?!exp)
                            示例六:否定顺序环视
                    3、肯定逆序:(?<=exp)
                        示例七:肯定逆序环视
                    4、否定逆序:(?<!exp)
                        示例八:否逆序环视
            三、环视的应用
                示例九:正则分块组合法-必须包含字母、数字、特殊字符
                示例十:正则逐步完善法-排除特定标签p/a/img,匹配html标签
                示例十一:正则减除查错法-匹配异常原因查找
            总结
    

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

    一、环视的概念

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

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

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

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

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

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

    (1)常规用法

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

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

    源字符串:

    notexefile1.txt
    exefile1.exe
    exefile2.exe
    exefile3.exe
    notexefile2.php
    notexefile3.sh
    

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

    exefile1
    exefile2
    exefile3
    

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

    (2)变种用法

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

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

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

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

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

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

    示例六:否定顺序环视

    源字符串:

    notexefile1.txt
    exefile1.exe
    exefile2.exe
    exefile3.exe
    notexefile2.php
    notexefile3.sh
    

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

    notexefile1
    notexefile2
    

    首先,拿到这个需求,看过前面肯定顺序环视例子的写法,我们很可能一下子写出来.+(?!.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=Zjmainstay
    age=26
    

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

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

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

    示例八:否逆序环视

    源字符串:

    name=Zjmainstay
    age=26
    

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

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

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

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

    三、环视的应用

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

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

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

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

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

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

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

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

    得到正则:

    </?([^> /]+)
    

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

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

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

    源字符串:

    <ul>
        <li class="item">item1</li>
        <li class="item">item2</li>
        <li class="item bug">item3</li>
        <li class="item">item4</li>
        <li class="item">item5</li>
    </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个
      问题解决!

    总结

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


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

    文章首发自Zjmainstay学习笔记《深入理解正则表达式环视的概念与用法

  • 相关阅读:
    108. Convert Sorted Array to Binary Search Tree
    How to check if one path is a child of another path?
    Why there is two completely different version of Reverse for List and IEnumerable?
    在Jenkins中集成Sonarqube
    如何查看sonarqube的版本 how to check the version of sonarqube
    Queue
    BFS广度优先 vs DFS深度优先 for Binary Tree
    Depth-first search and Breadth-first search 深度优先搜索和广度优先搜索
    102. Binary Tree Level Order Traversal 广度优先遍历
    How do I check if a type is a subtype OR the type of an object?
  • 原文地址:https://www.cnblogs.com/Zjmainstay/p/regexp-lookaround.html
Copyright © 2011-2022 走看看