是否可以在不使用递归或平衡组的情况下将嵌套括号与正则表达式进行匹配



问题:匹配正则表达式风格中任意嵌套的括号组,例如Java的Java.util.regex,它既不支持递归也不支持平衡组。即,在中匹配三个外部组

(F(i(r(s)t))((s)(e)((c)(o))(n)d)

这个练习纯粹是学术性的,因为我们都知道正则表达式不应该用来匹配这些东西,就像Q-tips不应该用来清理耳朵一样。

Stack Overflow鼓励自我回答问题,所以我决定创建这篇文章来分享我最近发现的一些东西

确实!可以使用正向引用:

(?=()(?:(?=.*?((?!.*?1)(.*)(?!.*2).*))(?=.*?)(?!.*?2)(.*)).)+?.*?(?=1)[^(]*(?=2$)

证明

等等;就在那里。右边从开始到结束匹配一整组嵌套的圆括号。每个匹配必须捕获并保存两个子字符串;这些对你没用。只需关注主要比赛的结果。

不,深度没有限制。不,里面没有隐藏递归构造。只是简单的老样子,带有一些前瞻性的参考。如果你的口味不支持前向引用(我看着你,JavaScript),那么我很抱歉。我真的是。我希望我能帮助你,但我不是一个奇怪的奇迹工作者。

这太棒了,但我也想与内部团体比赛

好吧,这是交易。我们之所以能够匹配这些外部组,是因为它们不重叠。一旦我们想要的比赛开始重叠,我们就必须稍微调整一下我们的策略。我们仍然可以检查主题是否有正确平衡的括号组。然而,我们需要用一个捕获组来保存它们,而不是直接匹配它们:

(?=()(?=((?:(?=.*?((?!.*?2)(.*)(?!.*3).*))(?=.*?)(?!.*?3)(.*)).)+?.*?(?=2)[^(]*(?=3$))) 

与上一个表达式完全相同,只是我将其大部分封装在前瞻中以避免消耗角色,添加了一个捕获组,并调整了反向引用索引,以便他们与新朋友相处得很好。现在,表达式在下一个括号组之前的位置匹配,感兴趣的子字符串保存为\1。

所以。。。这到底是怎么回事

我很高兴你问我。一般的方法很简单:一次迭代一个字符,同时匹配下一个出现的"("one_answers")",在每种情况下捕获字符串的其余部分,以便在下一次迭代中确定继续搜索的位置。让我把它一块一块地分解:

描述(?=()此前瞻处理查找下一个'('./td>此前瞻处理查找下一个'
注意组件
在做任何艰苦的工作之前确保"("跟随。
(?:用于遍历字符串的组的开始,因此以下lookahead重复匹配
句柄'('(?=
.*?((?!.*?1)匹配到下一个不跟在1后面的"("。下面,您将看到1填充了最后一个匹配的"()"后面的整个字符串部分。因此(?!.*?1)确保我们不会再次匹配相同的"(
(.*)(?!.*2).*)用字符串的其余部分填充1。同时,请检查是否至少存在另一个")"。这是一个PCRE创可贴,可以克服在lookahead中捕获组的错误
)
句柄')'(?=
.*?)(?!.*?2)匹配到不跟在2后面的下一个")"。与前面的"("匹配一样,这会强制匹配以前未匹配过的")">
(.*)用字符串的其余部分填充\ 2。上面提到的bug在这里不适用,所以一个简单的表达式就足够了
)
.使用单个字符,以便组可以继续匹配。使用字符是安全的,因为在新匹配点之前不可能出现下一个"("或")">
)+?尽可能少地匹配,直到找到一个平衡的组。通过以下检查验证
最终验证.*?(?=1)
[^(]*(?=2$)然后进行匹配,直到找到最后一个")"的位置,确保我们一路上不会遇到另一个"("(这意味着一个不平衡的组)

简介

输入修正

首先,你的输入是不正确的,因为有一个额外的括号(如下所示)

(F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
^

进行适当的修改以包括或排除附加括号,可能会出现以下字符串之一:

删除了附加括号

(F(i(r(s)t))) ((S)(e)((c)(o))n)d (((((((Third)))))))
^

添加额外的括号以匹配额外的右括号

((F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
^

Regex功能

其次,这实际上只有在包含递归功能的regex风格中才有可能实现,因为任何其他方法都无法正确匹配开/闭括号(如OP的解决方案中所示,它与上面提到的错误输入中的额外括号相匹配)。

这意味着,对于目前不支持递归(Java、Python、JavaScript等)的正则表达式风格,正则表达式中的递归(或模拟递归的尝试)是不可能的。


输入

考虑到原始输入实际上是无效的,我们将使用以下输入进行测试。

(F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))
(F(i(r(s)t))) ((S)(e)((c)(o))n)d (((((((Third)))))))
((F(i(r(s)t))) ((S)(e)((c)(o))n)d) (((((((Third)))))))

针对这些输入的测试应产生以下结果:

  1. 无效(无匹配)
  2. 有效(匹配)
  3. 有效(匹配)

代码

有多种方法可以匹配嵌套组。下面提供的解决方案都依赖于包含递归功能(例如PCRE)的regex风格。

请参阅此处使用的正则表达式

使用DEFINE块

(?(DEFINE)
(?<value>[^()rn]+)
(?<groupVal>(?&group)|(?&value))
(?<group>(?&value)*((?&groupVal))(?&groupVal)*)
)
^(?&group)$

注意:此正则表达式使用标志gmx

没有DEFINE块

请参阅此处使用的正则表达式

^(?<group>
(?<value>[^()rn]+)*
((?<groupVal>(?&group)|(?&value)))
(?&groupVal)*
)$

注意:此正则表达式使用标志gmx

不带x修饰符(一个衬垫)

请参阅此处使用的正则表达式

^(?<group>(?<value>[^()rn]+)*((?<groupVal>(?&group)|(?&value)))(?&groupVal)*)$

没有命名(组和引用)

请参阅此处使用的正则表达式

^(([^()rn]+)*(((?1)|(?2)))(?3)*)$

注意:这是我能想到的最短的方法。


说明

我将解释最后一个正则表达式,因为它是上面所有其他正则表达式的简化和最小化的示例

  • ^断言行开头的位置
  • (([^()rn]+)*(((?1)|(?2)))(?3)*)将以下内容捕获到捕获组1
    • ([^()rn]+)*将以下内容捕捉到捕获组2任意次数
      • [^()rn]+将集合()rn中不存在的任何字符匹配一次或多次
    • (从字面上匹配左/左括号字符(
    • ((?1)|(?2))将以下任一项捕获到捕获组3
      • (?1)重复第一个子模式(1)
      • (?2)重复第二个子模式(2)
    • )从字面上匹配右/右括号字符)
    • (?3)*多次重复第三个子模式(3)
  • $断言行末尾的位置

最新更新