98

我只是在阅读一个关于如何在双花括号内获取数据的问题(这个问题),然后有人提出了平衡组。我仍然不太确定它们是什么以及如何使用它们。

我通读了平衡组定义,但解释很难理解,而且我对我提到的问题仍然很困惑。

有人可以简单地解释什么是平衡组以及它们如何有用吗?

4

2 回答 2

187

据我所知,平衡组是 .NET 的正则表达式所独有的。

旁白:重复组

首先,您需要知道 .NET 是(再次,据我所知)唯一允许您访问单个捕获组的多个捕获的正则表达式风格(不是在反向引用中,而是在匹配完成之后)。

为了举例说明这一点,请考虑模式

(.)+

和字符串"abcd"

在所有其他正则表达式风格中,捕获组1只会产生一个结果:(d注意,完全匹配当然会abcd如预期的那样)。这是因为捕获组的每次新使用都会覆盖以前的捕获。

另一方面,.NET 会记住所有这些。它在堆栈中这样做。在匹配上面的正则表达式之后

Match m = new Regex(@"(.)+").Match("abcd");

你会发现

m.Groups[1].Captures

是一个CaptureCollection其元素对应于四个捕获

0: "a"
1: "b"
2: "c"
3: "d"

其中数字是CaptureCollection. 所以基本上每次再次使用该组时,都会将一个新的捕获推入堆栈。

如果我们使用命名的捕获组,它会变得更有趣。因为 .NET 允许重复使用相同的名称,我们可以编写一个正则表达式,例如

(?<word>\w+)\W+(?<word>\w+)

将两个单词捕获到同一组中。同样,每次遇到具有特定名称的组时,都会将捕获推送到其堆栈中。所以将此正则表达式应用于输入"foo bar"并检查

m.Groups["word"].Captures

我们找到两个捕获

0: "foo"
1: "bar"

这使我们甚至可以从表达式的不同部分将内容推送到单个堆栈中。但是,这只是 .NET 能够跟踪此 .NET 中列出的多个捕获的功能CaptureCollection。但我说,这个集合是一个堆栈。那么我们可以从中弹出 东西吗?

输入:平衡组

事实证明我们可以。如果我们使用类似 的组,则如果子表达式匹配(?<-word>...),则从堆栈中弹出最后一个捕获。因此,如果我们将之前的表达式更改为word...

(?<word>\w+)\W+(?<-word>\w+)

然后第二组会弹出第一组的捕获,CaptureCollection最后我们会收到一个空的。当然,这个例子是毫无用处的。

但是减号语法还有一个细节:如果堆栈已经为空,则组失败(无论其子模式如何)。我们可以利用这种行为来计算嵌套级别——这就是名称平衡组的来源(以及它变得有趣的地方)。假设我们要匹配正确括起来的字符串。我们将每个左括号压入堆栈,并为每个右括号弹出一个捕获。如果我们遇到一个右括号太多,它会尝试弹出一个空堆栈并导致模式失败:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$

所以我们有三个重复的选择。第一种选择消耗所有不是括号的东西。第二种选择匹配(s,同时将它们推入堆栈。第三种选择匹配)s,同时从堆栈中弹出元素(如果可能!)。

