我想让我的用户对某些功能使用正则表达式。我很好奇将用户输入传递给 re.compile() 的含义是什么。我假设用户没有办法给我一个可以让他们执行任意代码的字符串。我想到的危险是:
- 用户可以传递引发异常的输入。
- 用户可能会传递导致正则表达式引擎花费很长时间或使用大量内存的输入。
1.的解决方案很简单:捕获异常。我不确定 2 是否有一个好的解决方案。也许只是限制正则表达式的长度就可以了。
还有什么我需要担心的吗?
我想让我的用户对某些功能使用正则表达式。我很好奇将用户输入传递给 re.compile() 的含义是什么。我假设用户没有办法给我一个可以让他们执行任意代码的字符串。我想到的危险是:
1.的解决方案很简单:捕获异常。我不确定 2 是否有一个好的解决方案。也许只是限制正则表达式的长度就可以了。
还有什么我需要担心的吗?
我已经开发了一个允许用户输入他们自己的正则表达式的程序,你是对的 - 他们可以(并且确实)输入可能需要很长时间才能完成的正则表达式 - 有时比宇宙的生命周期还要长。更糟糕的是,在处理正则表达式时,Python 持有 GIL,因此它不仅会挂起运行正则表达式的线程,还会挂起整个程序。
限制正则表达式的长度是行不通的,因为问题是回溯。例如,r"(\S+)+x"
在长度为 N 且不包含“x”的字符串上匹配正则表达式将回溯 2**N 次。在我的系统上,这需要大约一秒钟的时间来匹配,"a"*21
并且每个附加字符的时间加倍,因此 100 个字符的字符串大约需要 19167393131891000 年才能完成(这是一个估计,我没有计时)。
有关更多信息,请阅读 O'Reilly 的“掌握正则表达式”一书——其中有几章是关于性能的。
编辑 为了解决这个问题,我们编写了一个正则表达式分析函数,试图捕捉和拒绝一些更明显的退化案例,但不可能全部获得。
我们看到的另一件事是修补 re 模块以在它回溯太多次时引发异常。这是可能的,但需要更改 Python C 源代码并重新编译,因此不可移植。我们还提交了一个补丁来在匹配 python 字符串时释放 GIL,但我认为它没有被核心接受(python 只保存 GIL,因为正则表达式可以针对可变缓冲区运行)。
对于临时用户来说,给他们一个子集语言要简单得多。例如,shell 在fnmatch中的通配规则。SQL LIKE 条件规则是另一个示例。
将用户的语言翻译成适当的正则表达式,以便在运行时执行。
编译正则表达式应该是相当安全的。虽然它编译成的不是严格的 NFA(反向引用意味着它不是很干净),但它仍然应该很容易编译成。
现在关于性能特征,这完全是另一个问题。由于回溯,即使是很小的正则表达式也可能具有指数时间特征。定义特定的特征子集并仅支持您自己翻译的非常有限的表达式可能会更好。
如果你真的想支持一般的正则表达式,你要么必须信任你的用户(有时是一个选项),要么限制使用的空间和时间。我相信使用的空间仅由正则表达式的长度决定。
编辑:正如 Dave 所指出的,显然全局解释器锁在正则表达式匹配期间被持有,这会使设置超时更加困难。如果是这种情况,您设置超时的唯一选择是在单独的进程中运行匹配。虽然不完全理想,但它是可行的。我完全忘记了multiprocessing
。兴趣点是关于共享对象的这一部分。如果你真的需要硬约束,那么单独的流程就是这里的方法。
没有必要使用 compile() ,除非您需要重用许多不同的正则表达式。该模块已经缓存了最后的表达式。
如果您允许用户输入任何正则表达式,那么第 2 点(执行时)可能会非常困难。你可以用几个字符制作一个复杂的正则表达式,比如著名的那个(x+x+)+y
。我认为这是一个尚未以一般方式解决的问题。一种解决方法可能是启动一个不同的线程并对其进行监视,如果它超过了允许的时间,则终止该线程并返回一个错误。
我真的不认为仅仅通过将代码传递给 re.compile 来执行代码是不可能的。我理解它的方式,re.compile(或任何语言的任何正则表达式系统)将正则表达式字符串转换为有限自动机(DFA 或 NFA),尽管有不祥的名称“编译”,但它与执行任何代码。
从技术上讲,您不需要使用re.compile()
对字符串执行正则表达式操作。事实上,如果只执行一次操作,编译方法通常会更慢,因为初始编译会产生开销。
如果您担心“编译”这个词,那么请避免使用它,只需将原始表达式传递给match
,search
等。无论如何,您最终可能会稍微提高代码的性能。