转载

深入正则表达式应用

前面已经写过一篇文章《 我眼里的正则表达式(入门) 》介绍过正则表达式的基础和基本套路 正则三段论定锚点,去噪点,取数据 了,接下来这篇文章,补充一点相对高级的概念:

 1. 概念一:按单字符匹配  2. 概念二:匹配优先和不匹配优先  3. 概念三:贪婪模式与非贪婪模式  4. 概念四:环视(断言)  5. 概念五:平衡组  6. 概念六:模式修饰符  7. 附:正则三段论应用 

概念一:按单字符匹配

正则里面的数据都是按照单个字符来进行匹配的,这个通过数值区间的例子最容易体现出来,比如,示例一:

我要匹配 0-15 的数值区间,用正则来写的话,便是 [0-9]|1[0-5] ,这里,便是把0-9这种单字符的情况,和10-15这种多字符的情况拆分开了,使用分支 | 来区分开,表示要么是0-9,要么是10-15。

上面是两位数值的情况,现在延伸至1-65535,我个人的处理思想是从大到小,一块块分解:

1. 65530-65535  ==>  6553[0-5]          末位区间0-5 2. 65500-65529  ==>  655[0-2][0-9]      第四位区间0-2,末位区间0-9 3. 65000-65499  ==>  65[0-4][0-9]{2}    第三位区间0-4,后两位0-9 4. 60000-64999  ==>  6[0-4][0-9]{3}     第二位区间0-4,后三位0-9 5. 10000-59999  ==>  [1-5][0-9]{4}      第一位区间1-5,后四位0-9 6. 1-9999       ==>  [1-9][0-9]{0,3}    第一位只能是1-9,后三位可有可无

最后组合起来:

(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3})

便得到1-65535匹配正则。

根据数据处理需求,可以在正则前后加上 ^$ ,以匹配整个数据串,或者前后加入 /b ,把它当做单词边界处理。 没有限定字符的边界往往是js正则判断中常见的错误之一

概念二:匹配优先和不匹配优先

匹配优先和不匹配优先从字面理解也是比较容易的,所谓匹配优先,就是,能匹配我就先匹配;二不匹配优先就是,能不匹配我就先不匹配,这段匹配先跳过,先看看后面的匹配能不能通过。

概念三:贪婪模式与非贪婪模式

正则的贪婪模式和非贪婪模式是一个比较容易混淆的概念,不过,我们可以这么理解,一个人很贪婪,所以他会能拿多少拿多少,换过来,那就是贪婪模式下的正则表达式,能匹配多少就匹配多少,尽可能最多。

下面举个例子,示例二:

需求:匹配1后面跟任意个0 源串:10001 使用贪婪模式:10*       结果:1000 和 1 使用非贪婪模式:10*?    结果:1 和 1

首先, * 是匹配0个或多个的意思。

贪婪模式下,它表示,首先匹配一个1,然后匹配1后面的0,最多可以匹配3个0,因此得到1000,然后第二次又匹配到一个1,但是后面没有0,因此得到1;
非贪婪模式下,它表示,首先匹配一个1,然后1后面的0,能不匹配就不匹配了,所以,它每次都只是匹配了一个1。

看到这里,也许,有些人觉得,哎呀,我懂了!那么,下来我们改改,看看你是不是真懂了。

示例三:

需求:匹配1后面跟任意个0,再跟一个1 源串:10001 使用贪婪模式:10*1       结果:10001 使用非贪婪模式:10*?1    结果:10001

为什么这两次的结果一样了?

因为,正则表达式要判断完这整个正则才算成功,这种情况下,

贪婪模式,首先匹配一个1,然后匹配1后面尽可能多的0,发现3个,再匹配0后面的一个1,正则表达式匹配完,完成匹配,得到10001;
非贪婪模式,首先匹配一个1,然后,0*?是非贪婪模式,它不想匹配了(非贪婪模式不匹配优先),看看后面是不是1了?然后发现哎妈呀,后面是个0啊,然后它回头,不能再偷懒了,用0*?匹配一个0吧,然后匹配到10,又不想匹配了,看看后面有没有1了,还是没有,又回去用0*?匹配掉一个0,得到100,继续偷懒,但是发现后面还不是1,然后又用0*?匹配得到1000,最后,发现真不容易啊,终于看到1了,匹配得到10001,正则表达式匹配完,完成匹配。

看到这里,是不是懂了?

那究竟哪个模式好呢?

关于贪婪模式好还是非贪婪模式好的讨论,只能说根据需求而定,不过,在平时的时候用,一般使用非贪婪模式较多,因为贪婪模式经常会由于 元字符范围限制不严谨 而导致匹配越界,得到非预期结果。

概念四:环视(断言/零宽断言)

环视,在不同的地方又称之为零宽断言,简称断言。

用一句通俗的话解释:

环视,就是先从全局环顾一遍整个正则,(然后断定结果,)再做进一步匹配处理。

断言,就是先从全局环顾一遍整个正则,然后断定结果,再做进一步匹配处理。

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

环视主要有以下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  结果:AAA AAA(?=B)      源串:AAAB  结果:AAA AAA(?!B)      源串:AAAC  结果:AAA

而示例五体现了: 环视它匹配过的地方,下次还能用它继续匹配

因为,整个匹配过程中,正则表达式一共走了3次字符串匹配,第一次匹配不全部是字母,第二次匹配不全部是数字,第三次匹配全部是字母数字组合,6-16个字符。

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

