深入理解正则表达式高级教程
一、按字符匹配
正则里面的数据都是按照单个字符来进行匹配的,这个通过数值区间的例子最容易体现出来,比如:
我要匹配0-15
的数值区间,用正则来写的话,便是[0-9]|1[0-5]
,这里,便是把[0-9]
这种单字符的情况,和10-15
这种多字符的情况拆分开了,使用分支|
来区分开,表示要么是0-9,,要么是10-15
上面是两位数的情况,现在延伸至1-65535,我个人的处理思想是从大到小,一块块分解:
1.65530-65535 6553[0-5]
2.65500-65529 655[0-2][0-9]
3.65000-65499 65[0-4][0-9]{2}
4.60000-64999 6[0-4][0-9]{3}
5.10000-59999 [1-5][0-9]{4}
6.1-9999 [1-9][0-9]{0,3}
最后组合起来:
(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})
根据数据处理需求,可以在正则前后加上^
和$
,以匹配整个数据串,或者前后加入\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?1 结果:10001
为什么这两次的结果一样了?
因为正则表达式要判断完整个正则才算成功,这种情况下
贪婪模式,首先匹配一个1,然后匹配1后面尽可能多的0,发现3个,再匹配0后面的一个1,正则表达式匹配完,完成匹配,得到10001
非贪婪模式,首先匹配一个1,然后0*?是非贪婪模式,它不想匹配了,看看后面是不是1了?然后发现使用0 * ?还可以继续匹配,然后匹配到10,以此继续,直到匹配到10001,完成匹配
看到这里,是不是懂了
那究竟哪个模式好用呢?
什么时候用贪婪模式,什么时候使用非贪婪模式,哪个性能好,哪个性能不好,不能一概而论,要根据情况分析
如下:
<a href="http://www.zjmainstay.cn/my-regexp" target="_blank" title="我眼里的正则表达式(入门)">我眼里的正则表达式(入门)</a>
<a title="我眼里的正则表达式(入门)" href="http://www.zjmainstay.cn/my-regexp" target="_blank">我眼里的正则表达式(入门)</a>
正则1: <a [^>]*?href="([^"]*?)"[^>]*?>([^<]*?)</a>
238次
正则2: <a [^>]*?href="([^"]*)"[^>]*>([^<]*)</a>
65次
正则3: <a [^>]*href="([^"]*)"[^>]*>([^<]*)</a>
136次
正则1是通用写法,正则2是在确定字符不会溢出的情况下消除非贪婪模式,正则3是证明并不是全部消除非贪婪模式就是最优。
因此,关于贪婪模式好还是非贪婪模式好的讨论,只能说根据需求而定,不过,在平时的时候用,一般使用非贪婪模式较多,因为贪婪模式经常会由于元字符范围限制不严谨
而导致匹配越界,得到非预期结果
在确定的数据结构里,可以尝试使用[^>]*>
这样排除字符贪婪模式替换非贪婪模式,提升匹配的效率。注意,贪婪部分([^>]*)
的匹配,最好不要越过其后面的字符(<
),否则会导致贪婪模式下的回溯,如正则3,[^>]*
的匹配越过了href,一直匹配到>
为止,而这时候再匹配href,会匹配不到而导致多次回溯处理,直到回溯到href前的为止,后面才继续了下去
四、环视(断言/零宽断言)
环视,在不用的地方又称之为零宽断言,简称断言
断言,就是先从全局环顾一遍正则,然后断定结果,再做进一步匹配处理
两个虽然字面不一样,意思确实同一个,都是做全局观望,再做进一步处理
环视的作用相当于对其所在位置加了一个附加条件,只有满足这个,环视子表达式才能匹配成功
环视主要有以下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的ISI,前置的(?!B)
只是对后面数据的一个限定,从而达到过滤匹配的效果
因此,环视做排除处理还是比较实用的,比如:
需求:字母,数字组合,不区分大小写,不能纯数字或纯字母,6-16个字符
通用写法: ^[a-z0-9]{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
五、平衡组
平衡组并不是所有程序语言都支持。
平衡组主要用到下面4个语法:
(?'group') 把捕获的内容命名为group,并压入堆栈
(?'-group') 从堆栈上弹出最后压入堆栈的名为group的捕获内容,如果堆栈本来为空,则本分组的匹配失败
(?(group)yes|no) 如果堆栈上存在以名为group的捕获内容的话,继续匹配yes部分的表达式,否则继续匹配no部分
(?!) 零宽负向先行断言,由于与没有后缀表达式,如没有(?!B)的B,试图匹配总是失败
六、正则三段论应用
两篇文章中,最重要的部分当属定锚点,去噪点,取数据,它是整个正则处理过程中的灵魂。它贯穿整个正则撰写过程
下面举例说明它的思想
源数据: 标题:深入正则表达式应用,作者: 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>
至此,正则三段论的基本思想已经展示完毕,大家还有什么不解请评论留言,本人看到会第一时间给予回复。
熟悉正则三段论处理思想,剩下的便是基本语义的熟练程度了,这个通过多用多练可以达到熟能生巧的境界。