乐趣区

正则捕获组与非捕获组

正则表达式分组分为捕获组(Capturing Groups)与非捕获组 Non-Capturing Groups,那为什么需要分组呢?


正则表达式分组分为捕获组(Capturing Groups)与非捕获组 Non-Capturing Groups。正则里面是用成对的小括号来表示分组的,如 (\d) 表示一个分组,(\d)(\d)表示有两个分组,(\d)(\d)(\d)表示有三个分组,有几对小括号元字符组成,就表示有几个分组,以此类推。那为什么需要分组呢?分组的目的如下:

  1. 作为可选分支
  2. 简写重复模式
  3. 缓存捕获数据及反向引用(只有捕获组才可以被反向引用)

捕获组

当你把一个正则表达式用一对小括号包起来的时候,就形成了一个捕获组,既然是捕获组,那它捕获的是什么呢?它会把括号里面的正则表达式匹配到的内容保存到该分组里面,也就是说,它捕获的就是分组里面的正则表达式匹配到的内容。而最简单的捕获组是 (),只是它匹配的是一个空字符(即字符之前的位置,如下图:)

接下来我们来看下,捕获组是怎么捕获和分组的。比如要把一个日期格式为 2019-11-10 的年月日分别匹配出来
很简单,我们先来看下年月日的结构组成:4 个数字后接一个连接符 -,再接上 2 个数字后接连接符 -,再接上 2 个数字。所以我们可以这么写 \d{4}-\d{2}-\d{2},匹配结果如下

可以看到,这个表达式匹配的是整个日期格式,并没有把年月日的信息分开匹配。那我们怎么写才能把年月日的信息分开呢?很简单,利用捕获组。如下图

可以看到,当我们在年月日对应的正则两边用小括号括起来的时候,陆续出现了 Group1、Group2 和 Group3 的信息了。既然数据可以分别获取到了,后面就可以对这三个数据进行你想要的操作了。

这是捕获组的第一个运用:用来拆分匹配到的数据 。接下来我们再来看下捕获组的第二个运用: 反向引用 。比如,现在有一个需求,要匹配如 1212,3434,7979 这类数据
我们先来分析一下数据结构:可以看出,后两个数据是前两个数据的重复,那就是 ABAB 这种模式,所以我们可以把 AB 先分组,然后在对其引用就可以了。即 (\d{2})\1,把\d{2} 匹配到的内容先保存到捕获组 1 中,然后再对捕获组 1 进行引用,当 (\d{2}) 匹配到的是 12 的时候,\1就表示 12,当 (\d{2}) 匹配到的是 34 的时候,\1就是 34。

正则里面,引用捕获组的语法是这样的 number,其中 number 是大于等于 1 的正整数(PS:,group0 表示整个正则表达式匹配到的内容,而捕获组的编号是从 1 开始的,也就是 1,大多数正则引擎最大支持 99 个捕获组,也就是 99)。我们知道,正则表达式是从字符串的左边开始往右匹配的,且一般都是消耗掉已经被匹配的字符串的(环视语法不需要消耗字符串)。因此,当前面已经出现了捕获组 1、2、3 等等的捕获组,我们就可以在后面的正则里面用 1、2、3 等来进行相应的引用了。

非捕获组

非捕获组的语法是在捕获组的基础上,在左括号的右侧加上 ?: 就可以了,那就是 (?:)。例如,(\d) 表示捕获组,而 (?:\d) 表示非捕获组。既然是非捕获组,那它就不会把正则匹配到的内容保存到分组里面。

如上图所示,当我们把捕获组改为非捕获组的时候,那些分组的信息就不见了,这个时候,(?:\d{4})-(?:\d{2})-(?:\d{2})\d{4}-\d{2}-\d{2} 匹配到的内容其实是一样的。

那什么时候需要用到非捕获组呢?

先来看第一点:不需要用到分组里面的内容的时候,用非捕获组,主要是为了 提升效率 ,因为捕获组多了一步保存数据的步骤,所以一般会多耗费一些时间,虽然时间很短。
再来看第二点:用在可选分支的时候,当我们 不需要分组里面的数据的时候 ,也可以用非捕获组,如果需要的话,则用捕获组。来看下面一个例子:

虽然这个时候 (red|blue|green) color 也可以实现该功能,但一般情况下,为了提高些许的性能,还是推荐用非捕获组(?:red|blue|green) color,但这不是强制性的,只是个建议。

下面我们再来看一个问题,需要匹配一串以逗号隔开的数字字符串,如 123,456,789,321,2345567,5678
但是逗号不能再最前面和最后面,且逗号不能连续相连出现。这个时候,简单的办法就要利用分组和量词来配合了,如下:

^\d+(?:,\d+)*$这里我们利用到了 ^ 来限制要以数字开头,$来限制要以数字结尾,()用来分组,而量词 * 用来对它前面的分组进行 0 或多次的重复匹配。

我们知道,^表示匹配开头位置,而 $ 表示匹配结尾的位置。但我们需要对一个字符串模式进行校验的时候,一般需要在前后加上这两个限制符号,以保证是从开始位置开始匹配的,到某处结尾,不然校验的结果往往是不对的。比如校验手机号码、身份证号码、密码等等,就需要加上 ^$;而如果只是匹配数据的话,一般是不需要加上的,因为我们要匹配的数据往往有可能在一大串字文本的任何位置上,加上了反而会适得其反。这里只是提示一下,后面会有校验的专题介绍。

命名捕获组

