Как написать рекурсивное регулярное выражение, которое соответствует вложенным круглым скобкам?

Я пытаюсь написать регулярное выражение, которое соответствует вложенным круглым скобкам, например:

"(((text(text))))(text()()text)(casual(characters(#$%^^&&#^%#@!&**&#^*[email protected]#^**_)))"

Строка, подобная этой, должна быть сопоставлена, потому что все вложенные круглые скобки закрыты, вместо этого:

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

Не следует или лучше соответствовать как минимум первой части "((текст))) (текста).

На самом деле, мое регулярное выражение:

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

Но это не работает должным образом, как я ожидаю. Как это исправить? Где я ошибаюсь? Спасибо!

Ответ 1

Когда я нашел этот ответ, я не смог понять, как изменить шаблон для работы с моими собственными разделителями, где { и }. Поэтому мой подход состоял в том, чтобы сделать его более общим.

Вот script, чтобы сгенерировать шаблон регулярного выражения с вашими собственными переменными влево и вправо.

$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 );

Ответ 2

Эта модель работает:

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

Содержимое внутри скобок просто описывается:

"все, что не является скобкой ИЛИ рекурсией (= другая скобка)" x 0 или более раз

Если вы хотите поймать все подстроки внутри скобок, вы должны поместить этот шаблон в lookahead, чтобы получить все перекрывающиеся результаты:

$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 (или lookbehind) ничего не соответствует, это только утверждение (тест). Таким образом, он позволяет проверять одну и ту же подстроку несколько раз.

Если вы показываете результаты всего шаблона (print_r($matches[0]);), вы увидите, что все результаты - это пустые строки. Единственный способ получить подстроки, найденные подшаблоном внутри lookahead, заключить подшаблон в группу захвата.

Примечание: рекурсивный подшаблон может быть улучшен следующим образом:

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

Ответ 3

В следующем коде используется класс Parser из Paladio (он под CC-BY 3.0), он работает на UTF-8.

То, как он работает, - это использовать рекурсивную функцию для итерации по строке. Он будет называть себя каждый раз, когда находит (. Он также обнаруживает несогласованные пары, когда он достигнет конца строки, не найдя соответствующий ).

Кроме того, этот код принимает параметр $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
            );
        }
    }
}