.NET 正则表达式中的“组”和“捕获”有啥区别?

Posted

技术标签:

【中文标题】.NET 正则表达式中的“组”和“捕获”有啥区别?【英文标题】:What's the difference between "groups" and "captures" in .NET regular expressions?.NET 正则表达式中的“组”和“捕获”有什么区别? 【发布时间】:2011-03-20 05:56:45 【问题描述】:

当谈到 .NET 的正则表达式语言时,我对“组”和“捕获”之间的区别有点模糊。考虑以下 C# 代码:

MatchCollection matches = Regex.Matches("Q", @"^\([A-Z])\$");

我希望这会导致对字母“Q”的一次捕获,但如果我打印返回的 MatchCollection 的属性,我会看到:

matches.Count: 1
matches[0].Value: Q
        matches[0].Captures.Count: 1
                matches[0].Captures[0].Value: Q
        matches[0].Groups.Count: 2
                matches[0].Groups[0].Value: Q
                matches[0].Groups[0].Captures.Count: 1
                        matches[0].Groups[0].Captures[0].Value: Q
                matches[0].Groups[1].Value: Q
                matches[0].Groups[1].Captures.Count: 1
                        matches[0].Groups[1].Captures[0].Value: Q

这里到底发生了什么?我知道整场比赛也有一次捕获,但是小组如何进入?为什么matches[0].Captures 不包含对字母“Q”的捕获?

【问题讨论】:

【参考方案1】:

您不会是第一个对此感到困惑的人。以下是著名的 Jeffrey Friedl 对此的评价(第 437 页以上):

根据您的观点,它要么添加 一个有趣的新维度 匹配结果,或增加混乱和 膨胀。

进一步:

组之间的主要区别 对象和捕获对象是 每个 Group 对象包含一个 代表捕获的集合 所有 中间人 匹配的 比赛期间的小组,以及 该组匹配的最终文本。

几页之后,这是他的结论:

通过 .NET 之后 文档和实际上 了解这些对象添加了什么, 我对他们的感情很复杂。在 一方面,这是一个有趣的 创新 [..] 另一方面,它 似乎增加了效率负担 [..] 不会使用的功能 在大多数情况下

换句话说:它们非常相似,但偶尔,当它发生时,你会发现它们的用途。在你长出另一根白胡子之前,你甚至可能会喜欢 Captures...


由于上述内容以及其他帖子中所说的内容似乎都没有真正回答您的问题,因此请考虑以下内容。将 Captures 视为一种历史跟踪器。当正则表达式匹配时,它会从左到右遍历字符串(暂时忽略回溯),当遇到匹配的捕获括号时,它将存储在$x(x 是任何数字)中,比如说@ 987654323@。

正常的正则表达式引擎,当要重复捕获括号时,将丢弃当前的$1 并将其替换为新值。不是 .NET,它将保留此历史记录并将其放置在 Captures[0]

如果我们将您的正则表达式更改为如下所示:

MatchCollection matches = Regex.Matches("QRS", @"(\[A-Z]\)+");

您会注意到第一个Group 将有一个Captures(第一组始终是整场比赛,即等于$0),第二组将拥有S,即只有最后一个匹配组。但是,这里有一个问题,如果你想找到另外两个 catch,它们在 Captures 中,其中包含 Q RS 的所有中间捕获。

如果您想知道如何从多重捕获中获取信息,它只显示字符串中明显存在的单个捕获的最后匹配项,您必须使用Captures

关于你最后一个问题的最后一句话:总匹配总是有一个总捕获,不要将它与单个组混合。捕获仅在组内有趣

【讨论】:

