在 C# 中替换流中的字符串(不覆盖原始文件)

本文关键字:覆盖 原始 文件 字符串 替换 | 更新日期: 2023-09-27 18:31:34

我有一个文件,我正在将其打开到流中并传递给另一个方法。但是,我想在将流传递给其他方法之前替换文件中的字符串。 所以:

string path = "C:/...";
Stream s = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
//need to replace all occurrences of "John" in the file to "Jack" here.
CallMethod(s);

不应修改原始文件,只能修改流。 最简单的方法是什么?

谢谢。。。

在 C# 中替换流中的字符串(不覆盖原始文件)

如果您只是将文件中作为行读取,然后处理这些行,而不是强迫自己坚持使用Stream,这要容易得多,仅仅是因为 stream 处理文本和二进制文件,并且需要能够一次读取一个字符(这使得这种替换非常困难)。 如果您一次阅读一整行(只要您没有多行替换),这很容易。

var lines = File.ReadLines(path)
    .Select(line => line.Replace("John", "Jack"));

请注意,ReadLines仍然会流式传输数据,并且Select不需要具体化整个事情,因此在执行此操作时,您仍然不会一次将整个文件读入内存。

如果您实际上不需要流式传输数据,则可以轻松地将其全部加载为一个大字符串,执行替换,然后基于该字符串创建流:

string data = File.ReadAllText(path)
    .Replace("John", "Jack");
byte[] bytes = Encoding.ASCII.GetBytes(data);
Stream s = new MemoryStream(bytes);

如果文件有很长的行,则替换的字符串可能包含换行符,或者存在其他约束阻止在需要流式传输时使用File.ReadLines(),则存在仅使用流的替代解决方案,即使它不是微不足道的。

实现你自己的流装饰器(包装器)来执行替换。 即一个基于 Stream 的类,它在其构造函数中获取另一个流,在其Read(byte[], int, int)覆盖中从流中读取数据并在缓冲区中执行替换。有关进一步的要求和建议,请参阅 Stream 实现者的注释。

我们称被替换的字符串为"针",源流称为"大海捞针",替换字符串称为"替换"。

针和更换需要使用干草堆内容物的编码(通常Encoding.UTF8.GetBytes())进行编码。在流中,数据不会转换为字符串,这与 StreamReader.ReadLine() 不同。因此,可以防止不必要的内存分配。

简单情况:如果指针和替换都只是一个字节,则实现只是缓冲区上的简单循环,替换所有出现的情况。如果 needle 是单个字节并且替换为空(即删除字节,例如删除行尾规范化的回车符),它是一个简单的循环,保持缓冲区的fromto索引,逐字节重写缓冲区字节。

在更复杂的情况下,实现 KMP 算法来执行替换。

    将数据
  • 从基础流(干草堆)读取到至少与针一样长的内部缓冲区,并在将数据重写到输出缓冲区时执行替换。需要内部缓冲区,以便在检测到完全匹配之前不会发布来自部分匹配的数据 - 然后,返回并完全删除匹配为时已晚。

  • 逐字节处理内部缓冲区,将每个字节馈送到 KMP 自动机中。每次自动机更新时,将其释放的字节写入输出缓冲区中的适当位置。

  • 当 KMP 检测到匹配时,替换它:重置自动机,保持内部缓冲区中的位置(这将删除匹配项)并将替换写入输出缓冲区。

  • 当到达任一缓冲区的末尾时,将内部缓冲区的未写入输出和未处理部分(包括当前部分匹配)保留为下次调用该方法的起点,并返回当前输出缓冲区。对该方法的下一次调用将写入剩余的输出,并开始处理当前停止的 haystack 的其余部分。

  • 当达到 haystack 结束时,释放当前部分匹配并将其写入输出缓冲区。

只是要注意在处理 haystack 的所有数据之前不要返回空的输出缓冲区——这会向调用方发出流结束的信号,从而截断数据。

这个问题可能有很多很好的答案。我会尝试一个我用过的,一直为我和我的同龄人工作。

我建议您创建一个单独的流,例如MemoryStream.从文件流中读取并写入内存流。然后,您可以从任何一个中提取字符串并替换内容,然后您将提前传递内存流。这使得它加倍确保你不会弄乱原始流,并且你可以在需要时从中读取原始值,尽管使用这种方法基本上使用了两倍的内存。