如何在 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将整个文件存储在一个字符串中,然后做一个
在该字符串中查找并替换。
- 结合使用 MatchCollection
File.ReadAllText()
- 逐行读取文件并查找
在那里匹配。
前两个的问题是它占用了大量内存,我担心程序是否能够处理所有这些。第三个选项的问题是我的正则表达式跨越多行,因此不会在一行中找到。我也看到其他帖子,但它们涵盖了替换特定文本字符串而不是使用正则表达式。
对我来说,将换页符附加到文件中找到的每个匹配项,然后保存该文件的好方法是什么?
编辑:
根据一些建议,我尝试使用 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
字符匹配。
我正在处理介于 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将整个文件存储在一个字符串中,然后做一个 在该字符串中查找并替换。
- 结合使用 MatchCollection File.ReadAllText()
- 逐行读取文件并查找 在那里匹配。
前两个的问题是它占用了大量内存,我担心程序是否能够处理所有这些。第三个选项的问题是我的正则表达式跨越多行,因此不会在一行中找到。我也看到其他帖子,但它们涵盖了替换特定文本字符串而不是使用正则表达式。
对我来说,将换页符附加到文件中找到的每个匹配项,然后保存该文件的好方法是什么?
编辑:
根据一些建议,我尝试使用 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
字符匹配。