匹配平衡括号的正则表达式

421

我需要一个正则表达式来选择两个外括号之间的所有文本。

例如:
START_TEXT(text here(possible text)text(possible text(more text)))END_TXT
^ ^

结果:
(text here(possible text)text(possible text(more text)))


6
这个问题很差,因为它没有清楚地表达需要问什么。所有的答案都有不同的理解。@DaveF,请您能否澄清这个问题? - Matt Fenwick
2
在这篇文章中回答了这个问题:https://dev59.com/a2w15IYBdhLWcg3w4_7k - sship21
21个回答

262

我想添加这个答案以供快速参考。随意更新。


.NET 正则表达式 使用平衡组

\((?>\((?<c>)|[^()]+|\)(?<-c>))*(?(c)(?!))\)

c 用作深度计数器。

Regexstorm.com 上的演示


PCRE 使用递归模式

\((?:[^)(]+|(?R))*+\)

在regex101上的演示;或者没有交替项:

\((?:[^)(]*(?R)?)*+\)

在regex101上的演示; 或者展开以提高性能:

\([^)(]*+(?:(?R)[^)(]*)*+\)

在regex101上的演示;该模式已粘贴在(?R)处,表示(?0)

Perl、PHP、Notepad++、Rperl=TRUEPythonPyPI正则表达式模块,使用(?V1)来实现Perl行为
(PyPI正则表达式包的新版本已经默认使用此方法→ DEFAULT_VERSION = VERSION1)


Ruby 使用 子表达式调用

在 Ruby 2.0 中,\g<0> 可以用于调用完整的模式。

\((?>[^)(]+|\g<0>)*\)

在 Rubular 上的演示;Ruby 1.9 只支持捕获组递归

(\((?>[^)(]+|\g<1>)*\))

在Rubular上的演示自Ruby 1.9.3起的原子分组


JavaScript API :: XRegExp.matchRecursive

会返回以下内容:
XRegExp.matchRecursive(str, left, right [, flags]): 匹配平衡的嵌套结构,如括号和HTML标签。 参数“left” 和 “right” 是用于匹配结构的左右标记,可以是正则表达式,也可以是字符串。 可选参数“flags” 可以包含正则表达式标志。

XRegExp.matchRecursive(str, '\\(', '\\)', 'g');

Java:@jaytea 的前向引用有趣的想法


没有递归 最多嵌套3层:
(JS,Java和其他正则表达式风格)

为了防止runaway if unbalanced,只在最内层的[)(]上使用*

\((?:[^)(]|\((?:[^)(]|\((?:[^)(]|\([^)(]*\))*\))*\))*\)

在regex101上的演示; 或者展开以获得更好的性能(推荐)。

\([^)(]*(?:\([^)(]*(?:\([^)(]*(?:\([^)(]*\)[^)(]*)*\)[^)(]*)*\)[^)(]*)*\)

在regex101上的演示;深层嵌套需要根据需要进行添加

// JS-Snippet to generate pattern
function generatePattern()
{
  // Set max depth & pattern type
  let d = document.getElementById("maxDepth").value;
  let t = document.getElementById("patternType").value;
  
  // Pattern variants: 0=default, 1=unrolled (more efficient)
  let p = [['\\((?:[^)(]|',')*\\)'], ['\\([^)(]*(?:','[^)(]*)*\\)']];
  
  // Generate and display the pattern
  console.log(p[t][0].repeat(d) + '\\([^)(]*\\)' + p[t][1].repeat(d));
} generatePattern();
Max depth = <input type="text" id="maxDepth" size="1" value="3"> 
<select id="patternType" onchange="generatePattern()">
  <option value="0">default pattern</option>
  <option value="1" selected>unrolled pattern</option>
</select>
<input type="submit" onclick="generatePattern()" value="generate!">


参考 - 这个正则表达式是什么意思?


