递归正则表达式中的 If-else 未按预期工作

If-else in recursive regex not working as expected

我正在使用正则表达式来解析一些 BBCode,因此正则表达式必须递归地工作以匹配其他标签内的标签。大多数 BBCode 都有一个参数,有时它会被引用,但并非总是如此。

我正在使用的正则表达式的简化等效项(带有 html 样式标签以减少所需的转义)是这样的:

'~<(\")?a(?(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> 因为当它尝试从第一个标签匹配时,第一个匹配组 </code> 被设置为 <code>",这是 not 当正则表达式 运行 递归地匹配内部标签时被覆盖,这意味着因为它没有被引用,所以它不匹配并且正则表达式失败。

如果我始终使用或不使用引号,它工作正常,但我不能确定我必须解析的内容是否会出现这种情况。有什么办法可以解决这个问题吗?


我使用的用于匹配 [spoiler]content[/spoiler][spoiler=option]content[/spoiler][spoiler="option"]content[/spoiler] 的完整正则表达式是

"~\[spoiler\s*+ #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"

它还有一些其他错误。

最简单的解决方案是使用替代方案:

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

但如果您真的不想重复 a 部分,您可以执行以下操作:

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

Demo

我刚刚将条件 ? 放入组中。这一次,捕获组总是匹配,但匹配可以为空,条件不再是必需的。

旁注:我已将所有格量词应用于 [^<] 以避免 catastrophic backtracking.


在你的情况下,我认为匹配通用标签比匹配特定标签更好。匹配所有标签,然后在您的代码中决定如何处理匹配项。

这是一个完整的正则表达式:

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

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

\[/\k<tag>\]

Demo

请注意,我添加了 J 选项 (PCRE_DUPNAMES) 以便能够使用 (?<arg>...) 两次。

(?(1)...) 只检查组 1 是否已经被定义,所以一旦组被第一次定义,条件就为真。这就是为什么你得到这个结果(它与递归级别或其他无关)。

所以当在递归中达到 <a> 时,正则表达式引擎尝试匹配 <a"> 但失败了。

如果要使用条件语句,可以写成<("?)a(?(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))