正则表达式中的子组模式

与世无争的帅哥 提交于 2019-12-06 23:43:46

作者:西瓜玩偶(racnil070512 at hotmail dot com)

一、基础知识

在PCRE正则表达式中,我们可以利用圆括号定义一个子组,我们可以使用preg_match函数(其他函数的信息请参考PHP官方API文档)的第三个参数捕获圆括号中匹配的内容:

preg_match('#color\h*:\h*([A-Za-z]*)#', 'color: red', $matches);
print_r($matches);


运行的结果为:

Array
(
    [0] => color: red
    [1] => red
)


根据定义,子组(正则表达式中圆括号)中的内容会按照左半边括号出现的顺序,将匹配的内容分别存放至$matches数组中,下标从1开始(下标0的内容为整个匹配的字符串)。

这个特性可以让我们很方便地从被匹配的字符串中提取我们需要的信息。PCRE中的子组的功能其实非常强大,但是PHP官方的API文档并没有对齐作过多的介绍。下面的文章尝试对PCRE中的子组功能做一个初步的介绍。

二、匹配顺序

子组其中一个重要的作用就是用来描述“分支”的匹配,但是如果较短的分支是较长分支的前缀的话,那么较短的分支一定要放在较长的分支后面:

'#(eq|lte|gte|lt|gt)#'


注意,这里的lt必须放在lte的后面,否则的话正则表达式解析器读到lt时分支就已经匹配成功了,那么lte就永远不会被匹配到。

三、非捕获子组

有些时候子组只是用来描述“分支”的匹配的,我们并不想让最后的$matches里面出现括号里的内容,此时可以用非捕获子组(?:)告诉正则表达式解析器,它不需要被捕获:

'#(?:https?|ftp)://([A-Za-z\.]+)#'


这样,URL里面主机名部分就会被存放至$matches数组下标为1的域内。而前面的https?|ftp虽然也被打了圆括号,但是由于圆括号中有?:,所以并不会被保存到$matches中。

不过这里仅仅是举例子,在实际应用中,可以调用parse_url函数来更好地完成获取主机名的任务。

四、前向探测(Lookahead)

前向探测的目的是,在当前的点,向后读入内容(对于读取匹配内容的程序来说,它即将读入的内容被称为“前”;但是对于阅读者来说,即将读入的内容被
称为“后”),判断其是否与子组中的正则表达式相匹配。如果匹配,则继续匹配后面的内容,否则匹配失败。虽然前向探测会向后读入内容,但是被读入的内容并
不会被“消耗”掉,也不算做正则表达式匹配的一部分,也就是说,后面的正则表达式依然可以匹配到向后读入的内容。

如果这样说不太明白,可以看看下面的例子。利用(?=)就可以构造一个前向探测:

'#\d*(?= mm)#'

这个正则表达式会匹配如'100 mm'这样的字符串。由于前向探测的正则表达式mm并不属于正则表达式的一部分,所以最后整个表达式(注意,不是$matches下标为1的域,而是整个表达式,也就是下标0)匹配出来的结果是'100'。

更好的例子是检查密码是否符合规范:

'#^(?=\w{8,20}$)(?=[^A-Z]*[A-Z])(?=[^a-z]*[a-z])(?=\D*\d)(?=[^_]*_).*$#'

这个正则表达式在最开头的地方依次使用了5个前向探测子组,分别检查密码长度在8至20之间、含有大写字母、含有小写字母、含有数字以及含有下划线。只有当这五个条件都满足,正则表达式才会继续向下匹配。由于这些子组都不会消耗读入的内容,所以最后我们简单地使用一个.*就可以获取整个密码字符串。

五、前向逆探测(Negative Lookahead)

与前向探测类似,只不过子组中的表达式必须不满足才行。它的构造方法为(?!):

'#\d*(?!\d| mm)#'


这个表达式除了类似于'100 mm'以外其余的类似于'100 cm'这样的字符串都可以被匹配。注意子组正则表达式里面加了一个\d,因为不加它,当读入'100 mm'的时候,表达式还是会匹配到'10',这是因为'0 mm'不匹配' mm'。

六、后向探测(Lookbehind)

与前向探测类似,后向探测只不过是以当前点为准,向前读入内容。后向探测的构造方法为(?<=):

'#(?<=EUR ).*#'

这个正则表达式会匹配'EUR 100'这样的字符串。匹配结果为'100'而不是'EUR 100',这是因为后向探测是以当前点为准,向前读入内容,这也就意味着,当开始进行最后.*的匹配时,'EUR '早已被读过了。

不过这并不意味着后向探测会消耗内容,只是因为我们并没有在正则表达式中匹配'EUR '而已。如果你有兴趣,可以尝试下面的表达式:

'#EUR (?<=EUR)\d*#'

这样,匹配出来的结果就是'EUR 100'了。

七、后向逆探测(Negative Lookbehind)

与后向探测类似,只不过子组内的表达式必须不匹配。这里就不再举例了。

八、命名子组

我们可以利用下面的语法命名一个子组:

'#(?P<prefix>A+)C#'


它会匹配类似于'AAAAC'的字符串,子组匹配的内容'AAAA'不仅会以数字下标保存(这个例子中为1),亦会以字符串下标('prefix')保存在$matches里面。

九、子组的重复利用

利用下面的方式我们可以重复利用已经在正则表达式中出现的子组:

'#(\w+) (?1)#'

这个正则表达式会匹配'foo bar'。不过需要注意的是,重用的子组并不会被捕获。如果想要捕获重用的子组,则应该在子组外面再加上一个括号:

'#(\w+) ((?1))#'

我们甚至可以通过子组名称来重复利用它:

'#(?<pattern>\w+) (?&pattern)#'

甚至还可以递归地调用子组:

'#(\w+, (?1)?)(\w+)#'

上面的表达式会匹配'foo, bar, baz, qux'。

十、重置分支

这一点在PHP官方文档中已经提到了:

'#(?:(Sat)ur|(Sun))day#'

当匹配'Sunday'的时候,我们会发现在$matches里面下标为1的域是空的,这是因为它尝试过匹配(Sat),由于没有匹配到内容,所以它在$matches里面加入了一个空的匹配项。如果要去掉这个恼人的匹配项,我们需要在匹配不成功的时候重置分支:

'#(?|(Sat)ur|(Sun))day#'

将原来的冒号改为竖线之后,我们就会发现,原来空的匹配不见了。

十一、总结

上面的文章中介绍了PCRE中子组的使用方法,并且简单地介绍了九种子组的特殊功能。如果能够灵活地、适当地运用在我们的程序中,它就可以帮助我们省掉许多字符串处理的步骤。


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!