此正则表达式的对抗性输入

问题描述

有人问我一个面试问题:写一个函数 match(s,t) 来判断一个字符串 s 是否是另一个字符串 t广义子字符串。更具体地说,当且仅当删除 match 中的某些字符可以使其等于 t 时,s 应该返回 True。例如,match("abc","abbbc") 为 True,因为我们可以删除 b 中的两个额外的 t

当然面试官期待某种递归解决方案,但我感觉很冒险并写了

def match(s,t):
    return re.search('.*?'.join(s),t) is not None

这似乎满足了所有的测试用例,但后来我开始怀疑是否存在任何对抗性输入可以占用此操作(并可能使我们的服务容易受到 DoS 攻击)。有关扩展讨论,请参阅 this great article,但作为一个快速示例,请尝试

re.match("a?"*29+"a"*29,"a"*29)

并且需要几秒钟才能找到匹配项。原因是 re 实现了回溯正则表达式引擎:

考虑正则表达式 a?nan。它匹配字符串 an 当 a?选择不匹配任何字母,留下整个字符串由 an 匹配。回溯正则表达式实现实现零或一?先尝试一个,然后再尝试零。有 n 个这样的选择,总共有 2n 种可能性。只有最后一种可能性——为所有 ? 选择零——会导致匹配。因此回溯方法需要 O(2n) 时间,所以它不会超过 n=25。

回到面试问题,从技术上讲,'.*?'.join(s) 至少为我们提供了 len(s) - 1 个选择,这意味着可能存在一些类似于 "a?"*29+"a"*29"a"*29 的对抗性输入以上,但经过反复试验,我没能找到它们。

问题:什么 st 可以让 match 慢得离谱?

解决方法

懒惰的量词通常对性能非常好,但 AFAIK 它们并不能阻止病态的强调行为。

当正则表达式的开头与文本的开头匹配时尤其如此,但匹配很早,并且会在文本结尾失败,需要大量回溯来“修复”错误的早期延迟匹配正则表达式的开头。

在您的情况下,这是一个需要指数级步骤的病理输入示例:

# This should take at least few minutes to compute
n = 12
match('ab'*(n+1),'abbbbb'*n+'a')

以下是基于 match 的值所需的步骤数和 Python n 时间:

 n |  steps |    time
 1 |     44 |   2.4 µs
 2 |    374 |   6.4 µs
 3 |   2621 |  22.1 µs
 4 |  18353 | 131.3 µs
 5 | 126211 | 925.8 µs
 6 |    -   |   6.2 ms
 7 |    -   |  42.2 ms
 8 |    -   | 288.7 ms
 9 |    -   |  1.97 s

您可以理解和分析 this great website 上产生的正则表达式匹配行为(在这种情况下更具体地说是回溯)。

,

您的解决方案由于回溯而受到影响,这就是为什么接受的答案能够提供会大大减慢速度的输入。

这是一个正则表达式解决方案,它与我之前的答案非常相似,而且似乎还避免了困扰您当前解决方案的问题。

def match(s,t):
    return re.search("".join(f'[^{re.escape(c)}]*{re.escape(c)}' for c in s),t) is not None

上一个回答

这并不能直接回答您的问题,但是

当然面试官期待某种递归解决方案......

我不这么认为。像这样的东西也可以工作

def match(s,t):
    i = -1
    return all ((i := t.find(c,i + 1)) != -1 for c in s)

我只是想提供一种不需要递归或正则表达式的替代解决方案,因此会导致您提到的漏洞。