对于数据迁移项目,我需要对一长串短英语句子的格式进行基本验证。
出于某种原因,一些特定的字符串与非常缓慢匹配(在我的笔记本电脑上最长可达90秒)。
我最终得到了正则表达式的预期结果,但我很好奇为什么这个正则表达式如此缓慢。我自己也搞清楚了。
^((\()?[0-9a-zäàâèéêçôóû']+(\)(\s|$)|\s|-|\.|\s?/\s?|!|\?)?)+$
IgnoreCase|Compiled
(在.NET 4.5 / C#上运行)
查询:
longword longword longword longword ‘a’ and ‘b’
longword longword (longword longword).
对于较短的琴弦,它以正常速度工作。在上面的示例中用longword
替换word
将会加速很多事情。从第二个示例中删除.
也会使其正常工作。再添加一些longwords
,它可以运行一整天。
特别是第一个例子让我感到困惑,因为正则表达式中甚至不允许‘’
个字符。
答案 0 :(得分:3)
正则表达式将很慢失败,因为它包含一种嵌套的量词,由于它们创建的大量匹配可能会导致过度耗时的回溯。
过度回溯的常见罪魁祸首是具有灵活量词的相邻子模式,这两个子模式都可以匹配相同的子串,嵌套量词就是这样的一个例子。
考虑到正则表达式(\w+)+
可以通过8种不同方式匹配字符串"word"
word \w{4}
w-ord \w{1} \w{3}
wo-rd \w{2} \w{2}
wor-d \w{3} \w{1}
wo-r-d \w{2} \w{1} \w{1}
w-or-d \w{1} \w{2} \w{1}
w-o-rd \w{1} \w{1} \w{2}
w-o-r-d \w{1} \w{1} \w{1} \w{1}
以及128种方式的字符串"longword"
,以及2048种方式的"verylongword"
,很快就会清楚地看到模式与字符串字符串匹配的可能方式的数量随着其长度呈指数增长:
Math.Pow(2, string.Length - 1)
这就是为什么“在上面的例子中用字替换长字会加速很多事情”。
你的正则表达式比上面复杂得多,所以如果第一个不匹配的字符出现在字符串中,那么正则表达式引擎将不得不回溯并尝试大量的替代方法来匹配字符串直到该点之前可以确定完全匹配是不可能的。
您的正则表达式中没有任何内容可以匹配示例字符串中找到的).
和‘a’
,因此正则表达式会失败 - 但是当不可匹配的字符出现时,这将需要很长时间才能完成在字符串的末尾。
通过尝试匹配例如字符串
,可以确认嵌套量词是有问题的"longword longword longword!"
显然简单的模式
^([a-z\s]+)+$
在我的机器上,引擎无法找到匹配项需要十秒钟
如果添加了可选\s?
- ^([a-z\s]+\s?)+$
- 所用时间翻倍。
你的正则表达式有超过十种不同的可选?
替代品,可以在主角类[]
的每次匹配后考虑,这样会同样加剧回溯。
解决方案是通过使引擎成为原子组来防止引擎回溯到外部()
内的子模式匹配的内容。只需在打开?>
后添加(
即可完成此操作。
@"^(?>(\()?[0-9a-zäàâèéêçôóû']+(\)(\s|$)|\s|-|\.|\s?/\s?|!|\?)?)+$"
或等效,但可能更有效率
@"^(?>\(?[a-z0-9äàâèéêçôóû']+(?:[\s./!?-]|\)(?:\s|$))?)+$"
请注意,这仍然不允许).
或)!
等您可能想要这样做。
请参阅Optimizing Regular Expression Performance, Part II: Taking Charge of Backtracking