1
当您使用占有量词重复一个组时,将该组设置为原子是无用的,因为在每次重复中,该组中的所有回溯位置都被删除。因此,编写(?>[^)(]+|(?R))*+与编写(?:[^)(]+|(?R))*+相同。下一个模式也是如此。关于展开版本,您可以在此处放置一个占有量词:[^)(]*+以防止回溯(如果没有关闭括号)。 - Casimir et Hippolyte
1
@CasimiretHippolyte 谢谢!我调整了PCRE模式,对于Ruby 1.9,您是指整个模式应该像这样https://rubular.com/r/ioj4C7KwN3UsOs吗?请随意更新。我理解您的意思,但不确定是否有很大的改进空间。 - bobble bubble
1
如果有人需要.NET的花括号版本,请使用以下代码:\{(?>\{(?<c>)|[^{}]+|\}(?<-c>))*(?(c)(?!))\} - MgSam
3
对于递归,我建议使用 (\((?:[^)(]+|(?1))*+\))(也可以是?2?3等,取决于分组的编号),而不是 \((?:[^)(]+|(?R))*+\)?R 总是递归回表达式的开头。如果你只使用这个表达式,那么没问题。但是,例如,如果你在 if 语句后面查找逻辑比较 if \((?:[^)(]+|(?R))*+\) 将无法匹配任何内容,因为需要重复 if 才能匹配括号,而不是仅匹配括号。然而,if (\((?:[^)(]+|(?1))*+\)) 只会检查一次 if,然后递归检查第一个组。 - Trashman
1
@bobblebubble 说得好。如果我要抛弃第三组,为什么要捕获它呢?使用正则表达式总有多种方法来达到同样的效果。 - Trashman
显示剩余12条评论

178

正则表达式不是处理递归结构的正确工具,因为你正在处理嵌套结构。

但是有一种简单的算法可以解决这个问题,我在这个答案中更详细地描述了它,针对的是以前的一个问题。其核心思想是编写代码扫描字符串,并保持未被匹配的开括号的计数器。当该计数器返回至零时,就会知道已到达最终的闭括号。


23
.NET的实现拥有“平衡组定义(Balancing Group Definitions)”[http://msdn.microsoft.com/en-us/library/bs2twtah.aspx#balancing_group_definition],可以允许这种情况发生。 - Carl G
49
我不同意正则表达式不适合此任务的观点,理由如下:1)大多数正则表达式实现都有可行的解决方案,即使不完美也没问题;2)通常您试图在上下文中查找平衡的分隔符对,同时要考虑其他适合正则表达式的标准;3)有时您需要将正则表达式传递给只接受正则表达式的某些API,而您别无选择。 - Kenneth Baltrinic
40
正则表达式是完成该任务的正确工具。这个答案不正确。请查看rogal111的答案。 - Andrew
12
完全同意这个答案。尽管正则表达式中有一些递归的实现,它们等价于有限状态机,并且不支持处理嵌套结构,但是上下文无关文法可以做到这一点。请查看Homsky的形式语法层次结构。 - Nick Roz
4
弗兰克是正确的,上下文无关文法无法用正则表达式来描述。这是答案的关键点。 - juliccr
显示剩余4条评论

142

5
这里提供一个例子会很有帮助,我无法让类似于“(1,(2,3))(4,5)”之类的东西工作。 - Andy Hayden
5
@AndyHayden,这是因为“(1,(2,3))(4,5)”有两个用空格分隔的组。使用带有全局标志的正则表达式:/(([^()]|(?R))*)/g。这是在线测试:http://regex101.com/r/lF0fI1/1。 - rogal111
7
在.NET 4.5中,对于这个模式,我会得到以下错误:无法识别的分组结构 - nam
4
太棒了!这是一个很好的正则表达式特性。感谢你是唯一一个真正回答问题的人。此外,那个regex101网站不错。 - Andrew
2
顺便提一下,这个名字有点不准确,因为真正的正则表达式并不是递归的 - EJoshuaS - Stand with Ukraine
显示剩余7条评论

33
[^\(]*(\(.*\))[^\)]*

