关于正则表达式里面提到的子组,不知哪路大神来给解个惑?
老是看到有提到子组的,想来想去想不明白,头皮都抓破三张
老是看到有提到子组的,想来想去想不明白,头皮都抓破三张
2016-08-06
在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#'
将原来的冒号改为竖线之后,我们就会发现,原来空的匹配不见了。
子组通过圆括号分隔界定,并且它们可以嵌套。 将一个模式中的一部分标记为子组(子模式)主要是来做两件事情:
将可选分支局部化。比如,模式cat(arcat|erpillar|)匹配 ”cat”, “cataract”, “caterpillar” 中的一个,如果没有圆括号的话,它匹配的则是 ”cataract”, “erpillar” 以及空字符串。
将子组设定为捕获子组(向上面定义的). 当整个模式匹配后, 目标字符串中匹配子组的部分将会通过 pcre_exec()() 的ovector 参数回传给调用者。 左括号从左至右出现的次序就是对应子组的下标(从 1 开始), 可以通过这些下标数字来获取捕获子模式匹配结果。
比如,如果字符串 ”the red king” 使用模式((red|white) (king|queen)) 进行匹配, 模式匹配到的结果是 array(“red king”, ”red king”, “red”, “king”) 的形式, 其中第 0 个元素是整个模式匹配的结果,后面的三个元素依次为三个子组匹配的结果。 它们的下表分别为 1, 2, 3。
事实上,圆括号履行的两种功能并不总是有用的。 经常我们会有一种需求需要使用子组进行分组, 但又不需要(单独的)捕获它们。 在子组定义的左括号后面紧跟字符串 ”?:” 会使得该子组不被单独捕获, 并且不会对其后子组序号的计算产生影响。比如, 如果字符串 ”the white queen” 匹配模式 ((?:red|white) (king|queen)), 匹配到的结果会是 array(“white queen”、“white queen”、“white queen”),的和 king|queen 这两个子组。 捕获子组序号的最大值是 99, 最大允许拥有的所有子组(包括捕获的和非捕获的)的最大数量为 200。
为了方便简写,如果需要在非捕获子组开始位置设置选项, 选项字母可以位于 ? 和 : 之间,比如:
(?i:saturday|sunday)
(?:(?i)saturday|sunday)
上面两种写法实际上是相同的模式。因为可选分支会从左到右尝试每个分支, 并且选项没有在子模式结束前被重置, 并且由于选项的设置会穿透对后面的其他分支产生影响,因此, 上面的模式都会匹配 ”SUNDAY” 以及 ”Saturday”。
在 PHP 4.3.3 中,可以对子组使用 (?P<name>pattern) 的语法进行命名。 这个子模式将会在匹配结果中同时以其名称和顺序(数字下标)出现, PHP 5.2.2中又增加了两种味子组命名的语法: (?<name>pattern) 和 (?’name’pattern)。
有时需要多个匹配可以在一个正则表达式中选用子组。 为了让多个子组可以共用一个后向引用数字的问题, (?| 语法允许复制数字。 考虑下面的正则表达式匹配Sunday:
(?:(Sat)ur|(Sun))day
这里当后向引用 1 空时Sun 存储在后向引用 2 中. 当后向引用 2 不存在的时候 Sat 存储在后向引用 1中。 使用 (?|修改模式来修复这个问题:
(?|(Sat)ur|(Sun))day
举报