【问题标题】:Bad Regex performance while searching for times (xx:xx:xx)搜索时间时正则表达式性能不佳 (xx:xx:xx)
【发布时间】:2014-04-28 19:32:13
【问题描述】:

我必须处理一个大文件(几 MB)并从中删除以时间标记的 cmets。一个例子:

blablabla  12:10:40 I want to remove this
blablabla some more
even more bla

过滤后,我希望它看起来像这样:

blablabla
blablabla some more
even more bla

最好的方法应该是简化正则表达式:

Dataout = Regex.Replace(Datain, "[012][0123456789]:[012345][0123456789]:[012345][0123456789].*", string.Empty, RegexOptions.Compiled);

现在这非常适合我的目的,但它有点慢。我假设这是因为前两个字符 [012] 和 [0123456789] 与很多数据匹配(它是一个包含十六进制的 ASCII 文件数据,例如“0045ab0123”等)。所以正则表达式经常匹配前两个字符。

当我将正则表达式更改为

Dataout = Regex.Replace(Datain, ":[012345][0123456789]:[012345][0123456789].*", string.Empty, RegexOptions.Compiled);

它得到了巨大的加速,可能是因为文件中根本没有很多 ':'。好的!但是我仍然需要检查第一个“:”之前的两个字符是数字,然后删除该行的其余部分。

所以我的问题归结为:

  • 如何让正则表达式首先搜索最不频繁出现的 ':' 并且只有在找到匹配项后才检查之前的两个字符?

或者也许还有更好的方法?

【问题讨论】:

  • 日期前是否总是有空格?
  • lookbehind 在这里有用吗?我不确定lookbehind 是在找到潜在匹配之后还是在它检查匹配之前得到评估。
  • 不,不幸的是前面并不总是有空格,所以它可能看起来像:“0A0B1216:43:11 blabla”。我同意如果有空间,搜索会更容易..
  • 您真的应该阅读此stackoverflow.com/questions/513412/… 以更好地了解RegexOptions.Compiled 在哪些方面有助于您提高速度,以及在哪些方面对其不利,因此您可以正确利用它。
  • 您可以使用:(?<=[0-2][0-9]:)[0-5][0-9]:[0-5][0-9].* 执行全局搜索,然后在matchCollection 中使用反向循环,使用match.index 和匹配长度删除子字符串。 AdrianHHH 用这种方式写了一个答案。

标签: c# regex performance match


【解决方案1】:

您可以在问题中使用这两个正则表达式。首先与前导冒号表达式匹配以快速查找或排除可能的行。如果成功,则使用完整的替换表达式。

MatchCollection mc = Regex.Matches(Datain, ":[012345][0123456789]:[012345][0123456789].*"));

if ( mc != null && mc.Length > 0 )
{
    Dataout = Regex.Replace(Datain, "[012][0123456789]:[012345][0123456789]:[012345][0123456789].*", string.Empty, RegexOptions.Compiled);
}
else
{
    Dataout = Datain;
}

变化可能是

Regex finder = new Regex(":[012345][0123456789]:[012345][0123456789].*");
Regex changer = new regex("[012][0123456789]:[012345][0123456789]:[012345][0123456789].*");

if ( finder.Match(Datain).Success)
{
    Dataout = changer.Replace(Datain, string.Empty);
}
else
{
    Dataout = Datain;
}

另一个变体是使用上面的finder。如果找到字符串,则只需检查前两个字符是否为数字。

Regex finder = new Regex(":[012345][0123456789]:[012345][0123456789].*");

