C# - StreamReader和寻找

22

你能使用StreamReader读取普通的文本文件,然后在读取中间保存当前位置后关闭StreamReader,然后再次打开StreamReader并从该位置开始读取吗?

如果不能,那么还有什么其他方法可以实现相同的功能而不锁定文件吗?

我尝试过这个,但它不起作用:

var fs = File.Open(@ "C:\testfile.txt", FileMode.Open, FileAccess.Read);
var sr = new StreamReader(fs);

Debug.WriteLine(sr.ReadLine()); //Prints:firstline

var pos = fs.Position;

while (!sr.EndOfStream) 
{
    Debug.WriteLine(sr.ReadLine());
}

fs.Seek(pos, SeekOrigin.Begin);

Debug.WriteLine(sr.ReadLine());
//Prints Nothing, i expect it to print SecondLine.

这是我另外尝试过的代码:

var position = -1;
StreamReaderSE sr = new StreamReaderSE(@ "c:\testfile.txt");

Debug.WriteLine(sr.ReadLine());
position = sr.BytesRead;

Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());
Debug.WriteLine(sr.ReadLine());

Debug.WriteLine("Wait");

sr.BaseStream.Seek(position, SeekOrigin.Begin);
Debug.WriteLine(sr.ReadLine());
6个回答

37

我知道这有点晚了,但我刚刚发现了StreamReader中一个令人难以置信的缺陷;使用StreamReader时无法可靠地进行查找。就我个人而言,我的具体需求是需要读取字符,但如果满足某些条件则需要“倒退”;这是我解析某种文件格式时的副作用。

使用ReadLine()不是一个选项,因为它只在非常简单的解析工作中有用。我需要支持可配置的记录/行分隔符序列和转义分隔符序列。此外,我不想实现自己的缓冲区,以便支持“倒退”和转义序列;这应该是StreamReader的工作。

该方法按需计算基础字节流中的实际位置。它适用于UTF8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE和任何单字节编码(例如代码页1252、437、28591等),无论是否存在前导BOM。该版本不适用于UTF-7、Shift-JIS或其他可变字节编码。

当我需要在基础流中寻找任意位置时,我直接设置BaseStream.Position,然后调用DiscardBufferedData()以便在下一次Read()/Peek()调用时让StreamReader重新同步。

友情提示:不要随意设置BaseStream.Position。如果你分割了一个字符,你将使下一个Read()无效,并且对于UTF-16/-32,你还将使此方法的结果无效。

public static long GetActualPosition(StreamReader reader)
{
    System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetField;

    // The current buffer of decoded characters
    char[] charBuffer = (char[])reader.GetType().InvokeMember("charBuffer", flags, null, reader, null);

    // The index of the next char to be read from charBuffer
    int charPos = (int)reader.GetType().InvokeMember("charPos", flags, null, reader, null);

    // The number of decoded chars presently used in charBuffer
    int charLen = (int)reader.GetType().InvokeMember("charLen", flags, null, reader, null);

    // The current buffer of read bytes (byteBuffer.Length = 1024; this is critical).
    byte[] byteBuffer = (byte[])reader.GetType().InvokeMember("byteBuffer", flags, null, reader, null);

    // The number of bytes read while advancing reader.BaseStream.Position to (re)fill charBuffer
    int byteLen = (int)reader.GetType().InvokeMember("byteLen", flags, null, reader, null);

    // The number of bytes the remaining chars use in the original encoding.
    int numBytesLeft = reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen - charPos);

    // For variable-byte encodings, deal with partial chars at the end of the buffer
    int numFragments = 0;
    if (byteLen > 0 && !reader.CurrentEncoding.IsSingleByte)
    {
        if (reader.CurrentEncoding.CodePage == 65001) // UTF-8
        {
            byte byteCountMask = 0;
            while ((byteBuffer[byteLen - numFragments - 1] >> 6) == 2) // if the byte is "10xx xxxx", it's a continuation-byte
                byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
            if ((byteBuffer[byteLen - numFragments - 1] >> 6) == 3) // if the byte is "11xx xxxx", it starts a multi-byte char.
                byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
            // see if we found as many bytes as the leading-byte says to expect
            if (numFragments > 1 && ((byteBuffer[byteLen - numFragments] >> 7 - numFragments) == byteCountMask))
                numFragments = 0; // no partial-char in the byte-buffer to account for
        }
        else if (reader.CurrentEncoding.CodePage == 1200) // UTF-16LE
        {
            if (byteBuffer[byteLen - 1] >= 0xd8) // high-surrogate
                numFragments = 2; // account for the partial character
        }
        else if (reader.CurrentEncoding.CodePage == 1201) // UTF-16BE
        {
            if (byteBuffer[byteLen - 2] >= 0xd8) // high-surrogate
                numFragments = 2; // account for the partial character
        }
    }
    return reader.BaseStream.Position - numBytesLeft - numFragments;
}
当然,这种方法使用反射来访问私有变量,因此存在风险。但是,此方法适用于.NET 2.0、3.0、3.5、4.0、4.0.3、4.5、4.5.1、4.5.2、4.6和4.6.1。除了风险之外,另一个关键的假设是底层字节缓冲区是byte[1024];如果Microsoft以错误的方式更改它,则该方法对于UTF-16/-32会出现问题。
已针对填充有Ažテ(10个字节:0x41 C5 BE E3 83 86 F0 A3 98 BA)的UTF-8文件和填充有A(6个字节:0x41 00 01 D8 37 DC)的UTF-16文件进行了测试。重点是强制沿着byte[1024]边界分裂字符,以所有不同的方式进行。
更新(2013-07-03):我修复了该方法,原来使用来自其他答案的错误代码。该版本已针对包含需要使用代理对的字符的数据进行了测试。数据被放入3个文件中,每个文件都有不同的编码;一个是UTF-8,一个是UTF-16LE,一个是UTF-16BE。
更新(2016-02):处理被切分的字符的唯一正确方法是直接解释底层字节。UTF-8可以正确处理,UTF-16/-32也可以(根据byteBuffer的长度)。

