【问题标题】:Regex taking surprisingly long time正则表达式花费了令人惊讶的长时间
【发布时间】:2012-09-12 01:26:44
【问题描述】:

我有一个用户输入的搜索字符串。通常,搜索字符串使用空格分隔,然后执行 OR 搜索(如果项目匹配任何搜索字符串元素,则匹配)。我想提供一些“高级”查询功能,例如使用引号将包含空格的文字短语括起来的能力。

虽然我已经敲定了一个不错的正则表达式来为我拆分字符串,但是执行它需要花费非常长的时间(在我的机器上 > 2 秒)。我打破它以找出打嗝的位置,更有趣的是,它似乎发生在最后一个 Match 匹配之后(大概是在输入的末尾)。直到字符串匹配结束的所有匹配都在更短的时间内捕获,但是最后一个匹配(如果是这样的话 - 没有返回)几乎花费了 2 秒。

我希望有人能对我如何加快这个正则表达式的速度有所了解。我知道我正在使用带有无限量词的后视,但就像我说的那样,在匹配最后一场比赛之前,这似乎不会导致任何性能问题。

代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace RegexSandboxCSharp {
    class Program {
        static void Main( string[] args ) {

            string l_input1 = "# one  \"two three\" four five:\"six seven\"  eight \"nine ten\"";

            string l_pattern =
                @"(?<=^([^""]*([""][^""]*[""])?)*)\s+";

            Regex l_regex = new Regex( l_pattern );

            MatchCollection l_matches = l_regex.Matches( l_input1 );
            System.Collections.IEnumerator l_matchEnumerator = l_matches.GetEnumerator();

            DateTime l_listStart = DateTime.Now;
            List<string> l_elements = new List<string>();
            int l_previousIndex = 0;
            int l_previousLength = 0;
            //      The final MoveNext(), which returns false, takes 2 seconds.
            while ( l_matchEnumerator.MoveNext() ) {
                Match l_match = (Match) l_matchEnumerator.Current;
                int l_start = l_previousIndex + l_previousLength;
                int l_length = l_match.Index - l_start;
                l_elements.Add( l_input1.Substring( l_start, l_length ) );

                l_previousIndex = l_match.Index;
                l_previousLength = l_match.Length;
            }
            Console.WriteLine( "List Composition Time: " + ( DateTime.Now - l_listStart ).TotalMilliseconds.ToString() );

            string[] l_terms = l_elements.ToArray();

            Console.WriteLine( String.Join( "\n", l_terms ) );

            Console.ReadKey( true );

        }
    }
}

输出
(这正是我得到的。)

一个
“二三”

五:“六七”

《九十》

【问题讨论】:

  • 你能在没有可变长度后视的情况下编写正则表达式吗?这大概就是问题所在。或者只是编写一个简单的解析器而不是正则表达式。
  • 我曾考虑使用解析器,但正则表达式似乎更简单。我需要做的就是将文本分成几块,牢记引号。直到最后一个 MoveNext() 之前,正则表达式就像狄更斯一样 - 这是唯一需要 2 秒的地方。
  • 感谢投票者反馈如何改进这个问题。
  • 您能否以变量转储的形式编写正则表达式查询的预期输出?然后我可以检查表达式,看看它是否真的在做它需要做的事情。
  • @CJxD - 我已经添加了预期的输出,但我得到的正是我想要的。在最后一次 MoveNext() 之前性能非常出色 - 返回 false - 这需要超过 2 秒。如果正则表达式引擎已经在输入的末尾,为什么要多花 2 秒才能返回 false

标签: c# regex performance


【解决方案1】:

尝试将您的正则表达式更改为以下内容:

(?<=^((?>[^"]*)(["][^"]*["])?)*)\s+

这里唯一的变化是将[^"]* 放入atomic group,这样可以防止catastrophic backtracking 发生。

注意:上面的正则表达式显然没有使用C#的正则表达式字符串语法,我不太熟悉,但我认为应该是这样的:

@"(?<=^((?>[^""]*)([""][^""]*[""])?)*)\s+";

为什么会发生灾难性的回溯:
找到所有有效匹配后,尝试的下一个匹配是最后引用部分内的空格。因为空格前有奇数个引号,所以后向操作会失败。

此时,lookbehind 内部的正则表达式将开始回溯。锚点意味着它将始终从字符串的开头开始,但它仍然可以通过从匹配的末尾删除元素来回溯。让我们看一下lookbehind里面的正则表达式:

^([^"]*(["][^"]*["])?)*

由于引用的部分是可选的,它们可以作为正则表达式回溯删除。对于不在引用部分内的每一块非引用字符,在回溯之前,每个字符都将被匹配为正则表达式开头的 [^"]* 的一部分。在该部分开始回溯时,最后一个字符将从[^"]* 匹配的内容中删除,并被外部重复拾取。在这一点上,它变得非常类似于上面灾难性回溯链接中的示例。

【讨论】:

  • 优秀。不过还是一头雾水。我原以为字符串断言 (^) 的开始会阻止灾难性的回溯。
  • (顺便说一句,正则表达式现在执行不到一毫秒。再次感谢。)
  • 我只是在回溯中添加了一些解释,希望它有意义,但解释起来有点棘手。本质上,您最终会得到与 ([^"]*)* 类似的行为,其中嵌套重复会导致正则表达式失败之前的指数级步骤。
猜你喜欢
  • 2019-10-27
  • 2016-05-15
  • 1970-01-01
  • 2023-04-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多