如何在C#中使用正则表达式查找和替换较大文件(150MB-250MB)中的文本?

3

我正在处理大小在150MB至250MB之间的文件,并且需要为匹配集合中的每个匹配项添加一个换页符(/f)字符。目前,我每个匹配项的正则表达式如下:

Regex myreg = new Regex("ABC: DEF11-1111(.*?)MORE DATA(.*?)EVEN MORE DATA(.*?)\f", RegexOptions.Singleline);

我希望你能够修改文件中的每个匹配项(然后覆盖该文件),使其成为稍后可以使用较短的正则表达式找到的内容:

Regex myreg = new Regex("ABC: DEF11-1111(.*?)\f\f, RegexOptions.Singleline);

换句话说,我想在文件中每次找到匹配项时简单地附加一个换页符(\f)并保存它。
我在stackoverflow上看到了很多替换文本的例子,但对于较大的文件则不太常见。典型的做法包括:
- 使用streamreader将整个文件存储在字符串中,然后在该字符串中进行查找和替换。 - 使用MatchCollection与File.ReadAllText()结合使用 - 逐行读取文件并在那里查找匹配项。
前两种方法的问题在于它们会消耗大量内存,我担心程序能否处理所有这些内容。第三个选项的问题是我的正则表达式跨越多行,因此无法在单行中找到。我还看到其他帖子,但它们涵盖替换特定文本字符串而不是使用正则表达式。
对于我在文件中找到的每个匹配项附加一个换页符,然后保存该文件的一个好方法是什么?
编辑:
根据一些建议,我尝试玩弄StreamReader.ReadLine()。具体来说,我会读取一行,查看它是否与我的表达式匹配,然后根据结果写入文件。如果它与表达式匹配,我会写入文件。如果它没有匹配表达式,我会将其附加到一个字符串中,直到它匹配表达式。就像这样:
Regex myreg = new Regex("ABC: DEF11-1111(.?)MORE DATA(.?)EVEN MORE DATA(.*?)\f", RegexOptions.Singleline);
//For storing/comparing our match.
string line, buildingmatch, match, whatremains;
buildingmatch = "";
match = "";
whatremains = "";

//For keep track of trailing bits after our match.
int matchlength = 0;

using (StreamWriter sw = new StreamWriter(destFile))
using (StreamReader sr = new StreamReader(srcFile))
{
    //While we are still reading lines in the file...
    while ((line = sr.ReadLine()) != null)
    {
        //Keep adding lines to buildingmatch until we can match the regular expression.
        buildingmatch = buildingmatch + line + "\r\n";
        if (myreg.IsMatch(buildingmatch)
        {
            match = myreg.Match(buildingmatch).Value;
            matchlength = match.Lengh;
            
            //Make sure we are not at the end of the file.
            if (matchlength < buildingmatch.Length)
            {
                whatremains = buildingmatch.SubString(matchlength, buildingmatch.Length - matchlength);
            }
            
            sw.Write(match, + "\f\f");
            buildingmatch = whatremains;
            whatremains = "";
        }
    }
}

问题在于处理大约150MB的文件需要大约55分钟的时间。必须有更好的方法来解决这个问题...


听起来你可以考虑另一种方法。例如,你能否从一个“ABC:”读取字符串块,并在块上进行匹配? - tym32167
@tym32167 感谢您的回复!不幸的是,由于文件的方式,从一个ABC:到另一个ABC:的匹配是不可能的。实际上,ABC:本身在同一匹配中出现多次;不幸的是,有很多重复的数据,这就是为什么我需要使用我所拥有的正则表达式的原因。 - nightmare637
1
为什么花费的时间这么长呢?因为 buildingmatch +=line; 不断地将一行文本添加到缓冲区中。每次添加后,您都会运行相同的正则表达式,该表达式从缓冲区的开头开始搜索。它无法匹配,因为它找不到结尾,因为尚未添加。就像 !N (N阶乘) 一样。想象一下,在缓冲区中搜索 Q,当每次搜索从开头开始时,缓冲区每次扩展1个字符,并且在第10,000次通过后才添加 Q - sln
如果你正在尝试做一个FIFO类型的缓冲栈,最好保留一些使用指标,如最大缓冲区大小、最大行数、平均行大小。这将告诉你这种方法的有效性是否良好。 - sln
1
最后,如果您需要保留整个记录在缓冲区中,更好的方法是识别一行内的开头,即您知道的恒定值。例如,在一行上运行主正则表达式的部分:ABC:DEF11-1111。如果匹配,则重新开始缓冲。然后在每行中查找结尾\f,如果没有找到则添加到缓冲区中。如果找到了,则在缓冲区上运行主正则表达式一次。如果匹配,则将记录取出,清除缓冲区并开始寻找新记录。如果不匹配,则继续添加行,但仅检查行以寻找结尾。等等... - sln
显示剩余7条评论
4个回答

2

如果您可以将整个字符串数据加载到一个字符串变量中,就没有必要先匹配,然后在循环中附加文本到匹配项中。您可以使用单个Regex.Replace操作:

string text = File.ReadAllText(srcFile);
using (StreamWriter sw = new StreamWriter(destfile, false, Encoding.UTF8, 5242880))
{
     sw.Write(myregex.Replace(text, "$&\f\f"));
}

详情:

  • string text = File.ReadAllText(srcFile); - 读取srcFile文件并将其存储到text变量中(match表达会有歧义)
  • myregex.Replace(text, "$&\f\f") - 将所有myregex的匹配项替换为它们自身($&是对整个匹配值的反向引用),同时在每个匹配项后附加两个\f字符。

1
我能够在合理的时间内找到一个可行的解决方案;它可以在不到5分钟的时间内处理完我的整个150MB文件。
首先,正如评论中提到的那样,在每次迭代后将字符串与正则表达式进行比较是一种浪费。相反,我从这里开始:
string match = File.ReadAllText(srcFile);
MatchCollection mymatches = myregex.Matches(match);

字符串可以容纳多达2GB的数据,因此虽然不是理想的,但我认为大约150MB的数据存储在字符串中不会有问题。然后,与每读入x行文件时检查一次匹配相比,我可以一次性检查文件中的所有匹配项!

接下来,我使用了这个:

StringBuilder matchsb = new StringBuilder(134217728);
foreach (Match m in mymatches)
{
     matchsb.Append(m.Value + "\f\f");
}

既然我已经大致知道文件的大小,我可以开始初始化我的字符串构建器。更何况,如果您对字符串执行多个操作(我就是这种情况),使用字符串构建器会更加高效。从那里开始,只需要将换页符附加到每个匹配项即可。

最后,导致性能损失的部分:

using (StreamWriter sw = new StreamWriter(destfile, false, Encoding.UTF8, 5242880))
{
     sw.Write(matchsb.ToString());
}

通常情况下,初始化StreamWriter的方式至关重要。通常只需将其声明为:
StreamWriter sw = new StreamWriter(destfile);

这对大多数用例来说都是可以的,但当你处理更大的文件时问题就显而易见了。像这样声明时,你会使用默认缓冲区4KB写入文件。对于较小的文件,这很好。但对于150MB的文件呢?这将最终需要很长时间。所以我通过将缓冲区更改为约5MB来纠正了这个问题。
我发现这个资源真的帮助我更有效地理解如何写入文件:https://www.jeremyshanks.com/fastest-way-to-write-text-files-to-disk-in-c/ 希望这也能帮助下一个人。

1
我很想知道在正则表达式中使用Replace方法是否可以进一步提高性能。即使没有提高性能,这至少会使代码变得更简单,因为您不再需要StringBuilder了... - Simon MᶜKenzie
1
这可能是个好主意;我会尝试并让你知道结果! - nightmare637
1
@SimonMᶜKenzie,Replace方法并没有显著提高性能,但正如您所指出的那样,它确实使代码简单了很多。再次感谢您的建议! - nightmare637
.NET 6 可能会进一步提高性能,除非您已经在使用它。阅读此文:https://devblogs.microsoft.com/dotnet/file-io-improvements-in-dotnet-6/ - Bent Tranberg
如果你仍在做这种事情,@nightmare637,你可能想看一下Gigantor。它支持对巨大文件进行正则表达式搜索和替换。我的笔记本电脑上完成了32 GB的测试,共进行了13,952次匹配/替换,只用了38秒。因此,对于您的250 MB数据,应该只需要不到一秒的时间。 - dynamicbutter

0
即使您的用例是处理无法放入内存的文件,Gigantor 也能让操作变得快速简便。
// Create the progress event required by Gigantor
System.Threading.AutoResetEvent progress = new(false);

// Create a regular expression
System.Text.RegularExpressions.Regex regex = new(
    "ABC: DEF11-1111(.*?)MORE DATA(.*?)EVEN MORE DATA(.*?)\f",
    RegexOptions.Compiled);

// Create the searcher
Imagibee.Gigantor.RegexSearcher searcher = new(srcPath, regex, progress);

// Do the search
Imagibee.Gigantor.Background.StartAndWait(searcher, progress, (_) => { });

// Add extra form feed to each match
using System.IO.FileStream output = File.Create(destPath);
searcher.Replace(output, (match) => { return $"{match.Value}\f"; } );

0

在C#中处理大型文本文件并需要执行搜索和替换操作时,有几种方法可以考虑以优化性能。

一种方法是使用内存映射文件。内存映射文件允许您像使用内存数组一样访问大型文件,这比使用标准文件I/O更有效率。要使用内存映射文件,您可以在C#中使用MemoryMappedFile类。

如果内存映射文件是可行的选项,则它们可以比传统的读写方法提供更快速的访问文件内容的方式。


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