我因此而爱你。我一整天都在尝试反转流读取器的位置,遇到了各种奇怪的问题,但这个方法只用了一次就解决了! - Dean North
我忘了:感谢Matt Houser帮助我追踪问题;他的示例数据和时间非常有帮助! - Granger
嗨@Granger,我尝试了你的代码,但在第二条指令处失败,并显示错误System.MissingFieldException已被抛出。无法找到变量charBuffer。我正在使用标准的StreamReader实例,例如var s = new StreamReader("blah.txt");,但是出现了这个错误。你知道是什么问题吗? - Yeehaw
@Yeehaw - 听起来你没有针对我测试过的任何 .Net 框架版本进行目标定位。你可能需要使用 ILSpy 来查看内部变量名称的更改。 - Granger
1
这应该被接受为答案,因为找到当前流位置的最佳方法是(当您拥有正确的字节位置时)很容易进行搜索。即使在netcore/net5-net7中也可以使用,只需进行一些小的更改。ThrowOnInvalidBytes现在被替换为将new DecoderExceptionFallback()传递给Encoding.GetEncoding(...)。并且您必须更新反射名称,它们现在在前面加上'_'。 - huancz
显示剩余8条评论

17

是的,你可以,参见以下内容:

var sr = new StreamReader("test.txt");
sr.BaseStream.Seek(2, SeekOrigin.Begin); // Check sr.BaseStream.CanSeek first
更新:请注意,您不能仅使用sr.BaseStream.Position来做任何有用的事情,因为StreamReader使用缓冲区,所以它不会反映实际读取的内容。我猜你会有问题找到真正的位置。因为你不能只计算字符(不同的编码和因此字符长度不同)。我认为最好的方法是直接使用FileStream本身。 更新: 使用这里的TGREER.myStreamReaderhttp://www.daniweb.com/software-development/csharp/threads/35078 这个类添加了BytesRead等属性(适用于ReadLine()但显然不适用于其他读取方法),然后您可以像这样操作:
File.WriteAllText("test.txt", "1234\n56789");

long position = -1;

using (var sr = new myStreamReader("test.txt"))
{
    Console.WriteLine(sr.ReadLine());

    position = sr.BytesRead;
}

Console.WriteLine("Wait");

using (var sr = new myStreamReader("test.txt"))
{
    sr.BaseStream.Seek(position, SeekOrigin.Begin);
    Console.WriteLine(sr.ReadToEnd());
}

1
你可以选择 :) 在这里查看被接受的答案:https://dev59.com/BHI-5IYBdhLWcg3w8NSB - Lasse Espeholt
我会编辑我的问题并附上代码,但还有一件事,我无法相信该类不再具有EndOfStream! - Stacker
无论如何,他指出readline会在流结束时返回null,这可以代替endofstream。 - Stacker
我编辑了实际类以始终允许读写共享。 - Stacker
2
@marisks:daniweb上提供的解决方案无法处理多字节UTF8字符,因为它计算读取的字符数量来更新位置。 - Zonko
显示剩余8条评论