a functionality that won't be used in the majority of cases 我想他错过了船。在短期内(?:.*?(collection info))4,20 将效率提高了百分之几。 @sln,不确定您指的是什么以及“他”是谁(弗里德尔?)。您给出的示例似乎与此讨论或使用的表达无关。此外,非贪心量词比贪心量词更有效,并且需要了解输入集和仔细的性能测试。 @Abel - 我从一个标记为重复的问题来到这里。我看到弗里德尔引用了。这篇文章很旧,需要更新以保持现代感。只有使用 Dot Net 才能做到这一点,这就是它与大多数其他人的不同之处。分解:一个量化的非捕获整体组示例(?:..)+。延迟匹配任何 .*? 直到捕获子表达式(组)。继续。在单个匹配中,组集合会沉淀出一系列所需的内容。无需查找下一个,无需重新进入,使其速度提高 10 到 20 倍或更多。 @sln,这个问题是关于其他事情的,特别是关于其他正则表达式引擎中没有的 .net 功能(组与捕获,见标题)。我在这里没有看到任何过时的东西,.net 仍然可以正常工作,实际上这部分在 .net 中很长一段时间都没有改变。性能不是问题的一部分。是的,非捕获分组更快,但同样,这里的主题是相反的。为什么贪婪比懒惰更快在许多在线文本和弗里德尔的书中都有解释,但这里是 OT。也许另一个问题(哪个?)不是真正的重复? @Abel - 我知道我一直在说,但你一直没有听到。我对 Friedl a functionality that won't be used in the majority of cases 的声明感到不满。事实上,它是正则表达式领域中最受追捧的功能。懒惰/贪婪?这和我的cmets有什么关系?它允许拥有可变数量的捕获缓冲区。它可以在单个匹配中扫描整个字符串。如果.*?(dog) 找到第一个dog,那么(?:.*?(dog))+ 将在单个匹配中的整个字符串中找到all dog。性能提升很明显。【参考方案2】:

这可以用一个简单的例子(和图片)来解释。

匹配3:10pm与正则表达式((\d)+):((\d)+)(am|pm),并使用Mono交互csharp

csharp> Regex.Match("3:10pm", @"((\d)+):((\d)+)(am|pm)").
      > Groups.Cast<Group>().
      > Zip(Enumerable.Range(0, int.MaxValue), (g, n) => "[" + n + "] " + g);
 "[0] 3:10pm", "[1] 3", "[2] 3", "[3] 10", "[4] 0", "[5] pm" 

那么 1 在哪里?

由于在第四组中有多个匹配的数字,因此如果我们引用该组(即使用隐式 ToString()),我们只会“获取”最后一个匹配项。为了暴露中间匹配,我们需要更深入地引用相关组的Captures 属性:

csharp> Regex.Match("3:10pm", @"((\d)+):((\d)+)(am|pm)").
      > Groups.Cast<Group>().
      > Skip(4).First().Captures.Cast<Capture>().
      > Zip(Enumerable.Range(0, int.MaxValue), (c, n) => "["+n+"] " + c);
 "[0] 1", "[1] 0" 

感谢this article。

【讨论】:

好文章。一图胜千言。 你是明星。【参考方案3】:

组是我们在正则表达式中与组相关联的内容

"(a[zx](b?))"

Applied to "axb" returns an array of 3 groups:

group 0: axb, the entire match.
group 1: axb, the first group matched.
group 2: b, the second group matched.

