我只是在阅读一个关于如何在双花括号内获取数据的问题(这个问题),然后有人提出了平衡组。我仍然不太确定它们是什么以及如何使用它们。
我通读了平衡组定义,但解释很难理解,而且我对我提到的问题仍然很困惑。
有人可以简单地解释什么是平衡组以及它们如何有用吗?
据我所知,平衡组是 .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
因此,将所有这些东西放在一起,我们可以:
全部在一个正则表达式中。如果那不令人兴奋... ;)
当我第一次了解它们时,我发现一些有用的资源:
只是 M. Buettner 出色答案的一个小补充:
(?<A-B>)
语法有什么问题?(?<A-B>x)
与 略有不同(?<-A>(?<B>x))
。它们产生相同的控制流*,但捕获方式不同。
例如,让我们看一下平衡大括号的模式:
(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))
在比赛结束时,我们确实有一个平衡的字符串,但这就是我们所拥有的 - 我们不知道大括号在哪里,因为堆栈B
是空的。引擎为我们所做的辛勤工作已经一去不复返了。
(正则表达式风暴示例)
(?<A-B>x)
是该问题的解决方案。如何?它不捕获x
into $A
:它捕获上一次捕获B
和当前位置之间的内容。
让我们在我们的模式中使用它:
(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))
$Content
对于沿途的每一对,这将捕获到大括号(及其位置)之间的字符串中。
对于字符串{1 2 {3} {4 5 {6}} 7}
,将有四个捕获:3
、6
、4 5 {6}
和1 2 {3} {4 5 {6}} 7
- 比没有或. 好得多}
}
}
}
。
(例如-单击table
选项卡并查看${Content}
,捕获)
事实上,它完全可以在没有平衡的情况下使用:(?<A>).(.(?<Content-A>).)
捕获前两个字符,即使它们是按组分隔的。
(前瞻在这里更常用,但它并不总是可扩展的:它可能会重复您的逻辑。)
(?<A-B>)
是一个强大的功能 - 它可以让您精确控制您的捕获。当你试图从你的模式中获得更多时,请记住这一点。