【问题标题】:RegEx debugging正则表达式调试
【发布时间】:2016-10-21 02:25:50
【问题描述】:

我正在通过我可以访问的两个单独的调试工具在字符串 AAAC(来自 rexegg.com 的示例)上调试正则表达式 ^(A+)*B

  1. regex101.com
  2. RegexBuddy v4

下面是结果(左边是regex101):

我的问题主要不是关于对我来说也很重要的步数,而是如何进行回溯。为什么我们会看到差异? (regex101 使用 PCRE 库,我设置 RegexBuddy 库相同)

全面的逐步解释真的对我有利。

【问题讨论】:

  • ++ 用于在您的问题中使用<sup> 标签。:-) 我喜欢有人关心设计。
  • @Shafizadeh 尽管如此,这在语义上既不正确,也不是支持该问题的充分理由,因为投票应该基于内容。
  • @Xufox 你是对的..我只做过一次,我不会再这样做了。
  • (A+)* 失败时重复A+ (1..N) 的排列和(...)* (0..N) 的排列。它变成指数级的。可以用(..)* 包装块,只要它的内容在本地受到限制。更好的方法是使用极端的失败条件对嫌疑人进行基准测试。问题会很快出现。我会发布一些基准。
  • AFAIK,RegexBuddy 使用 JGSoft 正则表达式引擎,该引擎旨在模仿其他正则表达式引擎进行调试,因此它不像 regex101 那样真的使用 PCRE。至于 regex101,调试器是使用 PCRE_AUTO_CALLOUT feature 实现的——这让 regex101 检查模式中每个位置的当前匹配状态,并从那里推断何时发生回溯。因此,由于引擎的设计不尽相同,因此您在相同的结果中得到不同的显示也就不足为奇了。

标签: regex pcre backtracking


【解决方案1】:

我不会依赖步骤数或任何调试作为测试
要么回溯,要么失败。

一般来说,远离简单的构造,例如(A+)*,这不仅
回溯内部A+,但也回溯外部(..)*
outter 的每一遍都会产生一个新的(新的)inner 系列的回溯。

获得像RegexFormat这样好的基准测试软件
并测试一个系列的指数时间结果。
随着内容的增加,您正在寻找线性结果
数量。

下面是(A+)?(A+)* 的示例。我们将目标设置为已知故障
并稳步增加长度以扩展该故障的处理。

如果您查看正则表达式 2 (A+)*,您会注意到只是指数增长
三个目标增加。
最后,我们爆出目标,使引擎内部资源过载。


编辑:经过一些分析,我对回溯步骤发表了一个微不足道的结论。
通过分析下面的基准时间,回溯似乎是一个 2^N 过程。
其中 N 是失败时回溯的字符文字数。

鉴于 Revo 的简单案例,隔离回溯会更容易一些。
因为看起来总时间的 %98 只涉及回溯。

鉴于该假设,人们可以从 2 个点获取时间结果,并生成一个方程。

