Если еще в рекурсивное регулярное выражение не работает, как ожидалось


Я использую регулярное выражение для разбора некоторого BBCode, поэтому регулярное выражение должно работать рекурсивно, чтобы также соответствовать тегам внутри других. Большая часть кода BBCode имеет аргумент, и иногда он цитируется, хотя и не всегда.

Упрощенный эквивалент регулярного выражения, которое я использую (с тегами стиля html для уменьшения необходимого экранирования), таков:

'~<(")?a(?(1)1)> #Match the tag, and require a closing quote if an opening one provided
  ([^<]+ | (?R))* #Match the contents of the tag, including recursively
</a>~x'

Однако, если у меня есть тестовая строка, которая выглядит следующим образом:

<"a">Content<a>Also Content</a></a>

Он соответствует только <a>Also Content</a>, потому что когда он пытается соответствовать из первого тега, то первая группа соответствия, 1, устанавливается в ", и это не перезаписывается, когда регулярное выражение выполняется рекурсивно для соответствия внутреннему тегу, что означает, что, поскольку оно не заключено в кавычки, оно не совпадает и это регулярное выражение не выполняется.

Если вместо этого я последовательно использую или не использую кавычки, это работает нормально, но я не могу быть уверен, что это будет иметь место с содержанием, которое я должен разобрать. Есть ли какой-нибудь способ обойти это?


Полное регулярное выражение, которое я использую, чтобы соответствовать [spoiler]content[/spoiler], [spoiler=option]content[/spoiler] и [spoiler="option"]content[/spoiler], есть

"~[spoilers*+ #Match the opening tag
            (?:=s*+("|')?((?(1)(?!\1).|[^]]){0,100})(?(1)\1))?+s*] #If an option exists, match that
          (?: *(?:n|<br />))?+ #Get rid of an extra new line before the start of the content if necessary
          ((?:[^[n]++ #Capture all characters until the closing tag
            |n(?![spoiler]) Capture new line separately so backtracking doesn't run away due to above
            |[(?!/?spoiler(?:s*=[^]*])?) #Also match all tags that aren't spoilers
            |(?R))*+) #Allow the pattern to recurse - we also want to match spoilers inside spoilers,
                     # without messing up nesting
          n? #Get rid of an extra new line before the closing tag if necessary
          [/spoiler] #match the closing tag
         ~xi"

Есть еще пара других ошибок с ним, хотя.

2 6

2 ответа:

Самое простое решение-использовать вместо этого альтернативы:

<(?:a|"a")>
  ([^<]++ | (?R))*
</a>
Но если вы действительно не хотите повторять эту часть a, Вы можете сделать следующее:
<("?)a\1>
  ([^<]++ | (?R))*
</a>

Демо

Я только что поместил условное ? внутрь группы. На этот раз группа захвата всегда совпадает, но совпадение может быть пустым, и условное условие больше не нужно.

Примечание: я применил притяжательный Квантор к [^<], чтобы избежать катастрофическое отступление .


В вашем случае я считаю, что лучше сопоставить общий тег, чем конкретный. Сопоставьте все теги, а затем решите в своем коде, что делать с этим совпадением.

Вот полное регулярное выражение:

\[
  (?<tag>\w+) \s*
  (?:=\s*
    (?:
      (?<quote>["']) (?<arg>.{0,100}?) \k<quote>
      | (?<arg>[^\]]+)
    )
  )?
\]

(?<content>
  (?:[^[]++ | (?R) )*+
)

\[/\k<tag>\]

Демо

Обратите внимание, что я добавил опцию J (PCRE_DUPNAMES), чтобы иметь возможность использовать (?<arg>...) дважды.

(?(1)...) проверяет только, определена ли группа 1, поэтому условие истинно, как только группа определена в первый раз. Вот почему вы получаете этот результат (он не связан с уровнем рекурсии или чем-то еще).

Поэтому, когда <a> достигается в рекурсии, механизм регулярных выражений пытается соответствовать <a"> и терпит неудачу.

Если вы хотите использовать условный оператор, вы можете написать <("?)a(?(1)\1)> вместо этого. Таким образом, Группа 1 переопределяется каждый раз.

Очевидно, вы можете написать свой паттерн более эффективным способом, как это:

~<(?:a|"a")>[^<]*+(?:(?R)[^<]*)*+</a>~

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

$pattern = <<<'EOD'
~
\[ (?<tag>\w+) \s*
(?: 
  = \s* 
  (?| " (?<option>[^"]*) " | ' ([^']*) ' | ([^]\s]*) ) # branch reset feature
)?
\s* ]
(?<content> [^[]*+ (?: (?R) [^[]*)*+ )
\[/\g{tag}]
~xi
EOD;

Если вы хотите наложить определенный тег на уровне земли, вы можете добавить (?(R)|(?=spoiler\b)) перед именем тега.