我刚刚读到一个关于如何在双花括号中获取数据的问题(这个问题),然后有人提出了平衡组。我仍然不太清楚它们是什么以及如何使用它们。
我通读了平衡组的定义,但解释很难理解,我仍然对我提到的问题感到困惑。
有人能简单地解释一下什么是平衡组以及它们是如何有用的吗?
据我所知,平衡小组是独一无二的。NET的regex风格。
首先,你需要知道这一点。NET是(据我所知)唯一一个允许您访问单个捕获组的多个捕获的正则表达式(不是在反向引用中,而是在匹配完成后)。
为了举例说明这一点,考虑模式
(.)+
和字符串"abcd"
。
在所有其他regex风格中,捕获组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>\w+)\W+(?<-word>\w+)
然后第二组将弹出第一组的捕获,最后我们将收到一个空的CaptureCollection
。当然,这个例子毫无用处。
但是减号语法还有一个细节:如果堆栈已经为空,则组失败(不管其子模式如何)。我们可以利用这种行为来计算嵌套级别——这就是名称平衡组的来源(也是它变得有趣的地方)。假设我们要匹配正确插入括号的字符串。我们在堆栈上推送每个左括号,并为每个右括号弹出一个捕获。如果我们遇到一个右括号过多,它将尝试弹出一个空堆栈并导致模式失败:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*$
所以我们有三个重复的选择。第一种选择消耗了所有不是括号的东西。第二个选项匹配(
s,同时将它们推送到堆栈上。第三个选项匹配)
s,同时从堆栈中弹出元素(如果可能的话!)。
注意:为了澄清,我们只是检查没有不匹配的括号!这意味着不包含括号的字符串将匹配,因为它们在语法上仍然有效(在某些语法中,您需要括号来匹配)。如果您想确保至少有一组括号,只需添加一个lookfirst(?=.*[(])
就在^
之后。
不过,这种模式并不完美(或完全正确)。
还有一个问题:这不能确保堆栈在字符串末尾是空的(因此(foo(bar)
将是有效的)。NET(和许多其他风格)还有一个结构可以帮助我们:条件模式。一般语法是
(?(condition)truePattern|falsePattern)
其中FalsePattern
是可选的-如果省略,false大小写将始终匹配。条件可以是模式,也可以是捕获组的名称。我将在这里重点讨论后一种情况。如果它是捕获组的名称,则当且仅当该特定组的捕获堆栈不为空时才使用truePattern
。也就是说,像(?(name)yes|no)
读取"如果name
匹配并捕获了某些东西(仍然在堆栈上),则使用模式yes
否则使用模式no
"。
因此,在上述模式的末尾,我们可以添加类似于(?如果打开堆栈不是空的,则导致整个模式失败。使模式无条件失败的最简单的事情是
(?!)
(一个空的否定前瞻)。所以我们有了最后的模式:
^(?:[^()]|(?<Open>[(])|(?<-Open>[)]))*(?(Open)(?!))$
请注意,此条件语法本身与平衡组无关,但有必要充分利用它们的功能。
从这里开始,天空就是极限。许多非常复杂的用途是可能的,当与其他用途结合使用时,会有一些陷阱。NET-Regex功能,如可变长度lookbehinds(这是我自己学习的艰难方式)。然而,主要问题始终是:当使用这些功能时,您的代码仍然可以维护吗?你需要很好地记录它,并确保每个从事它的人也意识到这些特性。否则,您可能会更好,只需逐个字符手动遍历字符串,并以整数计算嵌套级别。
这部分的学分归Kobi所有(更多细节见下面的答案)。
现在,通过以上所有这些,我们可以验证字符串是否正确地加了括号。但是如果我们能得到所有这些括号内容的(嵌套的)捕获,它会更有用。当然,我们可以记住在未清空的单独捕获堆栈中打开和关闭括号,然后在单独的步骤中根据它们的位置执行一些子串提取。
^(?:[^()]|(?<Open>[(])|(?<Content-Open>[)]))*(?(Open)(?!))$
Kobi在他的回答中提供了这个现场演示
所以把所有这些东西放在一起,我们可以:
都在一个正则表达式中。如果这不令人兴奋的话……;)
当我第一次了解这些资源时,我发现它们很有帮助:
只是对M.Buettner极好的回答的一点补充:
(?
(?:[^{}]|(?<B>{)|(?<-B>}))+(?(B)(?!))
在匹配结束时,我们确实有一个平衡的字符串,但这就是我们所拥有的全部——我们不知道大括号在哪里,因为B
堆栈是空的。
(Regex Storm上的例子)
(?
让我们在模式中使用它:
(?:[^{}]|(?<Open>{)|(?<Content-Open>}))+(?(Open)(?!))
这将在$Content
中捕获大括号(及其位置)之间的字符串,用于沿途的每对大括号
对于字符串{1 2{3}{4 5{6}}}7}
有四个捕获:3
,6
,4 5{6}
,和1 2{3}{4 5{6}7
-比没有或}好得多
(示例-单击表
选项卡并查看${Content}
,捕获)
事实上,它可以在完全不平衡的情况下使用:(?
(?