正则表达式平衡组是什么?

102

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

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

请问有人能简单地解释一下平衡组是什么以及它们如何有用吗?


我想知道有多少正则表达式引擎实际上支持这个。 - Mike de Klerk
2
@MikedeKlerk 至少在 .NET 正则表达式引擎中支持该功能。 - It'sNotALie.
2个回答

196
据我所知,平衡组是.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能够跟踪多个捕获的功能,这些捕获列在CaptureCollection中。但是我说过,这个集合是一个堆栈。那么我们可以从其中弹出东西吗?

进入:平衡组

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

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

然后第二个组将弹出第一个组的捕获,最终我们将得到一个空的CaptureCollection。当然,这个示例并没有什么用处。 但是,减号语法还有一个细节:如果堆栈已经为空,则该组失败(无论其子模式如何)。我们可以利用这种行为来计算嵌套级别 - 这就是名称平衡组的由来(以及它变得有趣的地方)。假设我们想匹配正确括号的字符串。我们将每个开括号推入堆栈,并为每个闭括号弹出一个捕获。如果我们遇到了太多的闭括号,它将尝试弹出一个空堆栈并导致模式失败:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$

在重复中,我们有三种选择。第一种选择消耗所有不是括号的字符。第二个选择匹配包含 ( 的内容,并将其推送到堆栈中。第三个选择匹配包含 ) 的内容,并从堆栈中弹出元素(如果可能)。

注意:澄清一下,我们只检查未匹配的括号是否存在!这意味着不包含任何括号的字符串仍然会匹配,因为它们在某些语法中仍然是语法上有效的(其中需要括号匹配)。如果要确保至少有一个圆括号,请在^后面添加前瞻(?=.*[(])

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

最终章:条件模式

还有一个问题:这不能保证字符串末尾堆栈为空(因此 (foo(bar) 也是有效的)。.NET(和许多其他风格)有一个更多的结构来帮助我们:条件模式。一般语法如下:

(?(condition)truePattern|falsePattern)

falsePattern是可选的,如果省略,则false-case始终匹配。条件可以是模式,也可以是捕获组的名称。在此我将重点介绍后一种情况。 如果它是捕获组的名称,则仅当该特定组的捕获堆栈不为空时才使用truePattern。也就是说,像(?(name)yes|no)这样的条件模式读作“如果name已匹配并捕获了某些内容(仍然在堆栈上),则使用模式yes,否则使用模式no”。

因此,在我们上面的模式末尾,我们可以添加类似于(?(Open)failPattern)的内容,如果Open-堆栈不为空,则导致整个模式失败。使模式无条件失败的最简单方法是使用(?!)(空的负向先行断言)。因此,我们有了最终的模式:

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

请注意,这种条件语法本身与平衡组无关,但是必须利用它们的全部功能。
从这里开始,天空就是极限。许多非常复杂的用途是可能的,当与其他.NET-Regex功能(如变长回溯我自己也不得不艰难地学习)结合使用时,可能会有一些陷阱。然而,主要问题始终是:在使用这些功能时,您的代码是否仍然可维护?您需要对其进行充分的文档说明,并确保每个使用它的人也知道这些功能。否则,您最好手动逐个字符遍历字符串并计算嵌套级别。
附录: (?<A-B>...) 语法是什么?
此部分的功劳归功于Kobi(有关更多详细信息,请参见他下面的答案)。
现在,我们可以验证字符串是否正确地使用了括号。但如果我们能够获取所有这些括号内容的嵌套捕获,那将更加有用。当然,我们可以记住开放和闭合括号,在一个不被清空的单独捕获堆栈中,并且基于它们在单独步骤中的位置进行一些子串提取。
但是,.NET 在这里提供了更多的便利功能:如果我们使用 (?<A-B>subPattern),不仅从堆栈 B 中弹出一个捕获,而且还将从弹出的 B 捕获到当前组之间的所有内容推送到堆栈 A 上。因此,如果我们对于闭合括号使用这样的组,同时从我们的堆栈中弹出嵌套级别,我们也可以将该对内容推送到另一个堆栈上:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$

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

因此,我们可以将所有这些内容结合起来:

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

所有这些都可以在单个正则表达式中完成。如果这不令人兴奋... ;)

我第一次学习它们时发现有用的一些资源:


8
这个答案已被添加到Stack Overflow正则表达式FAQ中,标记为“高级Regex技巧”。 - aliteralmind

45

对M. Buettner卓越答案的一个小补充:

(?<A-B>)语法是什么意思?

(?<A-B>x)(?<-A>(?<B>x))微妙地不同,它们产生相同的控制流程*,但它们的捕获方式不同。
例如,让我们看一个匹配平衡括号的模式:

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

比赛结束时,我们确实有一个平衡的字符串,但那就是全部——因为 B 堆栈为空,所以我们不知道括号在哪里。引擎为我们完成的努力已经化为乌有。
(Regex Storm 上的示例)

(?<A-B>x) 就是解决这个问题的方案。怎么做呢?它不会x 捕获到 $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>) 是一个强大的功能 - 它可以精确控制您的捕获。在尝试从模式中获取更多信息时,请记住这一点。


@FYI,继续从你不喜欢的问题的讨论中,在这个新答案中继续。 :) - zx81
我正在尝试找出一种方法来执行平衡括号正则表达式检查,并在字符串内转义括号。例如,以下代码将通过:public class Foo { private const char BAR = '{'; private string _qux = "{{{"; }有人做过这个吗? - Mr Anderson
@MrAnderson - 你只需要在正确的位置添加 |'[^']*'example。如果你还需要转义字符,这里有一个例子:(Regex for matching C# string literals)[https://dev59.com/71TTa4cB1Zd3GeqPqDBa#4953878]。 - Kobi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接