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