据我所知,平衡组是 .NET 的正则表达式风格所独有的。
旁白:重复组
首先,您需要知道 .NET 是(再次据我所知)唯一允许您访问单个捕获组的多个捕获的正则表达式风格(不是在反向引用中,而是在匹配完成后)。
为了通过示例来说明这一点,请考虑以下模式
(.)+
和字符串"abcd"
.
在所有其他正则表达式风格中,捕获组1
只会产生一个结果:d
(注意,完整的比赛当然是abcd
正如预期的那样)。这是因为捕获组的每次新使用都会覆盖以前的捕获。
另一方面,.NET 会记住所有这些。它是在堆栈中执行的。匹配上面的正则表达式后
Match m = new Regex(@"(.)+").Match("abcd");
你会发现
m.Groups[1].Captures
Is a 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
。但我说过,这个系列是stack。那么我们可以pop东西来自吗?
输入:平衡组
事实证明我们可以。如果我们使用像这样的组(?<-word>...)
,然后从堆栈中弹出最后一个捕获word
如果子表达式...
火柴。因此,如果我们将之前的表达式更改为
(?<word>\w+)\W+(?<-word>\w+)
然后第二组将弹出第一组的捕获,我们将收到一个空的CaptureCollection
到底。当然,这个例子没什么用。
但减号语法还有一个细节:如果堆栈已经为空,则该组将失败(无论其子模式如何)。我们可以利用这种行为来计算嵌套级别 - 这就是平衡组名称的来源(也是它变得有趣的地方)。假设我们想要匹配正确加括号的字符串。我们将每个左括号压入堆栈,并为每个右括号弹出一个捕获。如果我们遇到太多的右括号,它将尝试弹出一个空堆栈并导致模式失败:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$
所以我们在重复过程中有三种选择。第一种选择消耗除括号之外的所有内容。第二个替代匹配(
s 同时将它们推入堆栈。第三个替代匹配)
s 同时从堆栈中弹出元素(如果可能的话!)。
Note: Just to clarify, we're only checking that there are no unmatched parentheses! This means that string containing no parentheses at all will match, because they are still syntactically valid (in some syntax where you need your parentheses to match). If you want to ensure at least one set of parentheses, simply add a lookahead (?=.*[(])
right after the ^
.
但这种模式并不完美(或完全正确)。
结局:条件模式
还有一个问题:这并不能确保字符串末尾的堆栈为空(因此(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>...)
syntax?
这部分的功劳归于 Kobi(有关更多详细信息,请参阅下面他的回答)。
现在有了上述所有内容,我们可以验证字符串是否正确加括号。但如果我们能够真正获得所有这些括号内容的(嵌套)捕获,那么它会更有用。当然,我们可以记住在未清空的单独捕获堆栈中打开和关闭括号,然后在单独的步骤中根据它们的位置进行一些子字符串提取。
但是 .NET 在这里提供了一项更方便的功能:如果我们使用(?<A-B>subPattern)
,不仅从堆栈中弹出捕获B
,还有弹出捕获之间的所有内容B
并且当前组被推入堆栈A
。因此,如果我们使用这样的组作为右括号,同时从堆栈中弹出嵌套级别,我们还可以将这对内容推送到另一个堆栈上:
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
Kobi provided this Live-Demo in his answer
因此,将所有这些事情放在一起,我们可以:
全部都在一个正则表达式中。如果这不令人兴奋......;)
当我第一次了解它们时,我发现一些有用的资源:
- http://blog.stevenlevithan.com/archives/balancing-groups
- MSDN 关于平衡组
- MSDN 关于条件模式
-
http://kobikobi.wordpress.com/tag/balancing-group/(有点学术性,但有一些有趣的应用)