如何简化此正则表达式以避免递归?

时间:2017-03-31 16:42:02

标签: php regex

正则表达式:

(?|`(?>[^`\\]|\\.|``)*`|'(?>[^'\\]|\\.|'')*'|"(?>[^"\\]|\\.|"")*"|(\?{1,2})|(:{1,2})([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*))

示例输入:

INSERT INTO xyz WHERE 
a=? 
and b="what?"
and ??="cheese"
and `col?`='OK'
and ::col='another'
and last!=:least

https://regex101.com/r/HnTVXx/6

它应与???:xyz::xyz匹配,但如果它们位于反引号字符串,双引号字符串或单引号内,则不匹配字符串。

当我尝试使用非常大的输入在PHP中运行时,我从PREG_RECURSION_LIMIT_ERROR获得preg_last_error()

如何简化此正则表达式模式,以便它不会进行如此多的递归?

以下是使用Niet's optimized regex在PHP中显示错误的一些测试代码:https://3v4l.org/GdtmP错误代码6为PREG_JIT_STACKLIMIT_ERROR。我见过的另一个是3 = PREG_RECURSION_LIMIT_ERROR

2 个答案:

答案 0 :(得分:3)

使用这种模式可以实现“匹配这个东西,但不是在这种情况下”的一般概念:

(don't match this(*SKIP)(*FAIL)|match this)

在你的情况下,你需要像...这样的东西。

(
  (['"`]) # capture this quote character
  (?:\\.|(?!\1).)*+ # any escaped character, or
                    # any character that isn't the captured one
  \1      # the captured quote again
  (*SKIP)(*FAIL)   # ignore this
  |
  \?\??   # one or two question marks
  |
  ::?\w+  # word characters marked with one or two colons
)x

https://regex101.com/r/HnTVXx/7

答案 1 :(得分:1)

同样的想法是跳过引用的部分((*SKIP)(*F)组合),还有两种技术来减少正则表达式引擎的工作:

  • 第一个字符歧视
  • 展开的模式

这两种技术有一些共同之处:限制交替的成本。

当您的模式以交替开始时,第一个字符区分很有用。一开始交替的问题是应该测试每个分支以便识别模式失败的位置。由于大多数时候,字符串中存在许多失败的位置,因此迅速丢弃它们构成了显着的改进。

例如,像"...|'...|`...|:...这样的东西也可以这样写:

(?=["'`:])(?:"...|'...|`...|:...)

["'`:](?:(?<=")...|(?<=')...|(?<=`)...|(?<=:)...)

这样,每个不以这些字符["'`:]之一开头的位置会立即被第一个标记拒绝,而不会测试每个分支。

展开的模式包括将以下内容重写为:" (?:[^"\\]|\\.)* "

" [^"\\]* (?: \\. [^"\\]* )* "

请注意,此设计消除了更改并大幅减少了步骤数:
basic
unrolled

使用这两种技术,您的模式可以这样写:

~
[`'"?:]
(?:
    (?<=`) [^`\\]*+ (?s:\\.[^`\\]*|``[^`\\]*)*+ ` (*SKIP) (*F)
  |
    (?<=') [^'\\]*+ (?s:\\.[^'\\]*|''[^'\\]*)*+ ' (*SKIP) (*F)
  |
    (?<=") [^"\\]*+ (?s:\\.[^"\\]*|""[^"\\]*)*+ " (*SKIP) (*F)
  |
    (?<=\?) \??
  |
    (?<=:) :? ([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)
)
~x

demo

其他方式:您可以构建一个匹配所有字符串且连续结果的模式,而不是在开头使用替换(改进或不改进)。一般设计是:

\G (all I don't want) (*SKIP) \K (what I am looking for)

\G是一个锚,它匹配前一个结果之后的位置或字符串的开头。使用它启动模式可确保所有匹配都是连续的。在这种情况下(在模式的开头和整个模式的因子中),您也可以用A修饰符替换它。

这给出了:

~
[^`'"?:]*
(?:
    ` [^`\\]*+ (?s:\\.[^`\\]*|``[^`\\]*)*+ ` [^`'"?:]*
  |
    ' [^'\\]*+ (?s:\\.[^'\\]*|''[^'\\]*)*+ ' [^`'"?:]*
  |
    " [^"\\]*+ (?s:\\.[^"\\]*|""[^"\\]*)*+ " [^`'"?:]*
)*
\K  # only the part of the match after this position is returned
(*SKIP) # if the next subpattern fails, the contiguity is broken at this position
(?:
    \?{1,2}
  |
    :{1,2} ([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)
)
~Ax

demo