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

时间:2013-06-08 21:00:11

标签: c# .net regex balancing-groups

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

我通读了Balancing Group Definition,但解释很难理解,而且我对我提到的问题仍感到困惑。

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

2 个答案:

答案 0 :(得分:164)

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

旁边:重复的小组

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

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

(.)+

和字符串"abcd"

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

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

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

你会发现

m.Groups[1].Captures

CaptureCollection,其元素对应于四个捕获

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

其中number是CaptureCollection的索引。因此,基本上每次再次使用该组时,都会将新的捕获推送到堆栈中。

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

这样的正则表达式
(?<word>\w+)\W+(?<word>\w+)

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

m.Groups["word"].Captures

我们找到两个捕获

0: "foo"
1: "bar"

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

输入:平衡组

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

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

然后第二组将弹出第一组的捕获,最后我们将收到一个空的CaptureCollection。当然,这个例子很没用。

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

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

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

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

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

结局:条件模式

还有一个问题:这不能确保字符串末尾的堆栈为空(因此(foo(bar)有效)。 .NET(以及许多其他版本)还有一个结构可以帮助我们:条件模式。一般语法是

(?(condition)truePattern|falsePattern)

其中falsePattern是可选的 - 如果省略,则false-case将始终匹配。条件可以是模式,也可以是捕获组的名称。我会在这里关注后一种情况。如果它是捕获组的名称,则当且仅当该特定组的捕获堆栈不为空时才使用truePattern。也就是说,像(?(name)yes|no)这样的条件模式读取&#34;如果name匹配并捕获了某些东西(仍在堆栈中),则使用模式yes否则使用模式{{1 }}&#34;

因此,在我们上面的模式结束时,如果no - 堆栈不为空,我们可以添加类似(?(Open)failPattern)的内容,这会导致整个模式失败。使模式无条件失败的最简单方法是Open(空的负向前瞻)。所以我们有最终模式:

(?!)

请注意,此条件语法本身与平衡组没有任何关系,但它必须充分利用它们。

从这里开始,天空就是极限。许多非常复杂的用途是可能的,当与其他.NET-Regex功能(如可变长度的lookbehinds(which I had to learn the hard way myself))结合使用时,会有一些问题。但问题始终是:使用这些功能时,您的代码是否仍然可维护?您需要非常好地记录它,并确保每个使用它的人都知道这些功能。否则你可能会更好,只需逐个字符地手动操作字符串并计算整数中的嵌套级别。

附录:^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$ 语法有什么用?

此部分的学分转到Kobi(有关详细信息,请参阅下面的答案)。

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

但.NET提供了一个更方便的功能:如果我们使用(?<A-B>...),不仅会从堆栈(?<A-B>subPattern)中弹出捕获,而且还会弹出捕获B和此当前组被推送到堆栈B。因此,如果我们使用这样的组作为结束括号,同时从我们的堆栈中弹出嵌套级别,我们也可以将该对的内容推送到另一个堆栈:

A

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

所以把所有这些东西放在一起我们可以:

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

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

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

答案 1 :(得分:38)

只是M. Buettner的优秀答案的一小部分补充:

(?<A-B>)语法的处理方式是什么?

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

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

在比赛结束时我们有一个平衡的字符串,但这就是我们所拥有的 - 我们不知道其中大括号是因为B堆栈是空的。引擎为我们所做的艰苦工作已经消失 <子>(example on 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 - 比 nothing 或} } } } <子>(example - click the table tab and look at ${Content}, captures

事实上,它可以在没有平衡的情况下使用:(?<A>).(.(?<Content-A>).)捕获前两个字符,即使它们被组分开。
(这里更常用的是先行,但它并不总是缩放:它可能会复制你的逻辑。)

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