如何编写一个递归正则表达式来匹配嵌套的括号?

10

我正在尝试编写一个正则表达式,可以匹配嵌套的括号,例如:

"(((text(text))))(text()()text)(casual(characters(#$%^^&&#^%#@!&**&#^*!@#^**_)))"

应该匹配像这样的字符串,因为所有嵌套的括号都是关闭的,而不是:

"(((text)))(text)(casualChars*#(!&#*(!))"

不应该,或者更好的选择是至少匹配第一个"(((text)))(text)"部分。

实际上,我的正则表达式是:

 $regex = '/( (  (\() ([^[]*?)  (?R)?  (\))  ){0,}) /x';

但它的功能并不像我期望的那样正常工作。该怎么修复呢?我哪里出错了?谢谢!

2
我写了一个SQL解析器,需要递归执行此操作。使用正则表达式进行递归函数比仅使用正则表达式更容易实现递归。 - EdgeCase
你的方向不对,纯正则表达式解决方案可能会过于复杂和难以维护。最好使用递归解析字符串。 - GordonM
不要... 好吧,理论上它是可以做到的,但当你设法做到时,它可能看起来像胡言乱语。哦,看,我们在正则表达式中发现了一个错误!呃...怎么修复?哦,我们也需要添加对括号的支持!呃...怎么添加?我告诉你,最好使用更易读的解析器。你问这个问题的事实表明,你可能无法维护它。 - Theraot
谢谢您的建议,但我仍然想这样做,您能帮我吗?为什么我的正则表达式没有按照我的预期工作? - tonix
@user3019105 你想要对匹配项做什么?(你只是想验证,还是想替换括号内的内容,或者只是在每个匹配项上运行回调函数)另外,你只想要最深层的括号还是所有括号都要? - Theraot
我正在尝试根据RFC 5321/5322规范修剪电子邮件地址的注释部分。我想在$matches[1]中捕获所有嵌套括号对,但我希望正则表达式仅匹配到最后一个正确关闭的")"括号,因此如果有其他不匹配的括号在前面,正则表达式将停止匹配。 - tonix
3个回答

13

这个模式可行:

$pattern = '~ \( (?: [^()]+ | (?R) )*+ \) ~x';

括号内的内容仅用于描述:

"所有不是括号或递归(=其他括号)" 0次或多次

如果您想捕获括号内的所有子字符串,必须将此模式放在前瞻中以获取所有重叠的结果:

$pattern = '~(?= ( \( (?: [^()]+ | (?1) )*+ \) ) )~x';
preg_match_all($pattern, $subject, $matches);
print_r($matches[1]);

请注意,我已添加了一个捕获组,并将(?R)替换为(?1):
(?R) -> refers to the whole pattern (You can write (?0) too)
(?1) -> refers to the first capturing group

这个 lookahead 技巧是什么?
在 lookahead(或 lookbehind)内部的子模式不匹配任何内容,它只是一个断言(测试)。因此,它允许多次检查相同的子字符串。
如果您显示整个模式结果(print_r($matches[0]);),您将看到所有结果都是空字符串。获取 lookahead 内部子模式找到的子字符串的唯一方法是将子模式括在捕获组中。
注意:递归子模式可以像这样改进:
\( [^()]*+ (?: (?R) [^()]* )*+ \)

1
我已经尝试了,但它不起作用,而且我没有捕获到子模式...还有其他的方法吗? - tonix
谢谢解释。需要尝试操作一下才能理解。 - hek2mgl
抱歉我的无知,“(?1)” 和 “\1” 是一样的吗?它是一个反向引用吗? 还有一个问题:在 [^()] 类之后,双重“++”是什么意思? - tonix
@user3019105:不,这是不同的。\1 是指捕获组 1 匹配的内容。(?1) 是指捕获组 1 内部的子模式。你只重复了子模式。 - Casimir et Hippolyte
谢谢!所以在这种情况下,它是$matches [1]变量中的第一个子模式,我是对的吗? 至于“++”和“* +”,它们是什么?我第一次看到它们……有参考资料或者你能解释一下吗?谢谢! - tonix
显示剩余5条评论

4

