【问题标题】:C# regex performance with whitespace is too slow带有空格的 C# 正则表达式性能太慢
【发布时间】:2012-01-27 17:10:57
【问题描述】:

我在 C# 中使用 WinForms NET 2.0。

我有大约 1000-1500 行的文本文件。其中的某些行以 4 个或更多字母单词开头,我必须在这些单词中添加一个冒号。在这些行的开头有空格是可选的,并且该行可以包含除这些单词之外的更多文本。这是一个例子:

    lda $00,x
    mov $20
    rep #$20
    tax
    lda #$0000,y
word
    ...         ; comment
  anotherword           ; this word has whitespace before it.

另外,如果已经有一个冒号,它会简单地忽略它们以防止添加更多。这是我的代码:

Regex R = new Regex(@"^\s*(?<word>[A-Za-z0-9_]{4,})", RegexOptions.Multiline); //keep the words stored in a group called word
MatchCollection M = R.Matches(txt); //let my text file string be "txt"

foreach (Match m in M)
{
    string mm = m.Groups["word"].Value;
    if (!Regex.IsMatch(txt, @"^\s*\b" + mm + @"\b:", RegexOptions.Multiline)) // if already a colon, return
        txt = Regex.Replace(txt, @"^\s*\b" + mm + @"\b", mm + ":", RegexOptions.Multiline);
}

一切正常,但问题是什么?它太慢了。我在文本文件中执行其他操作,但我已经确认它们很快,问题出在我上面的正则表达式中的两个“\s*”。当我同时删除它们时,搜索速度会提高 10 倍。

我该如何解决这个问题?

【问题讨论】:

  • 使用多行正则表达式解析非常大的字符串会很慢。为什么不逐行读取文本文件并迭代地应用正则表达式?我至少会尝试一下并比较速度 - 实际上它可能更快。例如,从我自己的实验中,我发现拥有 2 个单独的正则表达式通常比 1 个带有交替的正则表达式更快(“或”运算符 |)。
  • @bobbymcr:嗯,我之前应该考虑过。我试一试,然后看看结果。
  • @zneak:我正在做的是 6502 组装,但这只是为了举例而抛出的一些随机代码。

标签: regex search .net-2.0 whitespace performance


【解决方案1】:

@TimPietzcker 的替代解决方案:

result = Regex.Replace(subject, @"^(?>(\s*\w{4,}))(?!:)", "$1:", RegexOptions.Multiline);

(?&gt;...) 是一个原子分组。当正则表达式引擎进入原子分组时,不允许回溯该分组已使用的输入中的任何位置。

现在,为什么这是有益的?考虑一行:

             ab3 #13 spaces, then a, b, 3

如果不使用原子分组,则当正则表达式无法匹配第二个量词中的第4个字符时,它必须回溯到a之前的字符:但它是一个空格,它不匹配。以此类推,直到到达行首之前的字符,其中^ 不匹配,然后才声明失败(\s* 可以匹配空字符串)。

使用原子分组,引擎不会以这种方式回溯,这是性能的巨大提升,尤其是在处理大数据时。

【讨论】:

  • 是的,所有格/原子模式可能会提高一点效率,但是这个表达式比蒂姆的更大的改进是:^ 开头的锚点。这减少了引擎尝试每次匹配的位置数量(假设 .NET 实现了:“字符串/行优化开始” - 请参阅:MRE3 第 6 章第 246 页)。但是,.NET 可能足够聪明,可以对 Tim 的表达式应用这种优化(但我有点怀疑。)+1。
  • 在这种情况下必须是原子团; .NET 不支持所有格量​​词。
  • @AlanMoore 真的吗?我以为在最新版本中是这样的
  • 不是.NET 4.5。它也一直困扰着我。所有格量词so更方便。
  • Regex 中有很多我不知道的东西!我可能会详细了解它。另一方面,我将如何为此添加“例外”?例如,大约有 50 个 4 字母单词可能会开始行首,我不想在这些单词后面附加冒号。
【解决方案2】:

我在这里看到三个主要问题:

  1. 您在每一行执行了多达 3 次基本相同的正则表达式匹配。正如 Tim 所演示的,您不必多次触摸任何行,无论它是否与正则表达式匹配。此外,在使用相同的正则表达式执行 Replace() 之前,您永远不需要使用 Match() 或 IsMatch() 测试字符串。如果字符串与正则表达式不匹配,Replace() 将简单地原封不动地返回它。

  2. 没有必要像你现在做的那样在我的手上构建替换字符串。这就是捕获组的用途。

  3. \s 匹配所有空白字符,包括换行符。如果(例如)有九个空白行后跟一个匹配行,则正则表达式将消耗所有十行。如果第十行 * 不匹配,正则表达式引擎将放弃匹配尝试并从第二个空白行开始再次尝试。再次在第三行,第四行等。如果从您的正则表达式中删除 \s* 有很大的影响,这可能是原因:它试图匹配很多不必要的空格。如果您知道要查找的字符串始终位于一行,则应确保正则表达式仅匹配 horizo​​ntal 空格,即空格和制表符。

演示:

result = Regex.Replace(subject, @"(?m)^([ \t]*\w{4,})(?![\w:])", "$1:");

解释一下:

  • (?m) 只是一种更方便的方式来指定 Multiline 选项。
  • ^([ \t]*\w{4,}) 匹配行中的第一个单词以及任何前导空格,并将其全部捕获到第 1 组中。
  • (?![\w:]) 是负前瞻;它断言下一个字符(如果有的话)既不是单词字符也不是冒号。这样可以确保您使用了整个单词,并且单词后面没有冒号。
  • 在替换参数中,$1 是第一个捕获组内容的占位符。

我注意到您的正则表达式匹配前导空格而不捕获它,并且您没有在替换中添加任何内容。效果是从您执行此替换的任何行中删除前导空格,但不会从任何其他行中删除。如果这确实是您想要的,您可以将^([ \t]*\w{4,}) 更改为^[ \t]*(\w{4,})

【讨论】:

  • 我从没想过使用 [ \t] 代替 \s(我也没有想到 \s 检查换行符),感谢您提供的所有提示。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-03-30
  • 1970-01-01
  • 2011-09-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多