我使用的方程式的形式f(x) = a * r^x
给定坐标('A 的# 与所用时间),使用点
(7, 1.13) , (9, 4.43) 生成了这个f(x) = 0.009475 * 1.9799 ^ x
真的是这个# sec's = 0.009475 * 1.9799 ^ # letters
我将下面基准中的所有字母“A”的数量都放入了这个公式中。

#LTR's   Bench Time
 7         1.13
 9         4.43
13        70.51

它产生了准确的基准时间 (+/- .1%)。

然后我看到 1.9799 非常接近 2.0,所以我将“a”因子调整为 0.009 并再次运行它,得到:

f(7 letters) = 2 ^ 7 * .009 =  1.152 sec’s
f(9 letters) = 2 ^ 9 * .009 =  4.608 sec’s
f(13 letters) = 2 ^ 13 * .009 =  73.728 sec’s
f(27 letters) = 2 ^ 27 * .009 =  1207959.552 sec’s = 335 hours  

现在似乎很清楚,回溯步骤与次数相关
2 ^ N 关系中回溯的字母。

我还认为,一些引擎可以做这个简单的 2^N 数学运算是公平的赌注
仅在像这样的简单场景中。这些似乎是时代
引擎立即返回并说太复杂了!
这里的翻译是正则表达式足够简单,引擎可以
检测它。其他时候,引擎永远不会回来,
它挂了,可能会在一两年(或十年……)内回来。

因此,结论不是引擎是否会回溯,它会,而是如何
你的目标字符串会影响回溯吗?

所以,写正则表达式需要警惕,一定要提防
嵌套的开放式量词。

我想说,最好的办法总是打一个正则表达式 真正困难 让它失败。
而且我说的是可疑地方的过多重复文字。
结束编辑


Benchmark App

目标:AAAAAAAC

基准测试

Regex1:   ^(A+)?B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    0.07 s,   72.04 ms,   72040 µs


Regex2:   ^(A+)*B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    1.13 s,   1126.44 ms,   1126444 µs

目标:AAAAAAAAAC

基准测试

Regex1:   ^(A+)?B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    0.08 s,   82.43 ms,   82426 µs


Regex2:   ^(A+)*B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    4.43 s,   4433.19 ms,   4433188 µs

目标:AAAAAAAAAAAAAC

基准测试

Regex1:   ^(A+)?B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    0.10 s,   104.02 ms,   104023 µs


Regex2:   ^(A+)*B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    70.51 s,   70509.32 ms,   70509321 µs

目标:AAAAAAAAAAAAAAAAAAAAAAAAAAAAC

基准测试

Regex1:   ^(A+)?B
Options:  < none >
Completed iterations:   50  /  50     ( x 1000 )
Matches found per iteration:   0
Elapsed Time:    0.18 s,   184.05 ms,   184047 µs


Regex2:   ^(A+)*B
Options:  < none >
Error:  Regex Runtime Error !!   
Completed iterations:   0  /  50     ( x 1000 )
Matches found per iteration:   -1
Elapsed Time:    0.01 s,   7.38 ms,   7384 µs


Error item 2 :  Target Operation .. 

The complexity of matching the regular expression exceeded predefined
bounds. Try refactoring the regular expression to make each choice made by
the state machine unambiguous. This exception is thrown to prevent
"eternal" matches that take an indefinite period time to locate. 

【讨论】:

  • 这是一个应该围绕这个主题提供的完美答案。但是我在这里有一些问题: 1- 你没有谈论为什么我们看到两个调试器所采取的步骤不同。为什么真的? 2- 在第三个基准测试中,第二个正则表达式经过的时间是 70.51 秒(秒)。这样对吗?! 3- 如果您不介意,请解释每个测试的第三行。
  • @revo - 1. 我不知道他们的调试器是怎么做的。这可能是一个微不足道的差异。但是,我添加了一个带有 2^N 回溯理论的编辑。 2. 是的,70 秒是正确的(也请参见添加的编辑)。 3. 第三行是 Iterations completed / Iterations requested (均为 1000)。输入框采用从 1 到 9999 请求的迭代量的数字。有一个复选框可以选择是否使用 1000 的乘数。所以基本上它可以进行 1 - 9,999,000 次迭代。获取应用程序,亲自尝试。 Benchmark
【解决方案2】:

TL;DR

简而言之,“回溯”是当正则表达式引擎返回到“灵活”匹配时,尝试使用不同的路径来获得成功的匹配。

交替回溯

例如,在以下模式和输入中:

foo(bar|baz)
foobaz

正则表达式引擎将匹配“foo”,然后尝试两个选项中的第一个,匹配“b”,然后是“a”,但在“r”处失败。但是,它不会让整个匹配失败,而是“倒带”并从第二个选择开始,匹配“b”,然后是“a”,然后是“z”……成功!

使用量词回溯

这也适用于量词。量词是任何鼓励引擎匹配重复模式的东西,包括?*+{n,m}(取决于引擎)。

贪心量词(默认)会在继续模式的其余部分之前匹配尽可能多的重复。例如,给定下面的模式和输入:

\w+bar
foobar

模式\w+ 将从匹配整个字符串开始:“foobar”。但是,当它移动到b 时,正则表达式引擎已经到达输入的末尾并且匹配失败。引擎不会简单地放弃,而是要求最后一个贪婪量词放弃它的一个重复,现在匹配“fooba”。匹配仍然失败,因此引擎要求\w+ 放弃“a”(失败),然后放弃“b”。放弃“b”后,引擎现在可以匹配bar,匹配成功。

树和回溯

正则表达式的另一种思维方式是“树”,回溯是返回一个节点并尝试另一条路径。给定模式foo(bar|baz) 和输入“foobaz”,引擎将尝试如下操作:

foo(bar|baz)
|\
| \
|  : f (match)
|  : o (match)
|  : o (match)
|  | (bar|baz)
|  |\
|  | \
|  |  : b (match)
|  |  : a (match)
|  |  : r (FAIL: go back up a level)
|  |  ^
|  |\
|  | \
|  |  : b (match)
|  |  : a (match)
|  |  : z (match)
|  | /
|  |/
| /
|/
SUCCESS

计算“回溯”

至于为什么您会看到回溯“数量”的差异......这可能与内部优化和日志记录级别有很大关系。例如,RegexBuddy 似乎不会将匹配记录到^ 之前的空字符串,而 regex101 会。不过,最后,只要您最终得到相同的结果,您返回的确切顺序(您爬回树上的顺序)并不重要。

邪恶的正则表达式

您已经知道这一点,但是为了其他人的利益,您的正则表达式是为了演示“catastrophic backtracking”(又名“邪恶的正则表达式”),其中回溯尝试的次数随着输入增加。这些正则表达式可以被利用来执行DoS attacks,因此您必须小心不要将它们引入您的模式(如I found out)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多