Как написать рекурсивное регулярное выражение, которое соответствует вложенным круглым скобкам?
Я пытаюсь написать регулярное выражение, которое соответствует вложенным круглым скобкам, например:
"(((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
);
}
}
}