Match m = finder.Match(Datain);
if ( m.Success && m.Index > 1)
{
    if ( char.IsDigit(DataIn[m.index-1]) && char.IsDigit(DataIn[m.index-2])
    {
        Dataout = m.Index-2 == 0 ? string.Empty : DataIn.Substring(0, m.Index-2);
    }
    else
    {
        Dataout = Datain;
    }
}
else
{
    Dataout = Datain;
}

在第二个和第三个想法中,finder 和changer 应该在读取任何行之前声明并赋予值。行读循环内无需执行new Regex(...)。

【讨论】:

  • 关于“另一种变体”:我用 PHP 和:(?<=[0-2][0-9]:)[0-5][0-9]:[0-5][0-9].* 模式测试了相同的方法,得到的结果比使用模式[0-2][0-9]:[0-5][0-9]:[0-5][0-9].* 的简单替换快 9 到 10 倍。但是,我不知道 C# 版本的时间收益。
【解决方案2】:

您可以使用DateTime.TryParseExact 来检查一个单词是否是一个时间,并获取之前的所有单词。这是一个清除路径中所有行的 LINQ 查询,也许它更有效:

string format = "HH:mm:ss";
DateTime time;
var cleanedLines = File.ReadLines(path)
    .Select(l => string.Join(" ", l.Split().TakeWhile(w => w.Length != format.Length
       ||  !DateTime.TryParseExact(w, format, CultureInfo.InvariantCulture, DateTimeStyles.None, out time))));

如果性能非常关键,您还可以创建针对此任务优化的专用方法。这是一种应该更有效的方法:

public static string SubstringBeforeTime(string input, string timeFormat = "HH:mm:ss")
{
    if (string.IsNullOrWhiteSpace(input))
        return input;
    DateTime time;

    if (input.Length == timeFormat.Length && DateTime.TryParseExact(input, timeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out time))
    {
        return ""; // full text is time
    }
    char[] wordSeparator = {' ', '\t'};
    int lastIndex = 0;
    int spaceIndex = input.IndexOfAny(wordSeparator);
    if(spaceIndex == -1)
        return input;
    char[] chars = input.ToCharArray();
    while(spaceIndex >= 0)
    {
        int nonSpaceIndex = Array.FindIndex<char>(chars, spaceIndex + 1, x => !wordSeparator.Contains(x));
        if(nonSpaceIndex == -1)
            return input;
        string nextWord = input.Substring(lastIndex, spaceIndex - lastIndex);
        if( nextWord.Length == timeFormat.Length 
         && DateTime.TryParseExact(nextWord, timeFormat, CultureInfo.InvariantCulture, DateTimeStyles.None, out time))
        {
            return input.Substring(0, lastIndex);
        }
        lastIndex = nonSpaceIndex;
        spaceIndex = input.IndexOfAny(wordSeparator, nonSpaceIndex + 1);
    }
    return input;
}

样本数据和测试:

string[] lines = { "blablabla  12:10:40 I want to remove this", "blablabla some more", "even more bla  ", "14:22:11" };
foreach(string line in lines)
{
    string newLine = SubstringBeforeTime(line, "HH:mm:ss");
    Console.WriteLine(string.IsNullOrEmpty(newLine) ? "<empty>" : newLine);
}

输出:

blablabla  
blablabla some more
even more bla  
<empty>

【讨论】:

  • 在这里使用 AND 操作不会比 OR 更好吗?无论哪种方式,我都绝对想知道它是否更快
  • @Jonesy:这是不正确的,只要字长为 != 8 或(如果字长正好是 8)只要这个字不是一次。如果两者之一为真,则将采用该词。
  • 哦,我明白了,我在想它有点颠倒
【解决方案3】:

最后我选择了这个:

        bool MeerCCOl = true;
        int startpositie = 0;
        int CCOLfound = 0; // aantal keer dat terminal output is gevonden

        while(MeerCCOl)
        {
            Regex rgx = new Regex(":[0-5][0-9]:[0-5][0-9]", RegexOptions.Multiline | RegexOptions.Compiled);
            Match GevondenColon = rgx.Match(VlogDataGefilterd,startpositie);

            MeerCCOl = GevondenColon.Success; // CCOL terminal data gevonden, er is misschien nog meer..

            if (MeerCCOl && GevondenColon.Index >= 2)
            {
                CCOLfound++;
                int GevondenUur = 10 * (VlogDataGefilterd[GevondenColon.Index - 2] - '0') +
                                        VlogDataGefilterd[GevondenColon.Index - 1] - '0';
                if (VlogDataGefilterd[GevondenColon.Index - 2] >= '0' && VlogDataGefilterd[GevondenColon.Index - 2] <= '2' &&
                    VlogDataGefilterd[GevondenColon.Index - 1] >= '0' && VlogDataGefilterd[GevondenColon.Index - 1] <= '9' &&
                    GevondenUur>=0 && GevondenUur<=23)
                {
                    Regex rgx2 = new Regex("[012][0-9]:[0-5][0-9]:[0-5][0-9].*", RegexOptions.Multiline);
                    VlogDataGefilterd = rgx2.Replace(VlogDataGefilterd, string.Empty, 1, (GevondenColon.Index - 2));
                    startpositie = GevondenColon.Index - 2; // start volgende match vanaf de plek waar we de 
                }
            }
        }

它首先搜索 :xx:xx 的匹配项,然后检查之前的 2 个字符。如果它被识别为时间,它将删除整个事物。额外的好处是,通过单独检查小时数,我可以确保小时数是 00-23,而不是 00-29。匹配的数量也是这样计算的。

最初的简单正则表达式大约需要 550 毫秒。对于同一个数据文件,这段代码(虽然更混乱)大约需要 12 毫秒。这是惊人的 40 倍加速 :-)

谢谢大家!

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-11-01
    • 1970-01-01
    • 1970-01-01
    • 2018-02-11
    • 1970-01-01
    • 1970-01-01
    • 2014-06-02
    • 2023-04-10
    相关资源
    最近更新 更多