深入理解正则表达式高级教程

举报
斌哥来了 发表于 2021/07/26 20:29:51 2021/07/26
【摘要】 概念 一 :按单字符匹配 正则里面的数据都是按照单个字符来进行匹配的,这个通过数值区间的例子最容易体现出来,比如,示例 一 : 我要匹配0-15的数值区间,用正则来写的话,便是[0-9] | 1[0-5],这里,便是把0-9这种单字符的情况,和10-15这种多字符的情况拆分开了,使用分支|来区分开,表示要么是0-9,要么是10-15。  上面是两位数值的情况,现在延伸至1-6553...

概念 :按单字符匹配

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

我要匹配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,正则表达式匹配完,完成匹配。

看到这里,是不是懂了? 
那究竟哪个模式好呢?

什么时候使用贪婪模式,什么时候使用非贪婪模式,哪个性能好,哪个性能不好,不能一概而论,要根据情况分析。 
下面我举个例子: 
源码:

<a href =" zjmainstay.cn/my-regexp " target="_blank" title="我眼里的正则表达式(入门)">我眼里的正则表达式(入门)</a>

<a title="我眼里的正则表达式(入门)" href =" zjmainstay.cn/my-regexp " target="_blank">我眼里的正则表达式(入门)</a>

正则1:<a [^>]* ? href ="([^"]*?)"[^>]*?>([^<]*?)</a>(238次) 
正则2:<a [^>]* ? href ="([^"]*)"[^>]*>([^<]*)</a>(65次) 
正则3:<a [^>]* href ="([^"]*)"[^>]*>([^<]*)</a>(136次) 
附:执行次数的获取请下载正则表达式测试工具: RegexBuddy 4.1.0-正则测试工具.rar ,使用里面的Debug功能。

正则1是通用写法,正则2是在确定字符不会溢出的情况下消除非贪婪模式,正则3是证明并不是全部消除非贪婪模式就是最优。

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

在确定的数据结构里,可以尝试使用[^>]*>这样的排除字符贪婪模式 替换非贪婪 模式,提升匹配的效率。注意,贪婪部分([^>]*)的匹配,最好不要越过其后面的字符(>),否则会导致贪婪模式下的回溯,如正则3,[^>]*的匹配越过了 href ,一直匹配到>为止,而这时候再匹配 href ,会匹配不到而导致多次回溯处理,直到回溯到 href 前的位置, 后面才 继续了下去。

另外,需要注意一点,无论使用贪婪模式还是非贪婪模式,在不同语言需要注意回溯次数和嵌套次数的限制,比如在PHP中, pcre.backtrack_limit =100000, pcre.recursion_limit =100000。


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

环视,在不同的地方又 称之为零宽断言 ,简称断言。 
用一句通俗的话解释: 
环视,就是先从全局环顾一遍正则,(然后断定结果,)再做进一步匹配处理。 
断言,就是先从全局环顾一遍正则,然后断定结果,再做进一步匹配处理

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

环视的作用相当于对其所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。

环视主要有以下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

附: js 不支持(?<= exp ) 和 (?<! exp ) 语法


概念五:平衡组

平衡组并不是所有程序语言都支持,而我本人使用的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分钟入门教程#平衡组 》关于<>配对匹配的例子,展示平衡组用法,

概念六:模式修饰符

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

本人在写 php 正则时常用的模式修饰符主要有 i s ,如: 
$pattern = ' #[ a-z0-9]+#is';

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

在PHP中,模式修饰符有两种用法,一种是上面的,在分隔符后面的模式修饰符,它的作用范围是全局;另一种是在正则表达式中间的。 
例如:

正则: /( (? i )[A-Z]+)c/

测试字符: abcABC

匹配: abc

说明:局部(ab)的大小写被忽略了,(? i )的作用范围在分组1内


【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。