深入理解正则表达式环视的概念与用法
wvyb2788
8年前
<h2>文章大纲:</h2> <pre> 深入理解正则表达式环视的概念与用法 一、环视的概念 (一)环视概念与匹配过程示例 示例一:简单环视匹配过程 (二)什么是消耗正则的匹配字符? 示例二:一次匹配消耗匹配字符匹配过程 示例三:多次匹配消耗匹配字符匹配过程 二、环视的类型 (一)肯定和否定 (二)顺序和逆序 · 两种类型名称组合 · 四种组合的用法 四种组合正则与环视的摆放位置 1、肯定顺序:(?=exp) (1)常规用法 示例四:肯定顺序环视常规用法 (2)变种用法 示例五:肯定顺序环视变种用法 2、否定顺序:(?!exp) 示例六:否定顺序环视 3、肯定逆序:(?<=exp) 示例七:肯定逆序环视 4、否定逆序:(?<!exp) 示例八:否逆序环视 三、环视的应用 示例九:正则分块组合法-必须包含字母、数字、特殊字符 示例十:正则逐步完善法-排除特定标签p/a/img,匹配html标签 示例十一:正则减除查错法-匹配异常原因查找 总结</pre> <p>在《 <a href="/misc/goto?guid=4959715468718415289" rel="nofollow,noindex">深入讲解正则表达式高级教程-环视</a> 》中已经对环视做了简单的介绍,但是,可能还有一些读者比较迷惑,今天特意以专题的形式,深入探讨一下正则表达式的环视的概念与用法。</p> <h2><strong>一、环视的概念</strong></h2> <p>环视,在不同的地方又称之为零宽断言,简称断言。</p> <p>环视强调的是它所在的位置,前面或者后面,必须满足环视表达式中的匹配情况,才能匹配成功。</p> <p>环视可以认为是虚拟加入到它所在位置的附加判断条件,并不消耗正则的匹配字符。</p> <h3><strong>(一)环视概念与匹配过程示例</strong></h3> <p><strong>示例一:简单环视匹配过程</strong></p> <p>例如,对于源字符串 ABC ,正则 (?=A)[A-Z] 匹配的是:</p> <ol> <li>(?=A) 所在的位置,后面是 A</li> <li>表达式 [A-Z] 匹配 A-Z 中任意一个字母<br> 根据两个的先后位置关系,组合在一起,那就是:<br> (?=A) 所在的位置,后面是 A ,而且是 A-Z 中任意一个字母,因此,上面正则表达式匹配一个大写字母 A 。</li> </ol> <p>从例子可以看出,从左到右,正则分别匹配了环视 (?=A) 和 [A-Z] ,由于环视不消耗正则的匹配字符,因此, [A-Z] 还能对 A 进行匹配,并得到结果。</p> <h3><strong>(二)什么是消耗正则的匹配字符?</strong></h3> <p>在《深入讲解正则表达式高级教程》里我们已经讲过,正则是按照单个字符来进行匹配的,一般情况下是从左到右,逐个匹配源字符串中的内容。</p> <p><strong>示例二:一次匹配消耗匹配字符匹配过程</strong></p> <p>例如,对于源字符串 ABCAD ,正则 A[A-Z] 匹配的过程是:</p> <ol> <li>正则 A :因为没有位置限定,因此是从源字符串开始位置开始,也就是正则里的 ^ ,这个 ^ 是虚拟字符,表示匹配字符串开始位置,也就是源字符串 ABCAD 里的 A 前面的位置,因为正则 A 能够匹配源字符串 A ,匹配成功,匹配位置从源字符串 ^ 的位置后移一位,到达 A 后面,即此时源字符串 ABCAD 的 A 这个字符已经被消耗,接下来的正则匹配从 A 后面开始。</li> <li>正则 [A-Z] :当前匹配位置为第一个 A 字母后面位置,正则 [A-Z] 对源字符串 ABCAD 里的 B 字母进行匹配,匹配成功,位置后移到 B 字母后面的位置。至此,由于正则已经匹配完成,因此,正则 A[A-Z] 匹配结果是 AB 。</li> </ol> <p>我们知道,有些语言如js支持 g 模式修饰符,也就是全局匹配,那么,上面例子中,正则匹配1次成功之后,将会从匹配成功位置( B 字母后面位置)开始,再从头进行匹配一次正则,直到源字符串全部消耗完为止。</p> <p><strong>示例三:多次匹配消耗匹配字符匹配过程</strong></p> <p>因此,全局匹配的过程补充如下:</p> <ol> <li>正则 A :当前匹配位置为 B 字母后面位置,正则 A 去匹配源字符串中的 C ,匹配失败,匹配位置后移一位,此时 C 被消耗了。</li> <li>正则 A :当前匹配位置为 C 字母后面位置,正则 A 去匹配源字符串中的第二个 A 字母,匹配成功,匹配位置后移一位,此时 A 被消耗了。</li> <li>正则 [A-Z] :当前匹配位置为第二个 A 字母后面位置,正则 [A-Z] 对源字符串 ABCAD 里的 D 字母进行匹配,匹配成功,位置后移到 D 字母后面的位置,此时 D 被消耗了。</li> <li>由于正则里还有个源字符串结束位置,也就是正则里的 $ ,这个 $ 也是虚拟字符,因此,还要继续进行匹配:<br> 正则 A :当前匹配位置为 D 字母后面的位置,正则 A 去匹配源字符串的结束位置,匹配失败,匹配结束。</li> </ol> <p>最终匹配结果是 AB 和 AD 。</p> <h2><strong>二、环视的类型</strong></h2> <p>环视的类型有两类:</p> <h3><strong>(一)肯定和否定</strong></h3> <p>1、肯定: (?=exp) 和 (?<=exp)<br> 2、否定: (?!exp) 和 (?<!exp)</p> <h3><strong>(二)顺序和逆序</strong></h3> <p>1、顺序: (?=exp) 和 (?!exp)<br> 2、逆序: (?<=exp) 和 (?<!exp)</p> <h3><strong>· 两种类型名称组合</strong></h3> <p>1、肯定顺序: (?=exp)<br> 2、否定顺序: (?!exp)<br> 3、肯定逆序: (?<=exp)<br> 4、否定逆序: (?<!exp)</p> <h3><strong>· 四种组合的用法</strong></h3> <p>四种组合,根据正则与环视位置的不同,又可以组合出来8种不同的摆放方式。</p> <p>一般来说,顺序的环视,放在正则后面,认为是常规用法,而放在正则前面,对正则本身的匹配起到了限制,则认为是变种的用法。</p> <p>而逆序的环视,常规用法是环视放在正则前面,变种用法是放在正则后面。</p> <p>总结一句话就是:常规用法,环视不对正则本身做限制。</p> <p>但是,无论常规和变种,都是非常常见的用法。</p> <p>四种组合正则与环视的摆放位置</p> <pre> 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结尾的单词</pre> <p>下面示例,仅对肯定顺序环视进行两种用法的讲解,其他组合都有类似用法,读者参考上面列举8种位置用法自行测试。</p> <p><strong>1、肯定顺序: (?=exp)</strong></p> <p><strong>(1)常规用法</strong></p> <p>所谓常规用法,主要指正则匹配部分位于肯定顺序环视左侧,如: test(?=\.php) ,用于匹配后缀是 .php 的test文件。</p> <p><strong>示例四:肯定顺序环视常规用法</strong></p> <p>源字符串:</p> <pre> notexefile1.txt exefile1.exe exefile2.exe exefile3.exe notexefile2.php notexefile3.sh</pre> <p>需求:获取 .exe 后缀文件不含后缀的文件名</p> <p>正则:</p> <pre> <code>.+(?=\.exe)</code></pre> <p>结果:</p> <pre> exefile1 exefile2 exefile3</pre> <p>示例中,因为要获取 .exe 后缀不含后缀的文件名,因此,在不使用分组进行捕获的时候,我们利用了肯定顺序型环视的限定,达到了既限定为 .exe 后缀又不被捕获进匹配结果的效果,充分展示了环视不占位的特性。</p> <p>(2)变种用法</p> <p>所谓变种用法,主要指正则匹配部分位于肯定顺序环视右侧,匹配内容收到环视条件的限定,如: ^(?=[a-z]+$).+ ,虽然后面用的是 .+ ( . 除了不能匹配换行,能匹配任意字符),但是,这个表达式只能匹配一个以上的 a-z 字母组合,因为它被前面的环视限制了匹配范围。</p> <p><strong>示例五:肯定顺序环视变种用法</strong></p> <p>需求:必须包含字母(不区分大小写)、数字,6-16位密码</p> <p>正则:</p> <pre> <code>^(?=.*?[a-zA-Z])(?=.*?[0-9])[a-zA-Z0-9]{6,16}$</code></pre> <p>测试用例:</p> <pre> #量词条件: 1. 小于6 2. 6-16(关注边界值) 3. 大于16 #字符条件: 1. 纯数字 2. 纯英文 3. 数字+英文 4. 英文+数字 5. 英文数字乱序混合 注:每类字符条件都要考虑量词条件</pre> <p>示例中,使用 (?=.*?[a-zA-Z]) 限定后面的字符中至少有一个字母,使用 (?=.*?[0-9]) 限定后面的字符中至少有一个数字,最后通过实际匹配正则 [a-zA-Z0-9]{6,16} 限定量词。此示例,同样提现了环视不占位的特性,否则的话,第一个环视消耗完字符,会导致后面匹配失败,而实际并没有,因为环视不消耗匹配字符。</p> <p><strong>2、否定顺序: (?!exp)</strong></p> <p><strong>示例六:否定顺序环视</strong></p> <p>源字符串:</p> <pre> notexefile1.txt exefile1.exe exefile2.exe exefile3.exe notexefile2.php notexefile3.sh</pre> <p>需求:获取不是 .exe 后缀文件不含后缀的文件名</p> <p>正则:</p> <pre> <code>(.+)(?!\.exe)\.[^.]+$</code></pre> <p>结果:</p> <pre> notexefile1 notexefile2</pre> <p>首先,拿到这个需求,看过前面肯定顺序环视例子的写法,我们很可能一下子写出来 .+(?!\.exe) ,但是测试之后却发现, <strong>错了!</strong> 为什么?一万个为什么飘过~~~</p> <p>为什么匹配错误,这涉及到正则匹配的原理,匹配过程如下:</p> <p>为了解释方便,这里以多行模式进行讲解。</p> <p>正则 .+ :因为没有指定位置,从每行字符串开始位置开始匹配, .+ 是贪婪模式,尽可能多匹配,而且是匹配除了换行外的任意字符,因此,以第一行为例, .+ 匹配到 notexefile1.txt ,匹配位置移动到字符串最后。</p> <p>正则 (?!\.exe) :匹配字符串结束位置,不是 .exe ,成功,匹配结束。</p> <p>匹配结果得到: notexefile1.txt</p> <p>其他几行匹配过程是类似的,我们发现每行它都匹配上了,这不是我们预期的结果。</p> <p>为了得到预期的结果,我们需要在环视限定的条件下,把后缀部分消耗掉,同时利用否定顺序环视限定其不能是 .exe 后缀,然后用分组获取文件名,得到表达式: (.+)(?!\.exe)\.[^.]+$ 。这个表达式的匹配过程,跟上面其实是类似的,只不过因为表达式没有匹配完成,导致了回溯,回溯让出了后缀部分给 \.[^.]+ 去匹配了。</p> <p>在写这个正则的过程中,我们可以先写出 (.+)\.[^.]+$ 这样的正则,然后在再后缀位置插入环视限定,从而得到目标正则 (.+)(?!\.exe)\.[^.]+$ 。</p> <p>由于回溯过程涉及步骤过多,这里就不做展开,后面有机会再写一个关于正则回溯的文章,现在大家可以打开这个 <a href="/misc/goto?guid=4959715468813218059" rel="nofollow,noindex">否定顺序匹配与回溯</a> 演示页,分别查看3个版本的debug情况。</p> <p>选择版本:在正则输入框上面的下拉菜单里</p> <p>查看debug:左侧TOOLS区域的 Regex Debugger 菜单。</p> <p>注:由于该站jquery引用自谷歌,因此需要FQ加载才可以打开</p> <p>当然也可以用Regexbuddy的Debug功能,这个可以参考《 <a href="/misc/goto?guid=4959715468911413807" rel="nofollow,noindex">正则表达式工具RegexBuddy使用教程</a> 》查看Debug用法。</p> <p>三个版本的正则都是 (.+)(?!\.exe)\.[^.]+$<br> 源字符串分别是:</p> <ol> <li>测试示例六,使用示例六源字符串</li> <li> <p>测试匹配成功情况回溯,源字符串</p> <p>notexefile1.txt</p> </li> <li> <p>测试匹配失败情况回溯,源字符串</p> <p>exefile1.exe</p> </li> </ol> <p><strong>3、肯定逆序: (?<=exp)</strong></p> <p>(1)肯定逆序环视和否定逆序环视在一些语言中是不支持的,如JavaScript就不支持,大家在使用过程中需要注意一下。</p> <p>(2)很多语言不支持非确定长度的逆序环视。所谓非确定长度,是指逆序环视部分内容,不是固定长度的,如 (?<=.*;)abc ,这里用的 .* 就是不固定的长度。无论是分支情况还是什么,逆序环视部分需要固定长度。</p> <p>(3)有些语言里,支持特定范围的非确定长度,这个是指 (?<=.{0,100};)abc 这种,本来的 .* 使用 0-100 这样的限定最大长度为100的范围值。</p> <p>因此,大家使用过程中可以根据自己使用语言的差异,测试使用。</p> <p><strong>示例七:肯定逆序环视</strong></p> <p>源字符串:</p> <pre> name=Zjmainstay age=26</pre> <p>需求:获取name参数的值</p> <p>正则: (?<=name=).+</p> <p>示例很直白,前面必须是 name= ,然后获取其后面的数据,由于环视不占位,因此并没有出现在匹配结果中。</p> <p><strong>4、否定逆序: (?!exp)</strong></p> <p><strong>示例八:否逆序环视</strong></p> <p>源字符串:</p> <pre> name=Zjmainstay age=26</pre> <p>需求:获取不是name参数的值</p> <p>正则: ^[^=]+=(?<!name=)(.+)</p> <p>跟否定顺序示例一样,我们不能直接用 (?<!name=).+ 进行匹配,正则做法是先把参数部分匹配出来,再用否定逆序环视对它进行限定,限定它不能是 name= ,因此实现匹配。</p> <p>讲到这里,你们是否能想到前面否定顺序示例六中,可以用否定逆序来做?</p> <p>正则: (.+)\.[^.]+(?<!\.exe)$</p> <p>因此,几个环视组合,由于正则所摆放的位置不同,可以产生等价的效果。</p> <h2><strong>三、环视的应用</strong></h2> <p>环视一直是正则表达式使用过程中的难题,主要体现在它的不占位(不消耗匹配字符)但起限定作用、肯定和否定、顺序和逆序区分、摆放位置不同如何理解等概念上。经过上面的讲解,相信读者已经对这几个概念有了深刻的理解,但是,理解概念跟灵活运用是两码事。</p> <p>接下来我们再举几个平时常用的例子,帮助大家理解并掌握,达到灵活运用的程度。</p> <h3><strong>示例九:正则分块组合法-必须包含字母、数字、特殊字符</strong></h3> <p>正则: ^(?=.*?[a-z])(?=.*?\d)(?![a-z\d]+$).+$</p> <p>解析:</p> <p>(?=.*?[a-z]) 限制必须有字母</p> <p>(?=.*?\d) 限制必须有数字</p> <p>(?![a-z\d]+$) 限制从开头到结尾不能全为数字和字母</p> <p>.+ 在没有限定的情况下可以是任意字符</p> <p>^ 和 $ 限定字符串的开头和结尾</p> <p>组合起来就可以得到上面正则。</p> <h3><strong>示例十:正则逐步完善法-排除特定标签 p/a/img ,匹配html标签</strong></h3> <p>正则: </?(?!p|a|img)([^> /]+)[^>]*/?></p> <p>解析:</p> <p>常见的标签格式有:</p> <pre> <p>...</p> //无属性值 <p class="t"....>...</p> //有属性值 <img ..../> //有属性值自闭合 <br/> //无属性值自闭合</pre> <p>首先,从简单标签入手,对于 </p> 和 <br/> ,写出正则:</p> <p></?[^>]*/?></p> <p>由于 [^>] 通配符的匹配访问太大,因此,实际上无论有没有属性值,都被上面表达式给匹配了,这个没关系,我们通过进一步细化匹配通配符,缩小匹配范围。</p> <p>我们观察可得,标签名是这样得到的:</p> <pre> 无属性值:<p> <([^>]+) 有属性值:<p class <([^ ]+) 无属性值自闭合:<br/> <([^/]+) 闭合标签:</p> </([^>]+)></pre> <p>得到正则:</p> <pre> </?([^> /]+)</pre> <p>用这部分代替前面通配正则的标签名部分,得到:</p> <p></?([^> /]+)[^>]*/?></p> <p>最后,我们需要排除 p/a/img 标签,用否定顺序法,在标签名前面加入否定环视:</p> <p></?(?!p|a|img)([^> /]+)[^>]*/?></p> <p>大功告成,这是我们要的结果!</p> <p>此示例的正则逐步完善法是正则书写过程中常用方法,倒推回去也是可行的,比如,假如我们拿到一段很长的正则,而它的匹配结果是错误的,我们该怎么做?</p> <p>我们可以用逐步截断的方法,一步步的减除掉右侧的一部分,直到它恢复匹配,我们就知道刚刚被减除掉的部分正则是有问题的,观察它为什么导致错误,修改正确,再逐步恢复后面减除的正则即可。</p> <h3><strong>示例十一:正则减除查错法-匹配异常原因查找</strong></h3> <p>源字符串:</p> <pre> <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></pre> <p>正则: <li class="item">(.*?)</li></p> <p>减除排错过程:</p> <p>例子比较简单,主要演示思路过程。</p> <p>用上面的正则去匹配源字符串,我们发现,明明预期5个结果,但是却得到了4个,因此,我们开始进行减除正则排错。</p> <ol> <li>减除右侧 </li> ,此时正则 <li class="item">(.*?) 匹配4个</li> <li>减除右侧 (.*?) ,此时正则 <li class="item"> ,匹配4个</li> <li>减除 "item"> ,此时正则 <li class= , <strong>匹配5个</strong></li> <li>恢复 "item"> ,减除 > ,此时正则 <li class="item" ,匹配4个</li> <li>减除 " ,此时正则 <li class="item , <strong>匹配5个</strong><br> 至此,观察发现item后面还有其他可能,补充兼容:</li> <li>修复得正则 <li class="item[^"]*"</li> <li>逐步把前面减除的 " 后面部分补充回来,此时正则 <li class="item[^"]*">(.*?)</li> , <strong>匹配5个</strong><br> 问题解决!</li> </ol> <h2><strong>总结</strong></h2> <p>文章至此,已经完整讲解了正则表达式环视的概念与用法,读者从中能够了解到正则的逐步匹配原理,消耗与不消耗匹配字符原理,环视的不占位概念,环视作为一个虚拟位置限定其前后匹配的概念,环视肯定和否定类型与顺序和逆序类型的概念,以及各种概念原理的运用,最后还附带了正则书写过程中运用的分块组合法、逐步完善法和减除查错法,希望能够帮助广大读者更加深刻地理解正则表达式,达到灵活运用的程度。</p> <p> </p> <p> </p> <p>来自:http://www.cnblogs.com/Zjmainstay/p/regexp-lookaround.html</p> <p> </p>