2
如果您只想在文本流中搜索起始位置,我添加了此扩展到StreamReader,以便确定流的编辑应该发生的位置。当然,这基于字符作为逻辑的增量方面,但对于我的目的来说,它非常有效,可用于根据字符串模式获取文本/ASCII文件中的位置。然后,您可以使用该位置作为读取的起点,编写一个新文件,其中不包括起点之前的数据。
返回的流位置可以提供给Seek以从文本流读取中的该位置开始。它有效。我已经测试过了。但是,在匹配算法期间匹配非ASCII Unicode字符时可能会出现问题。这是基于美式英语和相关字符页面的。
基本原理:通过逐个字符扫描文本流,查找顺序字符串模式(与字符串参数匹配),仅向前通过流。一旦模式与字符串参数不匹配(即向前逐个字符),则它将重新启动(从当前位置)尝试逐个字符地获取匹配项。如果无法在流中找到匹配项,则最终会退出。如果找到匹配项,则返回流中当前的“字符”位置,而不是StreamReader.BaseStream.Position,因为该位置是基于StreamReader进行的缓冲。
正如评论中所示,此方法将影响StreamReader的位置,并且将在方法结束时将其设置回开头(0)。应使用StreamReader.BaseStream.Seek来运行到此扩展返回的位置。
注意:此扩展返回的位置也适用于使用文本文件时BinaryReader.Seek的起始位置。我实际上使用此逻辑来完成此操作,以便在丢弃PJL头信息以使文件成为可被GhostScript消费的“正确”PostScript可读文件后重写PostScript文件回磁盘。 :)
在PostScript(PJL标题之后)中搜索的字符串是:“%!PS-”,后跟“Adobe”和版本。
public static class StreamReaderExtension
{
    /// <summary>
    /// Searches from the beginning of the stream for the indicated
    /// <paramref name="pattern"/>. Once found, returns the position within the stream
    /// that the pattern begins at.
    /// </summary>
    /// <param name="pattern">The <c>string</c> pattern to search for in the stream.</param>
    /// <returns>If <paramref name="pattern"/> is found in the stream, then the start position
    /// within the stream of the pattern; otherwise, -1.</returns>
    /// <remarks>Please note: this method will change the current stream position of this instance of
    /// <see cref="System.IO.StreamReader"/>. When it completes, the position of the reader will
    /// be set to 0.</remarks>
    public static long FindSeekPosition(this StreamReader reader, string pattern)
    {
        if (!string.IsNullOrEmpty(pattern) && reader.BaseStream.CanSeek)
        {
            try
            {
                reader.BaseStream.Position = 0;
                reader.DiscardBufferedData();
                StringBuilder buff = new StringBuilder();
                long start = 0;
                long charCount = 0;
                List<char> matches = new List<char>(pattern.ToCharArray());
                bool startFound = false;

                while (!reader.EndOfStream)
                {
                    char chr = (char)reader.Read();

                    if (chr == matches[0] && !startFound)
                    {
                        startFound = true;
                        start = charCount;
                    }

                    if (startFound && matches.Contains(chr))
                    {
                        buff.Append(chr);

                        if (buff.Length == pattern.Length
                            && buff.ToString() == pattern)
                        {
                            return start;
                        }

                        bool reset = false;

                        if (buff.Length > pattern.Length)
                        {
                            reset = true;
                        }
                        else
                        {
                            string subStr = pattern.Substring(0, buff.Length);

                            if (buff.ToString() != subStr)
                            {
                                reset = true;
                            }
                        }

                        if (reset)
                        {
                            buff.Length = 0;
                            startFound = false;
                            start = 0;
                        }
                    }

                    charCount++;
                }
            }
            finally
            {
                reader.BaseStream.Position = 0;
                reader.DiscardBufferedData();
            }
        }

        return -1;
    }
}

0

FileStream.Position(或等效地,StreamReader.BaseStream.Position)通常会超前 - 可能远远超前于TextReader位置,因为底层缓冲正在进行。

如果您可以确定文本文件中如何处理换行符,则可以根据行长度和行尾字符累加读取的字节数。

File.WriteAllText("test.txt", "1234" + System.Environment.NewLine + "56789");

long position = -1;
long bytesRead = 0;
int newLineBytes = System.Environment.NewLine.Length;

using (var sr = new StreamReader("test.txt"))
{
    string line = sr.ReadLine();
    bytesRead += line.Length + newLineBytes;

    Console.WriteLine(line);

    position = bytesRead;
}

Console.WriteLine("Wait");

using (var sr = new StreamReader("test.txt"))
{
    sr.BaseStream.Seek(position, SeekOrigin.Begin);
    Console.WriteLine(sr.ReadToEnd());
}

对于更复杂的文本文件编码,您可能需要比这更高级的技巧,但对我而言它有效。


有没有特别的原因要用-1来初始化位置? - SKull
1
String.Length方法返回的是字符数而不是字节数。因此,任何多字节字符都不会被计算在内,所以这段代码在最好的情况下高度特定,在最坏的情况下是危险的。请参阅MSDN文档了解该方法的详细信息。 - Moog

0

来自MSDN:

StreamReader专为特定编码的字符输入而设计,而Stream类则专为字节输入和输出而设计。使用StreamReader从标准文本文件中读取信息的行。

在大多数涉及StreamReader的示例中,您将看到使用ReadLine()逐行读取。Seek方法来自基本上用于以字节形式读取或处理数据的Stream类。


3
因为 OP 在使用 StreamReader 时谈到了寻找,所以被标记为降级。这个答案没有解决寻找的问题,而是重复了MSDN的定义,这并不有用。 - enorl76

0

我发现上面的建议对我不起作用 - 我的用例只是需要简单地备份一个读取位置(我正在使用默认编码一次读取一个字符)。我的简单解决方案受到了上面评论的启发...您的结果可能会有所不同...

在读取之前,我保存了BaseStream.Position,然后确定是否需要备份...如果是,则设置位置并调用DiscardBufferedData()。


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