注意:为了澄清,我们只检查没有不匹配的括号!这意味着根本不包含括号的字符串匹配,因为它们在语法上仍然有效(在某些需要括号匹配的语法中)。如果您想确保至少有一组括号,只需(?=.*[(])^.

但是,这种模式并不完美(或完全正确)。

结局:条件模式

还有一个问题:这并不能确保堆栈在字符串末尾是空的(因此(foo(bar)是有效的)。.NET(和许多其他风格)还有另一种可以帮助我们的构造:条件模式。一般语法是

(?(condition)truePattern|falsePattern)

其中falsePattern是可选的 - 如果省略它,则 false-case 将始终匹配。条件可以是模式,也可以是捕获组的名称。我将在这里重点讨论后一种情况。如果它是捕获组的名称,则truePattern当且仅当该特定组的捕获堆栈不为空时才使用。也就是说,一个条件模式,如(?(name)yes|no)“如果name匹配并捕获了某些东西(仍在堆栈上),则使用模式,yes否则使用模式no”。

因此,在上述模式的末尾,如果-stack 不为​​空,我们可以添加类似(?(Open)failPattern)的内容,从而导致整个模式失败。Open使模式无条件失败的最简单的事情是(?!)(空的否定前瞻)。所以我们有我们的最终模式:

^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$

请注意,这种条件语法本身与平衡组无关,但有必要利用它们的全部力量。

从这里开始,天空就是极限。许多非常复杂的用途是可能的,并且在与其他 .NET-Regex 功能(例如可变长度后视)结合使用时会出现一些问题(我必须自己努力学习)。然而,主要问题始终是:使用这些功能时,您的代码是否仍然可维护?你需要很好地记录它,并确保每个使用它的人都知道这些特性。否则你可能会更好,只需手动逐个字符地遍历字符串并以整数计算嵌套级别。

附录:(?<A-B>...)语法是什么?

这部分的学分归 Kobi 所有(有关更多详细信息,请参阅下面的答案)。

现在有了以上所有内容,我们可以验证一个字符串是否正确括起来。但是,如果我们实际上可以(嵌套)捕获所有这些括号的内容,它会更有用。当然,我们可以记住在未清空的单独捕获堆栈中打开和关闭括号,然后在单独的步骤中根据它们的位置进行一些子字符串提取。

但是 .NET 在这里提供了一个更方便的特性:如果我们使用(?<A-B>subPattern),不仅从 stack 中弹出一个捕获B,而且在弹出的捕获B和当前组之间的所有内容都被推送到 stack 上A。因此,如果我们使用这样的组作为右括号,同时从堆栈中弹出嵌套级别,我们还可以将对的内容推送到另一个堆栈:

^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$

Kobi在他的回答中提供了这个Live-Demo

因此,将所有这些东西放在一起,我们可以:

  • 记住任意多个捕获
  • 验证嵌套结构
  • 捕获每个嵌套级别

全部在一个正则表达式中。如果那不令人兴奋... ;)

当我第一次了解它们时,我发现一些有用的资源:

于 2013-06-08T22:16:06.020 回答
44

只是 M. Buettner 出色答案的一个小补充:

(?<A-B>)语法有什么问题?

(?<A-B>x)与 略有不同(?<-A>(?<B>x))。它们产生相同的控制流*,但捕获方式不同。
例如,让我们看一下平衡大括号的模式:

(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))

在比赛结束时,我们确实有一个平衡的字符串,但这就是我们所拥有的 - 我们不知道大括号在哪里,因为堆栈B是空的。引擎为我们所做的辛勤工作已经一去不复返了。
正则表达式风暴示例

(?<A-B>x)是该问题的解决方案。如何?它捕获xinto $A:它捕获上一次捕获B和当前位置之间的内容。

让我们在我们的模式中使用它:

(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))

$Content对于沿途的每一对,这将捕获到大括号(及其位置)之间的字符串中。
对于字符串{1 2 {3} {4 5 {6}} 7},将有四个捕获:364 5 {6}1 2 {3} {4 5 {6}} 7- 比没有或. 好得多} } } }
例如-单击table选项卡并查看${Content},捕获

事实上,它完全可以在没有平衡的情况下使用:(?<A>).(.(?<Content-A>).)捕获前两个字符,即使它们是按组分隔的。
(前瞻在这里更常用,但它并不总是可扩展的:它可能会重复您的逻辑。)

(?<A-B>)是一个强大的功能 - 它可以让您精确控制您的捕获。当你试图从你的模式中获得更多时,请记住这一点。

于 2013-06-09T17:52:18.557 回答