除了这些只是“捕获”组。非捕获组(使用 '(?: ' 语法未在此处表示。

"(a[zx](?:b?))"

Applied to "axb" returns an array of 2 groups:

group 0: axb, the entire match.
group 1: axb, the first group matched.

捕获也是我们与“捕获的组”相关联的内容。但是当组多次使用量词时,只有最后一个匹配被保留为组的匹配。 captures 数组存储所有这些匹配项。

"(a[zx]\s+)+"

Applied to "ax az ax" returns an array of 2 captures of the second group.

group 1, capture 0 "ax "
group 1, capture 1 "az "

至于你的最后一个问题——在研究这个问题之前,我会认为 Captures 将是一个按它们所属的组排序的捕获数组。相反,它只是 groups[0].Captures 的别名。太没用了。。

【讨论】:

明确解释(y)【参考方案4】:

来自 MSDN documentation:

Captures 属性的真正用途是在将量词应用于捕获组时,以便该组在单个正则表达式中捕获多个子字符串。在这种情况下,Group 对象包含有关最后捕获的子字符串的信息,而 Captures 属性包含有关该组捕获的所有子字符串的信息。在以下示例中,正则表达式 \b(\w+\s*)+。匹配以句点结尾的整个句子。组 (\w+\s*)+ 捕获集合中的单个单词。因为 Group 集合仅包含有关最后捕获的子字符串的信息,所以它捕获句子中的最后一个单词“sentence”。但是,该组捕获的每个单词都可以从 Captures 属性返回的集合中获得。

【讨论】:

【参考方案5】:

假设您有以下文本输入 dogcatcatcat 和类似 dog(cat(catcat)) 的模式

在这种情况下,您有 3 个组,第一个(主要组)对应于匹配项。

匹配 == dogcatcatcat 和 Group0 == dogcatcatcat

Group1 == catcatcat

Group2 == catcat

那到底是怎么回事?

让我们考虑一个使用 Regex 类用 C# (.NET) 编写的小示例。

int matchIndex = 0;
int groupIndex = 0;
int captureIndex = 0;

foreach (Match match in Regex.Matches(
        "dogcatabcdefghidogcatkjlmnopqr", // input
        @"(dog(cat(...)(...)(...)))") // pattern
)

    Console.Out.WriteLine($"matchmatchIndex++ = match");

    foreach (Group @group in match.Groups)
    
        Console.Out.WriteLine($"\tgroupgroupIndex++ = @group");

        foreach (Capture capture in @group.Captures)
        
            Console.Out.WriteLine($"\t\tcapturecaptureIndex++ = capture");
        

        captureIndex = 0;
    

    groupIndex = 0;
    Console.Out.WriteLine();
        

输出

match0 = dogcatabcdefghi
    group0 = dogcatabcdefghi
        capture0 = dogcatabcdefghi
    group1 = dogcatabcdefghi
        capture0 = dogcatabcdefghi
    group2 = catabcdefghi
        capture0 = catabcdefghi
    group3 = abc
        capture0 = abc
    group4 = def
        capture0 = def
    group5 = ghi
        capture0 = ghi

match1 = dogcatkjlmnopqr
    group0 = dogcatkjlmnopqr
        capture0 = dogcatkjlmnopqr
    group1 = dogcatkjlmnopqr
        capture0 = dogcatkjlmnopqr
    group2 = catkjlmnopqr
        capture0 = catkjlmnopqr
    group3 = kjl
        capture0 = kjl
    group4 = mno
        capture0 = mno
    group5 = pqr
        capture0 = pqr

让我们只分析第一个匹配项 (match0)。

如您所见,共有三个次要组group3group4group5

    group3 = kjl
        capture0 = kjl
    group4 = mno
        capture0 = mno
    group5 = pqr
        capture0 = pqr

这些组 (3-5) 是由于 主模式 (dog(cat(...)(...)(...)))

的“子模式(...)(...)(...) 而创建的

group3 的值对应于它的捕获 (capture0)。 (如group4group5 的情况)。那是因为没有像(...)3 这样的组重复


好的,让我们考虑另一个组重复的例子。

如果我们修改要匹配的正则表达式模式(如上所示的代码) 从(dog(cat(...)(...)(...)))(dog(cat(...)3)), 您会注意到有以下组重复(...)3

现在输出发生了变化:

match0 = dogcatabcdefghi
    group0 = dogcatabcdefghi
        capture0 = dogcatabcdefghi
    group1 = dogcatabcdefghi
        capture0 = dogcatabcdefghi
    group2 = catabcdefghi
        capture0 = catabcdefghi
    group3 = ghi
        capture0 = abc
        capture1 = def
        capture2 = ghi

match1 = dogcatkjlmnopqr
    group0 = dogcatkjlmnopqr
        capture0 = dogcatkjlmnopqr
    group1 = dogcatkjlmnopqr
        capture0 = dogcatkjlmnopqr
    group2 = catkjlmnopqr
        capture0 = catkjlmnopqr
    group3 = pqr
        capture0 = kjl
        capture1 = mno
        capture2 = pqr

再次,让我们只分析第一场比赛 (match0)。

由于(...)3 重复n其中,没有更多次要组group4group5 >n>=2) 他们已合并为一个组group3

在这种情况下,group3 值对应于它的capture2最后一次捕获,换句话说)。

因此,如果您需要所有 3 个内部捕获(capture0capture1capture2),则必须循环浏览该组的 Captures 集合。

总结是:注意您设计模式组的方式。 您应该预先考虑是什么行为导致了组的规范,例如(...)(...)(...)2(.3)2 等。


希望它也有助于阐明CapturesGroupsMatches之间的区别。

【讨论】:

以上是关于.NET 正则表达式中的“组”和“捕获”有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式中的命名捕获组

正则表达式、linq表达式、lambda 表达式区别 ,这3者有啥关系和区别么?

什么是正则表达式“独立非捕获组”?

具有多个捕获组的 R 中的正则表达式组捕获

正则表达式 [REGEX] - 替换/替换 - 捕获组 1 和 2 中的内容

SQL查找和替换正则表达式捕获组限制?