if-else在递归正则表达式中没有按预期工作

时间:2015-06-27 12:49:29

标签: php regex pcre

我正在使用正则表达式来解析一些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]

"~\[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"

但是还有其他一些错误。

2 个答案:

答案 0 :(得分:3)

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

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

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

<("?)a\1>
  ([^<]++ | (?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)

(?(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))