[^\(]* 匹配字符串开头不是左括号的所有内容,(\(.*\)) 捕获被括号包围的子字符串,并且 [^\)]* 匹配字符串末尾不是右括号的所有内容。请注意,此表达式不尝试匹配括号;一个简单的解析器(请参见dehmann's answer)更适合这种情况。


类内部的括号不需要转义,因为它不是元字符。 - José Leal
16
这个表达式无法匹配类似于“text(text)text(text)text”的文本,它会返回“(text)text(text)”这段文本。正则表达式无法统计括号数量。 - Christian Klauser

27

本答案解释了正则表达式为什么不是处理此任务的正确工具的理论限制。


正则表达式无法实现此功能。

正则表达式基于一种称为有限状态自动机(FSA)的计算模型。正如名称所示,FSA只能记住当前状态,它没有关于以前状态的信息。

FSA

在上图中,S1和S2是两个状态,其中S1是起始和最终步骤。因此,如果我们尝试使用字符串0110,转换如下:

      0     1     1     0
-> S1 -> S2 -> S2 -> S2 ->S1
在上述步骤中,当我们在第二个 S2 时,即解析完 0110 中的 01 后,有限状态自动机对于 01 中前面的 0 没有任何信息,因为它只能记住当前状态和下一个输入符号。
在上述问题中,我们需要知道开括号的数量;这意味着它必须被“存储”在某个地方。但是由于有限状态自动机无法做到这一点,因此无法编写正则表达式。
然而,可以编写算法来完成此任务。算法通常属于 “下推自动机 (PDA)” 的范畴。 PDA 比 FSA 多一个附加栈来存储一些额外的信息。 PDA 可以用来解决上述问题,因为我们可以将开括号“推入”栈中,并在遇到闭括号时将其“弹出”。如果最后堆栈为空,则开括号和闭括号匹配。否则不匹配。

正则表达式中可以使用推入和弹出操作。 https://dev59.com/H2Qn5IYBdhLWcg3wNk_t https://www.regular-expressions.info/balancing.html - Marco
3
这里有几个答案,证明了它是可能的。 - Jiří Herník
6
这个回答从理论角度讲解了正则表达式。现在许多正则表达式引擎不仅依赖于这个理论模型,还会使用一些额外的内存来完成工作。 - Somnath Musib
8
@JiříHerník说的那些不是严格意义上的正则表达式:它们没有被Kleene定义为正则表达式。确实有一些正则表达式引擎实现了一些额外的功能,使它们能够解析更多不仅是正则语言的内容。 - Willem Van Onsem
1
这个应该是一个被接受的答案。不幸的是,许多“开发人员”没有适当的计算机科学/工程教育,也不知道诸如停机问题、泵引理等主题... - fade2black

25
(?<=\().*(?=\))

如果你想选择两个匹配括号之间的文本,那么使用正则表达式将无法实现(*)

这个正则表达式只会返回你字符串中第一个左括号和最后一个右括号之间的文本。


(*) 除非你的正则表达式引擎具有像平衡组递归这样的特性。支持这些功能的引擎数量正在缓慢增长,但它们仍然不是常见的。


"<="和"="符号分别代表什么意思?这个表达式针对哪个正则表达式引擎? - Christian Klauser
2
这是“look-around”,或者更准确地说是“零宽度前/后瞻断言”。现代大多数正则表达式引擎都支持它们。 - Tomalak
根据 OP 的示例,他想在匹配中包含最外层的括号。但是这个正则表达式会将它们丢弃。 - Alan Moore
1
@Alan M:你说得对。但根据问题文本,他想要最外层括号之间的所有内容。你可以选择。他说他已经尝试了几个小时,所以甚至没有考虑“包括最外层括号”这样简单的意图,因为它是如此微不足道:“(.*)”。 - Tomalak
另外,如果您允许递归正则表达式,那么这并非“不可能”。毫无限制地在StackOverflow问题中添加“不可能”一词会导致阅读困难。我建议增加一个关于递归的警告或者讨论语法问题。 - hayesgm
3
@ghayes的回答来自2009年。那是很久之前了;允许某种形式的递归的正则表达式引擎比现在更不普遍(而且它们仍然相当不普遍)。我会在我的回答中提到这一点。 - Tomalak

14

使用.NET正则表达式实现是可以的,但这并不是一件简单的事情,请仔细阅读。

你可以在这里阅读一篇好的文章。你还可能需要学习.NET正则表达式。你可以从这里开始阅读。

因为它们不需要转义,所以使用了尖括号<>

正则表达式看起来像这样:

<
[^<>]*
(
    (
        (?<Open><)
        [^<>]*
    )+
    (
        (?<Close-Open>>)
        [^<>]*
    )+
)*
(?(Open)(?!))
>

12

当处理嵌套模式时,我也曾陷入困境,而正则表达式是解决这类问题的正确工具。

/(\((?>[^()]+|(?1))*\))/

2
作为一个寻求类似主题帮助的用户,我不知道那个正则表达式具体是做什么的,以及如何将其应用到我的问题上。也许这是一个好答案,但考虑到正则表达式本身就很晦涩难懂,我必须查找每一部分才能确定它是否对我有帮助。鉴于有这种“解决方案”的答案太多了,我觉得我不会去尝试。 - Alex
1
https://regex101.com/ 是一个很好的解释工具,可以解释这个正则表达式。 - Mellester
这个使用?1的PCRE解决方案可以在?R方法(由bobble bubble发布)无法使用(*SKIP)(*FAIL)时使用。如果您需要查找括号中没有的内容,请使用此解决方案。我在这里创建了一个示例:https://regex101.com/r/xkVzVP/1 - Ben

6

这是正则表达式的终极版:

\(
(?<arguments> 
(  
  ([^\(\)']*) |  
  (\([^\(\)']*\)) |
  '(.*?)'

)*
)
\)

例子:

input: ( arg1, arg2, arg3, (arg4), '(pip' )

output: arg1, arg2, arg3, (arg4), '(pip'

请注意,'(pip' 被正确地处理为字符串。 (在正则表达式测试工具中尝试:http://sourceforge.net/projects/regulator/

如果没有嵌套或者你只关心最内层的组,我喜欢这种技术。它不依赖于递归。我能够使用它来提取包含括号的参数。我在Regex101上制作了一个可行的示例。 - Ben

5

除了bobble bubble的答案,还有其他支持递归结构的正则表达式风格。

Lua

使用%b()(花括号/方括号使用%b{}/%b[]):

  • for s in string.gmatch("Extract (a(b)c) and ((d)f(g))", "%b()") do print(s) end(请参见演示

Raku(前Perl6)

非重叠多个平衡括号匹配:

my regex paren_any { '(' ~ ')' [ <-[()]>+ || <&paren_any> ]* }
say "Extract (a(b)c) and ((d)f(g))" ~~ m:g/<&paren_any>/;
# => (「(a(b)c)」 「((d)f(g))」)

重叠的多个平衡括号匹配:

say "Extract (a(b)c) and ((d)f(g))" ~~ m:ov:g/<&paren_any>/;
# => (「(a(b)c)」 「(b)」 「((d)f(g))」 「(d)」 「(g)」)

请查看演示

Python re 非正则解决方案

请查看poke的答案以获取如何在平衡的括号之间获取表达式

Java 可定制的非正则解决方案

这是一个允许单个字符文字分隔符的可定制解决方案,适用于Java:

public static List<String> getBalancedSubstrings(String s, Character markStart, 
                                 Character markEnd, Boolean includeMarkers) 

{
        List<String> subTreeList = new ArrayList<String>();
        int level = 0;
        int lastOpenDelimiter = -1;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c == markStart) {
                level++;
                if (level == 1) {
                    lastOpenDelimiter = (includeMarkers ? i : i + 1);
                }
            }
            else if (c == markEnd) {
                if (level == 1) {
                    subTreeList.add(s.substring(lastOpenDelimiter, (includeMarkers ? i + 1 : i)));
                }
                if (level > 0) level--;
            }
        }
        return subTreeList;
    }
}

示例用法:

String s = "some text(text here(possible text)text(possible text(more text)))end text";
List<String> balanced = getBalancedSubstrings(s, '(', ')', true);
System.out.println("Balanced substrings:\n" + balanced);
// => [(text here(possible text)text(possible text(more text)))]

请查看在线Java演示,以证明它可以处理多个匹配。 - Wiktor Stribiżew

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接