Python 此正则表达式的对抗性输入
有人问我一个面试问题:写一个函数Python 此正则表达式的对抗性输入,python,regex,performance,security,non-greedy,Python,Regex,Performance,Security,Non Greedy,有人问我一个面试问题:写一个函数match(s,t)来决定一个字符串s是否是另一个字符串t的广义子字符串。更具体地说,match应该返回True,当且仅当删除t中的一些字符可以使其与s相等时。例如,match(“abc”、“abbbc”)为True,因为我们可以删除t中的两个额外的bs 当然,面试官期待的是某种递归的解决方案,但我觉得自己很冒险,所以写了一篇文章 def match(s, t): return re.search('.*?'.join(s), t) is not None
match(s,t)
来决定一个字符串s
是否是另一个字符串t
的广义子字符串。更具体地说,match
应该返回True,当且仅当删除t
中的一些字符可以使其与s
相等时。例如,match(“abc”、“abbbc”)
为True,因为我们可以删除t
中的两个额外的b
s
当然,面试官期待的是某种递归的解决方案,但我觉得自己很冒险,所以写了一篇文章
def match(s, t):
return re.search('.*?'.join(s), t) is not None
这似乎满足了所有测试用例的要求,但我开始怀疑是否存在任何敌对输入可以占用此操作(并可能使我们的服务容易受到DoS攻击)。有关详细讨论,请参见,但作为一个快速示例,请尝试
re.match("a?"*29+"a"*29, "a"*29)
要找到匹配项需要几秒钟的时间。原因是re
实现了回溯正则表达式引擎:
考虑正则表达式a?nan。当a?选择不匹配任何字母,使整个字符串由an匹配。回溯正则表达式实现实现零还是一?先尝试1,然后尝试0。有n个这样的选择,总共有2n种可能性。只有最后一种可能性,所有的选择都是零,才会导致匹配。因此,回溯方法需要O(2n)时间,因此它的伸缩性不会超过n=25
回到面试问题,从技术上讲,“.*”。join(s)
至少给我们提供了len(s)-1
选择,这意味着可能会有一些类似于上面的“a?”*29+“a”*29
和“a”*29
的敌对输入,但经过一些尝试和错误后,我没有找到它们
问:什么
s
和t
可以使匹配的速度慢得可笑?惰性量词通常对性能很好,但它们并不能阻止这种行为
当regexp的开头与文本的开头匹配时,尤其如此,但是匹配很早,并且在文本末尾会失败,需要大量回溯来“修复”regexp开头的错误的早期延迟匹配
在您的案例中,以下是需要指数级步骤的病理输入示例:
#这至少需要几分钟的计算时间
n=12
匹配('ab'*(n+1),'abbbb'*n+a'))
以下是基于n
值的步骤数和Python匹配所需的时间:
n |步数|时间
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毫秒
7 |-42.2毫秒
8 |-288.7毫秒
9 |-1.97秒
您可以理解和分析上的结果regexp匹配行为(在本例中更具体地说是回溯)。您的解决方案由于回溯而受到影响,这就是为什么接受的答案能够提供输入,从而大大降低其速度
这里有一个regex解决方案,它与我之前的答案非常相似,而且它似乎也避免了困扰当前解决方案的问题
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)
我只是想提供一个不需要递归或正则表达式的替代解决方案,从而导致您提到的漏洞。我想知道*?
首先尝试什么,是否使用下一个字符?我可以从您的示例中看出,它在回溯之前尝试尽可能多的匹配,但是根据,非贪婪限定符意味着“将匹配尽可能少的字符”。如果*?
尝试在回溯之前匹配尽可能多的字符,那么s=t='a'*n
也应该是一个病理输入(因为它最终会在t
中耗尽字符,因为它会在早期消耗过多),但一个快速的实验表明它不是:/*?
是懒惰的,因此尝试尽可能多地匹配第一个字符,但这种策略并不总是好的(比如这里)这里对每个*?
都考虑“尽可能少的字符将被匹配”:匹配字符串的长度被*?
最小化。但是,正则表达式必须找到有效匹配,并且由于第一个*?
的第一个最差匹配选择(由于惰性量词),需要很多回溯。很抱歉,我没有这样做:如果*?
尝试尽可能多地匹配第一个字符,那么为什么会重新匹配('a(.*?,'aaa')。组(1)
给出一个空字符串?如果它在回溯之前尝试贪婪地匹配,那么我应该将'aa'
作为第一个(也是唯一的)字符捕获组。s=t='a'*n
不是问题。事实上,第一个*?
将以不匹配任何内容开始(因为惰性量词),然后a
被匹配,然后第二个*?
执行相同的操作,最后,所有字符都将在没有回溯的情况下被匹配。*?a
的行为有点像[^a]*+a
,直到regexp的其余部分失败。谢谢。如果我不是那么冒险,我会编写一个尾部递归解决方案(通过在s
和t
的第一个字符上分派,如果有),这也应该像你那样躲避子弹:P@nalzok我还添加了一个正则表达式解决方案,它也绕过了让搜索速度慢得令人痛苦的可能性