C#正则表达式性能非常缓慢

23

我在正则表达式方面非常新手。我想使用以下正则表达式解析日志文件:

(?<time>(.*?))[|](?<placeholder4>(.*?))[|](?<source>(.*?))[|](?<level>[1-3])[|](?<message>(.*?))[|][|][|](?<placeholder1>(.*?))[|][|](?<placeholder2>(.*?))[|](?<placeholder3>(.*))

日志行看起来像这样:

2001.07.13 09:40:20|1|SomeSection|3|====== Some log message::Type: test=sdfsdf|||.\SomeFile.cpp||60|-1

一个大约有3000行的日志文件需要很长时间来解析它。您有一些提示可以加快性能吗?谢谢...

更新: 我使用正则表达式,因为我使用不同结构的日志文件,并且我这样使用:

string[] fileContent = File.ReadAllLines(filePath);
Regex pattern = new Regex(LogFormat.GetLineRegex(logFileFormat));

foreach (var line in fileContent)
{
   // Split log line
   Match match = pattern.Match(line);

   string logDate = match.Groups["time"].Value.Trim();
   string logLevel = match.Groups["level"].Value.Trim();
   // And so on...
}

解决方案:
感谢您的帮助。我已经进行了测试并得出以下结果:

1.) 仅添加了RegexOptions.Compiled:
从00:01:10.9611143到00:00:38.8928387

2.) 使用了Thomas Ayoub的正则表达式
从00:00:38.8928387到00:00:06.3839097

3.) 使用了Wiktor Stribiżew的正则表达式
从00:00:06.3839097到00:00:03.2150095


6
为什么要使用正则表达式呢?看起来你可以只使用String.Split来获取列数组。 - kiziu
7
除非你确实需要选择这种特定列中有数字的行,否则可以使用String.Split()。否则,请将所有的.*?替换为[^|]*,同时将[|][|][|]替换为[|]{3},这样可能会使代码运行更快。实际上,即使你需要检查某些特定列的值是否为数字,也可以使用非正则表达式的代码来实现。 - Wiktor Stribiżew
你还没有展示给我们实际上如何匹配正则表达式 - 请添加相关的代码。 - RB.
哦,没错,正则表达式对象——如果使用非静态方法——应该在循环外创建。 - Wiktor Stribiżew
字符类[|]似乎是获取一个字符的低效方式。正则表达式引擎可能会将其优化为单个显式字符,但也可能不会。我建议在整个代码中将它们替换为\| - AdrianHHH
3个回答

24

既然我现在看到你对正则表达式的性能有所了解,那么让我把我的评论“转换”成答案吧。

如我上面所提到的,用[^|]*替换所有的.*?,并用[|]{3}(或类似的,取决于[|]的数量)替换所有重复的[|][|][|]。此外,不要使用嵌套捕获组,这也会影响性能!

var logFileFormat = @"(?<time>[^|]*)[|](?<placeholder4>[^|]*)[|](?<source>[^|]*)[|](?<level>[1-3])[|](?<message>[^|]*)[|]{3}(?<placeholder1>[^|]*)[|]{2}(?<placeholder2>[^|]*)[|](?<placeholder3>.*)";

由于它会获取行的其余部分,因此只有最后一个 .* 可以保留“通配符”。

RegexHero上,这是您和我的正则表达式模式的比较。

输入图像描述

然后使用 RegexOptions.Compiled

Regex pattern = new Regex(LogFormat.GetLineRegex(logFileFormat), RegexOptions.Compiled);

你也可以使用RegexOptions.ExplicitCapture而不是删除嵌套捕获组。 - Markus
这是有道理的,因为命名组将捕获相关细节。 - Wiktor Stribiżew

12
如果您多次使用相同的正则表达式,请确保编译它,以避免每次重新创建正则表达式。这可以使性能提升数个数量级。
var regex = new Regex(".*", RegexOptions.Compiled);

以下是LinqPad代码,展示了使用正则表达式的三种方法,从快到慢排列。 regexFast 方法需要大约5秒钟,regexSlow 方法需要6秒钟,而regexSlowest 则需要大约50秒钟。请注意保留HTML标签。
void Main()
{
    var sw = new Stopwatch();

    var regex = @"(?<first>T[he]{2})\s*\w{5}.*";

    // This is the fastest method.
    sw.Restart();
    var regexFast = new Regex(regex, RegexOptions.Compiled);
    for (int i = 0; i < 9999999; i++)
    {
        regexFast.Match("The quick brown fox");
    }
    sw.Stop();
    sw.ElapsedMilliseconds.Dump();

    // This is a little slower - we didn't compile the regex so it has 
    // to do some extra work on each iteration.
    sw.Restart();
    var regexSlow = new Regex(regex);
    for (int i = 0; i < 9999999; i++)
    {
        regexSlow.Match("The quick brown fox");
    }
    sw.Stop();
    sw.ElapsedMilliseconds.Dump();

    // This method is super slow - we create a new Regex each time, so 
    // we have to do *lots* of extra work.
    sw.Restart();
    for (int i = 0; i < 9999999; i++)
    {
        var regexSlowest = new Regex(regex);
        regexSlowest.Match("The quick brown fox");
    }
    sw.Stop();
    sw.ElapsedMilliseconds.Dump();
}

你也可以使用正则表达式缓存:https://dev59.com/s3RC5IYBdhLWcg3wFNDX,性能应该与第二种方式相似。 - Guillaume

4
你的正则表达式可以进行优化,具体如下:
(?<time>([^|]*))[|](?<placeholder4>([^|]*))[|](?<source>([^|]*))[|](?<level>[1-3])[|](?<message>([^|]*))[|]{3}(?<placeholder1>([^|]*))[|][|](?<placeholder2>([^|]*))[|](?<placeholder3>([^|]*))

使用否定字符类(negated char class)而不是懒惰量词(lazy quantifiers)。这可以减少回溯。使用这种方法后,Regex101 的步骤从 316 减少到了 47。结合 RB. 的答案,你就可以得到好的结果了。


“Compiled”可能会有所帮助,正如RB所说,但是那些“.*?”是真正的问题。如果我在做这件事的时候能够摆脱嵌套组就好了。 - Alan Moore

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接