据我所知,平衡组是 .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 中列出的多个捕获的功能。但我说过,这个集合是一个堆栈。那么我们可以从中pop 东西吗?
输入:平衡组
事实证明我们可以。如果我们使用像(?<-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”。
所以在上述模式的末尾,如果Open-stack 不为空,我们可以添加类似(?(Open)failPattern) 的内容,这会导致整个模式失败。使模式无条件失败的最简单方法是(?!)(一个空的否定前瞻)。所以我们有了最终的模式:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$
请注意,这种条件语法本身与平衡组无关,但有必要充分利用它们。
从这里开始,天空就是极限。许多非常复杂的用途是可能的,并且在与其他 .NET-Regex 功能(如可变长度后视 (which I had to learn the hard way myself) 结合使用时存在一些问题。然而,主要问题始终是:使用这些功能时,您的代码是否仍然可维护?你需要很好地记录它,并确保每个使用它的人都知道这些特性。否则你可能会更好,只需手动逐个字符地遍历字符串并以整数计算嵌套级别。
附录:(?<A-B>...) 语法是什么?
这部分的功劳归科比所有(有关详细信息,请参阅下面的答案)。
现在有了以上所有内容,我们可以验证一个字符串是否正确加了括号。但是,如果我们实际上可以(嵌套)捕获所有这些括号的内容,它会更有用。当然,我们可以记住在未清空的单独捕获堆栈中打开和关闭括号,然后在单独的步骤中根据它们的位置进行一些子字符串提取。
但是.NET在这里提供了一个更方便的特性:如果我们使用(?<A-B>subPattern),不仅会从堆栈B中弹出一个捕获,而且还会从弹出的B捕获和当前组之间的所有内容推送到堆栈A。因此,如果我们使用这样的组作为右括号,同时从堆栈中弹出嵌套级别,我们还可以将对的内容推送到另一个堆栈:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
Kobi 在他的回答中提供了这个Live-Demo
因此,将所有这些东西放在一起,我们可以:
全部在一个正则表达式中。如果那不令人兴奋... ;)
当我第一次了解这些资源时,我发现它们很有帮助: