关于正则表达式:什么是非捕获组?

What is a non-capturing group? What does (?:) do?

如何使用?:,它有什么好处?


让我用一个例子来解释这一点。

请考虑以下文本:

1
2
http://stackoverflow.com/
https://stackoverflow.com/questions/tagged/regex

现在,如果我在上面应用下面的regex…

1
2
3
4
5
(https?|ftp)://([^/

]+)(/[^

]*)?

…我会得到以下结果:

1
2
3
4
5
6
7
8
9
Match"http://stackoverflow.com/"
     Group 1:"http"
     Group 2:"stackoverflow.com"
     Group 3:"/"

Match"https://stackoverflow.com/questions/tagged/regex"
     Group 1:"https"
     Group 2:"stackoverflow.com"
     Group 3:"/questions/tagged/regex"

但我不在乎协议——我只想要URL的主机和路径。因此,我将regex更改为包含非捕获组(?:)

1
2
3
4
5
(?:https?|ftp)://([^/

]+)(/[^

]*)?

现在,我的结果如下:

1
2
3
4
5
6
7
Match"http://stackoverflow.com/"
     Group 1:"stackoverflow.com"
     Group 2:"/"

Match"https://stackoverflow.com/questions/tagged/regex"
     Group 1:"stackoverflow.com"
     Group 2:"/questions/tagged/regex"

看到了吗?第一组尚未捕获。解析器使用它来匹配文本,但在最终结果中稍后会忽略它。

编辑:

按照要求,我也试着解释一下群体。

嗯,团体有很多用途。它们可以帮助您从更大的匹配(也可以命名)中提取准确的信息,它们允许您重新匹配以前匹配的组,并可用于替换。我们来举几个例子,好吗?

好吧,假设您有某种XML或HTML(请注意,regex可能不是该工作的最佳工具,但作为一个示例,它很好)。您希望解析标记,这样您可以执行类似的操作(我添加了空格以便于理解):

1
2
3
   \<(?<TAG>.+?)\> [^<]*? \</\k<TAG>\>
or
   \<(.+?)\> [^<]*? \</\1\>

第一个regex有一个命名组(标记),而第二个regex使用一个公共组。两个regex都做相同的事情:它们使用第一个组(标记的名称)中的值来匹配结束标记。区别在于,第一个使用名称匹配值,第二个使用组索引(从1开始)。

现在让我们试试替代品。请考虑以下文本:

1
Lorem ipsum dolor sit amet consectetuer feugiat fames malesuada pretium egestas.

现在,让我们用这个哑正则表达式来覆盖它:

1
\b(\S)(\S)(\S)(\S*)\b

此regex匹配至少有3个字符的单词,并使用组分隔前三个字母。结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Match"Lorem"
     Group 1:"L"
     Group 2:"o"
     Group 3:"r"
     Group 4:"em"
Match"ipsum"
     Group 1:"i"
     Group 2:"p"
     Group 3:"s"
     Group 4:"um"
...

Match"consectetuer"
     Group 1:"c"
     Group 2:"o"
     Group 3:"n"
     Group 4:"sectetuer"
...

因此,如果我们应用替换字符串:

1
$1_$3$2_$4

…在它上面,我们尝试使用第一个组,添加下划线,使用第三个组,然后使用第二个组,添加另一个下划线,然后使用第四个组。生成的字符串与下面的字符串类似。

1
L_ro_em i_sp_um d_lo_or s_ti_ a_em_t c_no_sectetuer f_ue_giat f_ma_es m_la_esuada p_er_tium e_eg_stas.

也可以使用命名组进行替换,使用${name}

为了更好地使用regex,我推荐http://regex101.com/,它提供了有关regex如何工作的大量详细信息;它还提供了一些可供选择的regex引擎。


可以使用捕获组来组织和分析表达式。非捕获组有第一个好处,但没有第二个好处的开销。例如,您仍然可以说非捕获组是可选的。

假设您想匹配数字文本,但有些数字可以写成1、2、3、4…如果要捕获数字部分,而不是(可选)后缀,可以使用非捕获组。

1
([0-9]+)(?:st|nd|rd|th)?

