递归正则表达式中的 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>
我刚刚将条件 ?
放入组中。这一次,捕获组总是匹配,但匹配可以为空,条件不再是必需的。
旁注:我已将所有格量词应用于 [^<]
以避免 catastrophic backtracking.
在你的情况下,我认为匹配通用标签比匹配特定标签更好。匹配所有标签,然后在您的代码中决定如何处理匹配项。
这是一个完整的正则表达式:
\[
(?<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每次都重新定义。
显然,您可以像这样以更有效的方式编写模式:
~<(?: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))
。