当我发现这个答案时,我无法弄清如何修改模式以适用于我的自定义分隔符,即 {}。所以我的方法是使其更加通用。

以下是一个脚本,可生成带有你自己的变量左右分隔符的正则表达式模式。

$delimiter_wrap  = '~';
$delimiter_left  = '{';/* put YOUR left delimiter here.  */
$delimiter_right = '}';/* put YOUR right delimiter here. */

$delimiter_left  = preg_quote( $delimiter_left,  $delimiter_wrap );
$delimiter_right = preg_quote( $delimiter_right, $delimiter_wrap );
$pattern         = $delimiter_wrap . $delimiter_left
                 . '((?:[^' . $delimiter_left . $delimiter_right . ']++|(?R))*)'
                 . $delimiter_right . $delimiter_wrap;

/* Now you can use the generated pattern. */
preg_match_all( $pattern, $subject, $matches );

1
很好,只要始终有一个 $delimiter_right 来关闭打开的 $delimiter_left,你就匹配了整个字符串。 - tonix

0
以下代码使用了我的 解析器类(它在 CC-BY 3.0 下发布),它适用于 UTF-8(感谢我的 UTF8 类)。
它的工作方式是通过使用递归函数来迭代字符串。每次找到 ( 时,它将调用自身。当它到达字符串结尾而没有找到相应的 ) 时,它还会检测不匹配的对。
此外,这个代码接受一个 $callback 参数,你可以用它来处理它找到的每一段内容。回调函数接收两个参数:1)字符串,2)级别(0 = 最深层)。无论回调函数返回什么都将替换字符串的内容(这些更改在更高级别的回调中可见)。
注意:该代码不包括类型检查。
非递归部分:
function ParseParenthesis(/*string*/ $string, /*function*/ $callback)
{
    //Create a new parser object
    $parser = new Parser($string);
    //Call the recursive part
    $result = ParseParenthesisFragment($parser, $callback);
    if ($result['close'])
    {
        return $result['contents'];
    }
    else
    {
        //UNEXPECTED END OF STRING
        // throw new Exception('UNEXPECTED END OF STRING');
        return false;
    }
}

递归部分:

function ParseParenthesisFragment(/*parser*/ $parser, /*function*/ $callback)
{
    $contents = '';
    $level = 0;
    while(true)
    {
        $parenthesis = array('(', ')');
        // Jump to the first/next "(" or ")"
        $new = $parser->ConsumeUntil($parenthesis);
        $parser->Flush(); //<- Flush is just an optimization
        // Append what we got so far
        $contents .= $new;
        // Read the "(" or ")"
        $element = $parser->Consume($parenthesis);
        if ($element === '(') //If we found "("
        {
            //OPEN
            $result = ParseParenthesisFragment($parser, $callback);
            if ($result['close'])
            {
                // It was closed, all ok
                // Update the level of this iteration
                $newLevel = $result['level'] + 1;
                if ($newLevel > $level)
                {
                    $level = $newLevel;
                }
                // Call the callback
                $new = call_user_func
                (
                    $callback,
                    $result['contents'],
                    $level
                );
                // Append what we got
                $contents .= $new;
            }
            else
            {
                //UNEXPECTED END OF STRING
                // Don't call the callback for missmatched parenthesis
                // just append and return
                return array
                (
                    'close' => false,
                    'contents' => $contents.$result['contents']
                );
            }
        }
        else if ($element == ')') //If we found a ")"
        {
            //CLOSE
            return array
            (
                'close' => true,
                'contents' => $contents,
                'level' => $level
            );
        }
        else if ($result['status'] === null)
        {
            //END OF STRING
            return array
            (
                'close' => false,
                'contents' => $contents
            );
        }
    }
}

发布功能像这样,已经落后了好几个光年。 - Martin Zvarík
@MartinZvarík 感谢您引起了我对此答案的关注,我已经修复了链接。这意味着它比正则表达式更容易维护。 - Theraot
1
我也喜欢你提到它是在创意共用许可证下发布的 :DD ... 兄弟... 把整个东西都删掉,我们就忘了它曾经在这里发布过... 五年后你一定会更明智。 - Martin Zvarík

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