它将匹配1,2,3中的数字…或以1、2、3……的形式但它只捕获数字部分。


当您希望对表达式分组时,使用?:,但不希望将其保存为字符串的匹配/捕获部分。

例如与IP地址匹配的内容:

1
/(?:\d{1,3}\.){3}\d{1,3}/

注意,我不关心保存前3个八位字节,但是(?:...)分组允许我缩短regex,而不会产生捕获和存储匹配的开销。


它使组不捕获,这意味着与该组匹配的子字符串将不包含在捕获列表中。Ruby中的一个例子说明了不同之处:

1
2
"abc".match(/(.)(.)./).captures #=> ["a","b"]
"abc".match(/(?:.)(.)./).captures #=> ["b"]

历史动机:非捕捉群体的存在可以用括号来解释。考虑表达式(a b)c和a bc,由于连接优先于,这些表达式分别表示两种不同的语言(ac、bc和a、bc)。但是,括号也用作匹配组(如其他答案所解释的…)。

当您希望使用括号但不捕获子表达式时,可以使用非捕获组。在示例中,(?A:B)C


捕获的组可以稍后在regex中使用以匹配,也可以在regex的替换部分使用它们。使一个非捕获组简单地免除了该组由于上述任何一个原因而被使用。

如果你试图捕捉许多不同的东西,并且有一些组你不想捕捉,那么不捕捉组是很好的。

这就是它们存在的原因。当你学习群体,学习原子群体,他们做了很多!也有环视小组,但他们有点复杂,使用不太多。

稍后在regex中使用的示例(backreference):

<([A-Z][A-Z0-9]*)\b[^>]*>.*?[查找XML标记(不支持NS)]

([A-Z][A-Z0-9]*)是一个捕获组(在本例中是标记名)

稍后在regex中是\1,这意味着它将只匹配第一组(([A-Z][A-Z0-9]*)组)中的相同文本(在这种情况下,它与结束标记匹配)。


让我举个例子来试试这个:

regex代码:-(?:animal)(?:=)(\w+)(,)\1\2

搜索字符串:

1号线-animal=cat,dog,cat,tiger,dog

2号线-animal=cat,cat,dog,dog,tiger

3号线-animal=dog,dog,cat,cat,tiger

(?:animal)->未捕获组1

(?:=)->未捕获组2

(\w+)->捕获组1

(,)->捕获组2

捕获组1的\1->结果,即第1行是cat,第2行是cat,第3行是dog。

\2->捕获组2的结果,即逗号(,)

因此,在这段代码中,通过给出1和2,我们在代码后面分别调用或重复捕获的组1和组2的结果。

按照代码的顺序(?:animal)应为第1组和(?:=)应为第2组并继续。

但是通过给予?:我们使匹配组不被捕获(匹配组中不倒计时,因此分组编号从第一个捕获组开始,而不是从未捕获组开始),以便重复匹配组的结果(?:animal)不能在以后的代码中调用。

希望这能解释非捕获组的用法。

在此处输入图像描述


我是一个javascript开发人员,我将尝试解释它与javascript相关的意义。

考虑一个场景,您希望匹配cat is animal。当你想匹配猫和动物时,两者之间应该有一个is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 // this will ignore"is" as that's is what we want
"cat is animal".match(/(cat)(?: is )(animal)/) ;
result ["cat is animal","cat","animal"]

 // using lookahead pattern it will match only"cat" we can
 // use lookahead but the problem is we can not give anything
 // at the back of lookahead pattern
"cat is animal".match(/cat(?= is animal)/) ;
result ["cat"]

 //so I gave another grouping parenthesis for animal
 // in lookahead pattern to match animal as well
"cat is animal".match(/(cat)(?= is (animal))/) ;
result ["cat","cat","animal"]

 // we got extra cat in above example so removing another grouping
"cat is animal".match(/cat(?= is (animal))/) ;
result ["cat","animal"]