概念五:平衡组

平衡组并不是所有程序语言都支持,而我本人使用的PHP语言就不支持,所以平时接触也是比较少的。

平衡组主要用到下面四个语法:

    (?'group') 把捕获的内容命名为group,并压入堆栈(Stack)     (?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败     (?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分     (?!) 零宽负向先行断言,由于没有后缀表达式,如没有(?!B)的B,试图匹配总是失败

在PHP中是支持(?(group)yes|no)语法的,这里的group是分组编号,即子模式编号,如(A)?(?(1)yes|no) ,匹配Ayes 和 no

下面这里引用《 正则表达式30分钟入门教程#平衡组 》的例子,展示平衡组用法,

<       #最外层的左括号  [^<>]*    #最外层的左括号后面的不是括号的内容  (   (    (?'Open'<) #碰到了左括号,在黑板上写一个"Open"    [^<>]*    #匹配左括号后面的不是括号的内容   )+   (    (?'-Open'>)   #碰到了右括号,擦掉一个"Open"    [^<>]*  #匹配右括号后面不是括号的内容   )+  )*  (?(Open)(?!))   #在遇到最外层的右括号时,判断黑板上还有没有没擦掉的"Open";如果还有,则匹配失败 >       #最外层的右括号 平衡组的一个最常见的应用就是匹配HTML,下面这个例子可以匹配嵌套的<div>标签: <div[^>]*>[^<>]*(((?'Open'<div[^>]*>)[^<>]*)+((?'-Open'</div>)[^<>]*)+)*(?(Open)(?!))</div> 

概念六:模式修饰符

模式修饰符在许多程序语言中都支持的,比如最常见的是 i ,不区分大小写,如javascript里的/[a-z0-9]/i,表示匹配字母数字,不区分大小写。

本人在写php正则时常用的模式修饰符主要有 is ,如:

$pattern = '#[a-z0-9]+#is';

模式修饰符s的作用主要是的 . 能够匹配换行,在处理换行数据时,通常会用到。

关于PHP模式修饰符的讲解,请查看 PHP手册 中的《 PHP模式修饰符 》。

七:正则三段论应用

《 我眼里的正则表达式(入门) 》 和 本文《 深入正则表达式应用 》几乎倾尽本人正则学习全部思想,但是很多读者反馈,看晕了!看到如此点评,实属无奈,因此,有必要追加本节,来个整体统筹运用,希望能让大家犹如拨云见月,洞悉其中的精义。

两篇文章中,最重要的部分当属 正则三段论定锚点,去噪点,取数据 ,它是整个正则处理过程中的灵魂,它贯穿整个正则撰写过程。

下面举例说明它的思想,示例六:

源数据:标题:深入正则表达式应用,作者:Zjmainstay 需求:匹配作者名字

我要从源数据取到 Zjmainstay 这个作者名,那么,在这里, 作者: 就是我们所说的锚点,因为在上面这段数据中它能够唯一定位到我们的数据 Zjmainstay (就在它后面),因此,我们得到

(1) 定锚点: 作者:

而在这里,我们不需要关心标题什么的,因此, 标题:深入正则表达式应用, 就是我们的噪点,因此,我们得到

(2) 去噪点

最后,我们确定 作者: 后面就是我们的数据,这个数据可以是任意字符,因此,我们得到正则:

作者:(.*)

而噪点部分,因为不会对数据取值造成干扰,直接去掉,不需要引入正则中。

下面再举一个噪点干扰的例子,示例七:

源数据: <a href="http://www.zjmainstay.cn/my-regexp" class="demo8" title="正则三段论应用举例">正则表达式入门教程</a> 需求:提取链接和标题,还有a标签的文字

看到这个源数据和需求,我们必须定位好锚点,主要有:

  1. <a //必须是a标签
  2. href=" 和 " //href=""的内容得到链接
  3. title=" 和 " //title=""的内容得到标题
  4. > 和 </a> //>和的内容得到标签文字

然后,其他的都是噪点,使用.*?替代,需要提取的数据部分使用括号获取子模式,得到分组数据,因此得到正则:

<a href="(.*?)".*?title="(.*?)">(.*?)</a>

看到这里,也许有朋友觉得,我还是不会写,那么,再来一个更简单的构建方法,细化步骤,从源串逐步得到正则,示例八:

1. 直接拷贝源串,特殊字符处理转义(本例没特殊字符)  <a href="http://www.zjmainstay.cn/my-regexp" class="demo8" title="正则三段论应用举例">正则表达式入门教程</a> 2. 从左到右,一段段转化  2.1 <a href="(.*?)" class="demo8" title="正则三段论应用举例">正则表达式入门教程</a>  2.2 <a href="(.*?)".*?title="正则三段论应用举例">正则表达式入门教程</a>  2.3 <a href="(.*?)".*?title="(.*?)">正则表达式入门教程</a>  2.4 <a href="(.*?)".*?title="(.*?)">(.*?)</a> 3. 得到最终的正则  <a href="(.*?)".*?title="(.*?)">(.*?)</a> 

至此,正则三段论的基本思想已经展示完毕,大家还有什么不解请评论留言,本人看到会第一时间给予回复。

熟悉正则三段论处理思想,剩下的便是基本语义的熟练程度了,这个通过多用多练可以达到熟能生巧的境界。

最后留下一句 至尊提醒.万能字符 ,大家看着用,遇到换行使用 [/s/S] 替换 . 即可。

(完)

正文到此结束
Loading...