捕获组其实是分为编号捕获组 Numbered Capturing Groups 和命名捕获组 Named Capturing Groups 的,我们上面说的捕获组,默认指的是编号捕获组。命名捕获组,也是捕获组,只是语法不一样。命名捕获组的语法如下:(?<name>group)(?'name'group),其中 name 表示捕获组的名称,group表示捕获组里面的正则。

命名捕获组有什么作用?
前面我们之间介绍了编号捕获组了,为什么还需要命名捕获组呢?其实命名捕获组跟编号捕获组比起来,唯一的优点是 用命名捕获组会比较直观 ,可以用名称描述捕获到的内容的含义,因为可以命名。
前面匹配年月日的正则
(\d{4})-(\d{2})-(\d{2})
我们现在可以把它改为
(?'year'\d{4})-(?'month'\d{2})-(?'day'\d{2})

(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})

如上图,编号捕获组会把匹配到的年月日分别保存到 group1、group2 和 group3 中,也就是说,它是按从左到右给捕获组编号的,然后捕获到的内容分别放在对应的组里面。
而命名捕获组,是把分组匹配到的内容保存到对应名称的分组里面,如下两种写法的命名捕获组,会把 2019 保存到名称为 year 的捕获组里面,把 11 保存到名称为 month 的捕获组里面,把 10 保存到名称为 day 的捕获组里面。


下面,我们再来思考几个问题。

1、命名捕获组能用?: 改为非捕获组吗?

答案是不行的,原因很简单,命名捕获组的作用本来就是为了更明确地给捕获组捕获到的内容命名的,如果能用?: 改为非捕获组,那就失去了它本来的价值了。

2、命名捕获组是怎么反向引用的?

我们可以用 \k<name>\k'name' 的形式来对前面的命名捕获组捕获到的值进行引用。如之前的
(\d{2})\1
可以改写为
(?<key>\d{2})\k<key>
其中,key是我们对捕获组的命名,而 \k<key> 是对 key 这个分组捕获到的内容进行引用,所谓的引用,就是利用它前面匹配到的字符串的值

3、一个正则里,多个命名捕获组能同名吗?

这个问题,一般是不允许的,但有个别编程语言是允许的,例如 java、python 等是不允许的,而 C#、Perl 等是允许的。因此,最好不要这么写。

java是从 JDK1.7 开始才开始支持命名捕获组的。那编号捕获组有存在编号重复的情况吗?答案是没有的,因为编号捕获组的编号是根据 () 去自动识别的,而命名分组是人为命名的。另外,虽然命名捕获组看起来比较明确捕获到的内容是什么,但其写法也是比较繁琐的,并且存在编程语言的兼容性问题,因此,能用编号捕获组去解决的问题,就不需要用命名捕获组了。

4、正则里面,怎么计算捕获组的个数?

在正则里面,计算整个表达式里面有几个捕获组的规律如下:
从左到右 ,计算 小左括号 (的个数 有几个就有几个捕获组 编号的顺序 也是 按出现的先后顺序去排序 的。以下三种情况除外:
\( 被转义过的括号不算
(?:) 非捕获组的不算
[(] 放在方括号里面的小括号也不算,这也属于被转义过。

(\d{4})-(\d{2})-(\d{2}) 几个分组?有几个捕获组?每个捕获组对应的编号是多少?

按照上面的规律,从 左到右 ,有三个(,并且这上个左括号不是被排除掉的那三种情况之一的,所以我们可以确定,该正则有三个分组,并且都是捕获组。由于(\d{4}) 所在的这个 ( 是最先出现的,因此 (\d{4}) 的编号是 1,为 group1;接下来出现的是第一个 (\d{2})(上面有两个(\d{2})),所以这是编号为 2 的捕获组,group2;而最后出现( 的位置是最后的那个(\d{2}),因此,这是编号为 3 的捕获组,即 group3。由上图的匹配结果详情也可以看出来。

再来看下,这个正则表达式的分组情况:
(?:\d{4})-(\d{2})-(\d{1,2})
我们还是 从左到右 查找 ( 的个数,由于第一个出现的 ((?:,所以它虽然是分组,但它是非捕获组,因此 (?:\d{4}) 不会有捕获组的编号,第二个出现 ( 的是 (\d{2}),因此这个捕获组的编号为 1,即 group1,接下来出现( 的是 (\d{1,2}),因此此处捕获组的编号为 2,即 group2。
所以,该正则有三个分组,第一个为非捕获组,第二个为捕获组 1,第三个为捕获组 2。
现在,我们来看下用 | 隔开的捕获组的编号是否也是符合上面的计算捕获组编号的规律。例如 (\d)|(\d)

由上图可以看出,这个数字 2 是由 group1 分组匹配到的,但是右下方的匹配详情里却没有显示 group2 的信息,是不是这种情况下只有一个分组呢?

其实不是的,这种情况也是有两个分组,group1 和 group2,只是此时 group2 没有匹配到数据,所以这个在线工具没有显示出来而已。其实你看下右上方的正则解释,就清楚了。描述很清楚 1st Capturing Group2st Capturing Group。因此 (\d)|(\d) 也是有两个捕获组的。下面我们用 RegexBuddy 工具查看匹配结果:

没错,就是有两个捕获组,只是此时 group2 不参与匹配而已

正则表达式分组的相关知识就介绍到这里,希望对你有帮助。实践出真知,想要深入学习,就得多学、多思、多实践。

微信公众号:Cooking Regex

退出移动版