Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/346.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python 此正则表达式的对抗性输入_Python_Regex_Performance_Security_Non Greedy - Fatal编程技术网

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我还添加了一个正则表达式解决方案,它也绕过了让搜索速度慢得令人痛苦的可能性