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

How can I find and replace text in a larger file (150MB-250MB) with regular expressions in C#?

我正在处理介于 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) 附加到在我的文件中找到的每个匹配项并保存它。

我看到大量有关替换文本的堆栈溢出的示例,但对于较大的文件则不多。要做什么的典型示例包括:

前两个的问题是它占用了大量内存,我担心程序是否能够处理所有这些。第三个选项的问题是我的正则表达式跨越多行,因此不会在一行中找到。我也看到其他帖子,但它们涵盖了替换特定文本字符串而不是使用正则表达式。

对我来说,将换页符附加到文件中找到的每个匹配项,然后保存该文件的好方法是什么?

编辑:

根据一些建议,我尝试使用 StreamReader.ReadLine()。具体来说,我会读取一行,看看它是否与我的表达式匹配,然后根据该结果写入一个文件。如果它匹配表达式,我会写入文件。如果它不匹配表达式,我会把它附加到一个字符串,直到它匹配表达式。像这样:

Regex myreg = new Regex("ABC: DEF11-1111(.?)更多数据(.?)更多数据(.*?)\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 分钟。必须有更好的方法来做到这一点...

我能够在合理的时间内找到有效的解决方案;它可以在 5 分钟内处理我的整个 150MB 文件。

首先,如评论中所述,在每次迭代后将字符串与 Regex 进行比较是一种浪费。相反,我是从这个开始的:

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");
}

因为我已经(大致)知道我的文件的大小,所以我可以继续初始化我的 stringbuilder。更不用说,如果您对一个字符串进行多项操作(我就是这样),那么使用字符串生成器会更有效率。从那里开始,只需将表单提要附加到我的每个匹配项即可。

最后,性能成本最高的部分:

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/

希望这对下一个人也有帮助。

如果您可以将整个字符串数据加载到单个字符串变量中,则无需先匹配然后将文本追加到循环中的匹配项中。您可以使用单个 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 字符匹配。