C#字符串多重正则替换-内存占用过高

8

基本上,我想做的是在单个字符串上运行多个(15-25)正则表达式替换,并实现最佳的内存管理。

概述: 通过ftp流式传输仅为文本的文件(有时为html),将其附加到 StringBuilder 以获取非常大的字符串。文件大小范围从300KB到30MB。

正则表达式有一定复杂性,但需要文件的多行(例如标识书的部分),因此任意地断开字符串或在每个下载循环中运行替换都不可行。

一个样例替换:

Regex re = new Regex("<A.*?>Table of Contents</A>", RegexOptions.IgnoreCase);
source = re.Replace(source, "");

每次替换运行时,内存都会飙升,我知道这是因为在C#中,字符串是不可变的,所以需要进行复制——即使我调用GC.Collect()也无法帮助减少30MB文件的内存消耗。
有没有更好的方法来处理这个问题,或者有没有一种使用恒定内存执行多个正则表达式替换的方法(制作2份副本(因此在内存中占用60MB),执行搜索,丢弃副本并返回到30MB)?
更新:
看起来没有简单的答案,但是对于未来遇到这个问题的人们,我最终结合了以下所有答案才将其达到可接受的状态:
  1. 如果可能,将字符串拆分成块,请参见manojlds的答案,以便在读取文件时寻找适当的结束点。

  2. 如果无法拆分,则至少稍后拆分——请参见ChrisWue的答案,以获取一些可以帮助该过程的外部工具,以管道传输到文件。

  3. 优化正则表达式,避免贪婪操作符,并尽可能限制引擎必须执行的操作量——请参见Sylverdrag的答案。

  4. 尽可能组合正则表达式,这可以减少替换的数量,特别是在正则表达式不基于彼此时非常有用(在清理错误输入方面非常有用)——请参见Brian Reichle的答案获取代码示例。

谢谢大家!

我在字符串上每次调用正则表达式,编译会对它进行替换的次数有帮助吗?例如在样例正则表达式中,如果要替换500个“Table of Contents”,那么编译版本会运行得更快吗? - WSkid
抱歉,我意识到我的错误并删除了我的评论,但您已经回复了。是的,编译可能对您没有好处。 - manojlds
我不确定需求是否允许您解析文件行。如果文件中的每一行可以独立考虑,那么我建议您解析每个文件行(CPU vs memory??),而不是将整个文件加载到内存中。CPU周期/所需时间可能会增加,但我认为使用的内存会减少。您可以尝试一下。 - Sandeep G B
你的内存消耗是因为Regex.Replace构建了一个新的字符串吗?如果是这样,那么不要使用Regex.Replace,而应该使用Regex.Match,并编写自己的替换引擎,它会读取/扫描输入字符串并编写输出流/ StringBuilder来替换Regex.Match为您找到的部分。这会有效吗? - Andrew Savinykh
4个回答

2

根据正则表达式的性质,您可能能够将它们合并为单个正则表达式,并使用接受MatchEvaluator委托的Replace()重载来确定匹配字符串的替换。

Regex re = new Regex("First Pattern|Second Pattern|Super(Mega)*Delux", RegexOptions.IgnoreCase);

source = re.Replace(source, delegate(Match m)
{
    string value = m.Value;

    if(value.Equals("first pattern", StringComparison.OrdinalIgnoreCase)
    {
        return "1st";
    }
    else if(value.Equals("second pattern", StringComparison.OrdinalIgnoreCase)
    {
        return "2nd";
    }
    else
    {
        return "";
    }
});

当然,如果后续的模式需要匹配先前替换的结果,则该方法会失效。

是的,抱歉我忘了添加一些替换是基于其他替换的。但是,使用这种方法,我能够将多个替换组合在一起,并且仅需要大约5个Replace调用-谢谢! - WSkid

2

我有一个相似的情况。

使用正则表达式的编译选项:

Source = Regex.Replace(source, pattern, replace, RegexOptions.Compiled);

根据您的情况,这可以极大地提高速度。

对于大于3-4 Mb的文件,这不是完全的解决方案。

如果您可以决定应该运行哪个正则表达式(不是我的情况),您应该尽可能优化正则表达式,避免昂贵的操作。例如,避免使用非贪婪操作符,避免使用向前和向后查找。

与其使用:

<a.*?>xxx

使用

<a[^<>]*>xxx

原因是不使用非贪婪操作符会强制正则表达式引擎与表达式中的每个字符进行比较,而 [^<>] 只需要将当前字符与 < 和 > 进行比较,并在匹配条件满足时立即停止。在大型文件上,这可能会使应用程序冻结的时间从半秒缩短到更短。

这并不能完全解决问题,但可以帮助一些。


那第二个小技巧确实有很大帮助,回过头来看,这是一个非常明显的优化——通过粗略测试,将大约8个非贪婪的*更改为非<>,将计算时间从之前的10分钟作业缩短了约2分钟。 - WSkid
@WSkid:只有2分钟的等待时间还不算太糟糕。你尝试过编译吗?对于大文件来说,它应该是值得的。在这里它会有相当大的区别。 - Sylverdrag

2

这确实是一篇有趣的阅读,但如果模式没有固定的上限长度,它将无济于事。我能想到的唯一解决方法是在扫描一个字符串结束时保持“状态”,然后开始下一个字符串。但是,您还需要保留可能匹配的过去“页面”。 - Brian Reichle
赞同Brian的意见,它还没有完全实现最佳效果,但我认为,我会将这种技术与其他答案结合使用,以便利用这种方法寻找到最佳断点,并将其保存到ChrisWue的答案中提到的文件中。有了你的答案,Brian,一切都变得更加容易管理。 - WSkid

1
假设您加载的文档具有某种结构,最好编写解析器将文档转换为结构化形式,将大字符串分成多个块,然后对该结构进行操作。
大字符串的一个问题是超过85,000字节的对象被视为大对象,并放置在不压缩的大对象堆上,这可能导致意外的内存不足情况。
另一个选择是通过外部工具(例如sed或awk)进行管道传输。

感谢您提供的第二部分信息 - 带我找到了这个错误报告,说明LOH确实在.Net 2和3.5中存在碎片,并且可能会出现OOM错误,但在4.0中略有改善:http://connect.microsoft.com/VisualStudio/feedback/details/521147/large-object-heap-fragmentation-causes-outofmemoryexception - WSkid

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