在复杂的正则表达式中,可能会出现希望使用大量组的情况,其中一些组用于重复匹配,另一些组用于提供返回引用。默认情况下,匹配每个组的文本加载到backreference数组中。如果我们有很多组,并且只需要从backreference数组中引用其中的一些组,那么我们可以重写此默认行为,以告诉正则表达式某些组仅用于重复处理,不需要捕获并存储在backreference数组中。


tl;dr non-captureing groups,顾名思义,是regex中不希望包含在匹配中的部分,?:是将组定义为非捕获的一种方法。

假设您有电子邮件地址[email protected]。下面的regex将创建两个组,id部分和@example.com部分。(\p{Alpha}*[a-z])(@example.com)。为了简单起见,我们提取整个域名,包括@字符。

现在我们假设,您只需要地址的ID部分。您要做的是获取匹配结果的第一组,在regex中由()包围,这样做的方法是使用非捕获组语法,即?:。所以regex (\p{Alpha}*[a-z])(?:@example.com)只返回电子邮件的id部分。


我不能对最重要的答案发表评论,我想增加一个明确的观点,这只在最重要的答案中有所暗示:

非捕获群(?...)。不从原始完全匹配中删除任何字符,它只将regex可视化地重新组织给程序员。

要访问regex的特定部分而不定义无关字符,您将始终需要使用.group()


我遇到的一个有趣的问题是,在非捕获组中可以有一个捕获组。请查看下面的regex以查找匹配的Web URL:

1
var parse_url_regex = /^(?:([A-Za-z]+):)(\/{0,3})([0-9.\-A-Za-z]+)(?::(\d+))?(?:\/([^?#]*))?(?:\?([^#]*))?(?:#(.*))?$/;

输入URL字符串:

1
var url ="http://www.ora.com:80/goodparts?q#fragment";

我的regex (?:([A-Za-z]+):)中的第一个组是一个非捕获组,它与协议方案和colon :字符(即http:相匹配,但是当我在代码下运行时,我看到返回数组的第一个索引包含字符串http当我认为http和colon :bo时因为它们在非捕获组中,所以不会被报告。

1
console.debug(parse_url_regex.exec(url));

enter image description here

我想,如果第一组(?:([A-Za-z]+):)是非捕获组,那么为什么它会在输出数组中返回http字符串。

因此,如果您注意到在非捕获组中有一个嵌套的组([A-Za-z]+)。嵌套组([A-Za-z]+)本身就是一个捕获组(开始时没有?:),位于非捕获组(?:([A-Za-z]+):)内。这就是为什么文本http仍然被捕获,但位于非捕获组内部但在捕获组外部的冒号:字符不会在输出数组中报告的原因。


我想我会给你答案的,如果不检查匹配是否成功,则不要使用捕获变量。

捕获变量$1等在匹配成功且未清除之前无效。

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/perl  
use warnings;
use strict;  
$_ ="bronto saurus burger";
if (/(?:bronto)? saurus (steak|burger)/)
{
    print"Fred wants a  $1";
}
else
{
    print"Fred dont wants a $1 $2";
}

在上面的例子中,为了避免在1美元内捕获bronto,(?):使用。如果模式匹配,那么$1将被捕获为下一个分组模式。因此,输出如下:

1
Fred wants a burger

如果您不希望保存匹配项,这很有用。


它非常简单,我们可以用简单的日期示例来理解,假设日期被提到为2019年1月1日或2019年5月2日或任何其他日期,并且我们只想将其转换为dd/mm/yyyy格式,那么我们不需要月份的名称,也就是1月或2月,因此为了捕获数字部分,而不是(选项al)后缀可以使用非捕获组。

所以正则表达式是,

1
([0-9]+)(?:January|February)?

就这么简单。


打开Google Chrome DevTools,然后打开控制台选项卡:并键入:

1
"Peace".match(/(\w)(\w)(\w)/)

运行它,您将看到:

1
["Pea","P","e","a", index: 0, input:"Peace", groups: undefined]

JavaScriptregexp引擎捕获三个组,即索引为1、2、3的项。现在使用非捕获标记查看结果。

1
"Peace".match(/(?:\w)(\w)(\w)/)

结果是:

1
["Pea","e","a", index: 0, input:"Peace", groups: undefined]

很明显,什么是非捕获组。