Как написать рекурсивное регулярное выражение, которое соответствует вложенным скобкам?
Я пытаюсь написать регулярное выражение, которое соответствует вложенным скобкам, например:
"(((text(text))))(text()()text)(casual(characters(#$%^^&&#^%#@!&**&#^*!@#^**_)))"
Строка, подобная этой, должна соответствовать, потому что все вложенные скобки закрыты:
"(((text)))(text)(casualChars*#(!&#*(!))"
Не должно или лучше должно соответствовать хотя бы первой части (((текст))) (текст).
На самом деле, мое регулярное выражение:
$regex = '/( ( (\() ([^[]*?) (?R)? (\)) ){0,}) /x';
Но это не работает должным образом, как я ожидаю. Как это исправить? Где я не прав? Спасибо!
2 ответа
Когда я нашел этот ответ, я не смог понять, как изменить шаблон для работы с моими собственными разделителями, которые где {
а также }
, Поэтому мой подход состоял в том, чтобы сделать его более общим.
Вот скрипт для генерации шаблона регулярного выражения с вашими собственными переменными разделителями слева и справа.
$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 );
Этот шаблон работает:
$pattern = '~ \( (?: [^()]+ | (?R) )*+ \) ~x';
Содержание в круглых скобках просто описывает:
"все, что не является круглой скобкой ИЛИ рекурсией (= другие круглые скобки)" 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 (или lookbehind) ничего не соответствует, это всего лишь утверждение (тест). Таким образом, это позволяет проверять одну и ту же подстроку несколько раз.
Если вы отображаете результаты всего шаблона (print_r($matches[0]);
), вы увидите, что все результаты являются пустыми строками. Единственный способ получить подстроки, найденные подшаблоном внутри предисловия, - заключить подшаблон в группу захвата.
Примечание: рекурсивный подшаблон может быть улучшен следующим образом:
\( [^()]*+ (?: (?R) [^()]* )*+ \)
Следующий код использует мой класс 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
);
}
}
}