【问题标题】:How to know position(linenumber) of a streamreader in a textfile?如何知道流阅读器在文本文件中的位置(行号)?
【发布时间】:2009-05-06 13:31:45
【问题描述】:

一个例子(可能不是现实生活,但要说明我的观点):

public void StreamInfo(StreamReader p)
{
    string info = string.Format(
        "The supplied streamreaer read : {0}\n at line {1}",
        p.ReadLine(),
        p.GetLinePosition()-1);               

}

GetLinePosition这里是streamreader的一种假想扩展方法。 这可能吗?

我当然可以数数自己,但这不是问题。

【问题讨论】:

    标签: c# streamreader


    【解决方案1】:

    我在寻找类似问题的解决方案时遇到了这篇文章,我需要在特定行中寻找 StreamReader。我最终创建了两个扩展方法来获取和设置 StreamReader 上的位置。它实际上并没有提供行号计数,但在实践中,我只是在每个 ReadLine() 之前获取位置,如果该行感兴趣,那么我保留起始位置以便稍后设置以像这样回到该行:

    var index = streamReader.GetPosition();
    var line1 = streamReader.ReadLine();
    
    streamReader.SetPosition(index);
    var line2 = streamReader.ReadLine();
    
    Assert.AreEqual(line1, line2);
    

    以及重要的部分:

    public static class StreamReaderExtensions
    {
        readonly static FieldInfo charPosField = typeof(StreamReader).GetField("charPos", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        readonly static FieldInfo byteLenField = typeof(StreamReader).GetField("byteLen", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        readonly static FieldInfo charBufferField = typeof(StreamReader).GetField("charBuffer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
    
        public static long GetPosition(this StreamReader reader)
        {
            // shift position back from BaseStream.Position by the number of bytes read
            // into internal buffer.
            int byteLen = (int)byteLenField.GetValue(reader);
            var position = reader.BaseStream.Position - byteLen;
    
            // if we have consumed chars from the buffer we need to calculate how many
            // bytes they represent in the current encoding and add that to the position.
            int charPos = (int)charPosField.GetValue(reader);
            if (charPos > 0)
            {
                var charBuffer = (char[])charBufferField.GetValue(reader);
                var encoding = reader.CurrentEncoding;
                var bytesConsumed = encoding.GetBytes(charBuffer, 0, charPos).Length;
                position += bytesConsumed;
            }
    
            return position;
        }
    
        public static void SetPosition(this StreamReader reader, long position)
        {
            reader.DiscardBufferedData();
            reader.BaseStream.Seek(position, SeekOrigin.Begin);
        }
    }
    

    这对我来说效果很好,取决于你对使用反射的容忍度它认为这是一个相当简单的解决方案。

    注意事项:

    1. 虽然我使用各种 Systems.Text.Encoding 选项进行了一些简单的测试,但我使用的几乎所有数据都是简单的文本文件(ASCII)
    2. 我只使用过StreamReader.ReadLine() 方法,虽然对 StreamReader 源的简要回顾似乎表明在使用其他读取方法时这仍然有效,但我还没有真正测试过这种情况。

    【讨论】:

    • System.Text.Encoding.UTF8一起工作
    • 您应该在该字段名称之前添加一个下划线。 Net Core source code.
    • 试过了,但是每当我使用流式阅读器进行读取时,我必须像这样重置位置:SetPosition(reader, GetPosition(reader));,有没有更好的方法?
    • 请注意,可能有一些字节已经从字节缓冲区读取但尚未转换为字符缓冲区。例如4 字节 UTF8 字符的前 2 个字节位于字节缓冲区的末尾。这些字节缓存在 StreamReader 的“编码”成员变量中的解码器对象中。
    • stackoverflow.com/a/17457085/4250616 有一个解决方案来处理那些吐出的字符。
    【解决方案2】:

    不,不可能。 “行号”的概念是基于已经读取的实际数据,而不仅仅是位置。例如,如果您要将读取器 Seek() 到任意位置,它实际上不会读取该数据,因此它无法确定行号。

    做到这一点的唯一方法是自己跟踪它。

    【讨论】:

      【解决方案3】:

      为任何 TextReader 提供行计数包装器非常容易:

      public class PositioningReader : TextReader {
          private TextReader _inner;
          public PositioningReader(TextReader inner) {
              _inner = inner;
          }
          public override void Close() {
              _inner.Close();
          }
          public override int Peek() {
              return _inner.Peek();
          }
          public override int Read() {
              var c = _inner.Read();
              if (c >= 0)
                  AdvancePosition((Char)c);
              return c;
          }
      
          private int _linePos = 0;
          public int LinePos { get { return _linePos; } }
      
          private int _charPos = 0;
          public int CharPos { get { return _charPos; } }
      
          private int _matched = 0;
          private void AdvancePosition(Char c) {
              if (Environment.NewLine[_matched] == c) {
                  _matched++;
                  if (_matched == Environment.NewLine.Length) {
                      _linePos++;
                      _charPos = 0;
                      _matched = 0;
                  }
              }
              else {
                  _matched = 0;
                  _charPos++;
              }
          }
      }
      

      缺点(为简洁起见):

      1. 不检查构造函数参数是否为 null
      2. 无法识别终止行的替代方法。读取由原始 \r 或 \n 分隔的文件时,将与 ReadLine() 行为不一致。
      3. 不覆盖“块”级方法,如 Read(char[], int, int)、ReadBlock、ReadLine、ReadToEnd。 TextReader 实现工作正常,因为它将其他所有内容路由到 Read();但是,可以通过以下方式获得更好的性能
        • 通过将调用路由到 _inner 来覆盖这些方法。而不是基础。
        • 将读取的字符传递给 AdvancePosition。查看示例 ReadBlock 实现:

      public override int ReadBlock(char[] buffer, int index, int count) {
          var readCount = _inner.ReadBlock(buffer, index, count);    
          for (int i = 0; i < readCount; i++)
              AdvancePosition(buffer[index + i]);
          return readCount;
      }
      

      【讨论】:

      • 这也不考虑使用Seek
      【解决方案4】:

      没有。

      考虑使用底层流对象(可以在任何行的任何点)寻找任何位置是可能的。 现在考虑一下这会对 StreamReader 保存的任何计数产生什么影响。

      StreamReader 是否应该找出它现在在哪一行? 无论文件中的位置如何,它是否应该只读取多行?

      除了这些问题之外,还有更多的问题会使这成为实施的噩梦,恕我直言。

      【讨论】:

      • 另一方面,我们是否应该因为重复已经说过的内容而获得代表? (不是说这张海报有,但总的来说这是可能的!)
      • @The Dag:没有那么多重复,就像同时说的那样。 . .金克斯! (顺便说一句,你想买一个 dag 吗?)
      【解决方案5】:

      这是一个使用 ReadLine() 方法实现 StreamReader 的人,该方法注册文件位置。

      http://www.daniweb.com/forums/thread35078.html

      我猜应该继承自StreamReader,然后将额外的方法和一些属性(_lineLength + _bytesRead)一起添加到特殊类中:

       // Reads a line. A line is defined as a sequence of characters followed by
       // a carriage return ('\r'), a line feed ('\n'), or a carriage return
       // immediately followed by a line feed. The resulting string does not
       // contain the terminating carriage return and/or line feed. The returned
       // value is null if the end of the input stream has been reached.
       //
       /// <include file='doc\myStreamReader.uex' path='docs/doc[@for="myStreamReader.ReadLine"]/*' />
       public override String ReadLine()
       {
                _lineLength = 0;
                //if (stream == null)
                //       __Error.ReaderClosed();
                if (charPos == charLen)
                {
                         if (ReadBuffer() == 0) return null;
                }
                StringBuilder sb = null;
                do
                {
                         int i = charPos;
                         do
                         {
                                 char ch = charBuffer[i];
                                 int EolChars = 0;
                                 if (ch == '\r' || ch == '\n')
                                 {
                                          EolChars = 1;
                                          String s;
                                          if (sb != null)
                                          {
                                                   sb.Append(charBuffer, charPos, i - charPos);
                                                   s = sb.ToString();
                                          }
                                          else
                                          {
                                                   s = new String(charBuffer, charPos, i - charPos);
                                          }
                                          charPos = i + 1;
                                          if (ch == '\r' && (charPos < charLen || ReadBuffer() > 0))
                                          {
                                                   if (charBuffer[charPos] == '\n')
                                                   {
                                                            charPos++;
                                                            EolChars = 2;
                                                   }
                                          }
                                          _lineLength = s.Length + EolChars;
                                          _bytesRead = _bytesRead + _lineLength;
                                          return s;
                                 }
                                 i++;
                         } while (i < charLen);
                         i = charLen - charPos;
                         if (sb == null) sb = new StringBuilder(i + 80);
                         sb.Append(charBuffer, charPos, i);
                } while (ReadBuffer() > 0);
                string ss = sb.ToString();
                _lineLength = ss.Length;
                _bytesRead = _bytesRead + _lineLength;
                return ss;
       }
      

      认为代码中存在一个小错误,因为字符串的长度用于计算文件位置,而不是使用读取的实际字节数(缺乏对 UTF8 和 UTF16 编码文件的支持)。

      【讨论】:

        【解决方案6】:

        我来这里是为了寻找简单的东西。如果你只是使用 ReadLine() 而不关心使用 Seek() 或任何东西,只需创建一个 StreamReader 的简单子类

        class CountingReader : StreamReader {
            private int _lineNumber = 0;
            public int LineNumber { get { return _lineNumber; } }
        
            public CountingReader(Stream stream) : base(stream) { }
        
            public override string ReadLine() {
                _lineNumber++;
                return base.ReadLine();
            }
        }
        

        然后你用正常的方式,比如从一个名为 file 的 FileInfo 对象

        CountingReader reader = new CountingReader(file.OpenRead())
        

        而您刚刚阅读了reader.LineNumber 属性。

        【讨论】:

        • 很好的答案,但您应该澄清,这只有在 ReadLine 是您调用的 only 方法时才有效。
        • 这是一个解决方案,只是乞求导致错误。稍后有人会使用 Seek 方法或其他方法,但它将不再起作用(或者您将其传递给使用 ReadLine 以外的方法的某个方法)。如果该类只能正确使用 ReadLine,则不应从 StreamReader 继承。我很确定这违反了 Liskov 替换原则。
        • @jahav 你的整个论点都是错误的。 Seek() 工作正常。一切功能完全相同,除非您只使用 ReadLine() 您可以看到 LineNumber。如果代码只需要一个 StreamReader,那么它不知道 LineNumber 并且一切正常。如果您知道您有一个 CountingReader,而不仅仅是一个 StreamReader,那么您就知道限制(仅使用 ReadLine),因此您可以相应地使用它。这是针对特定用途的,您应该正确理解和使用您的工具。
        • @Andy Hubbard Seek 将正常工作,但 LineNumber 属性在 Seek 或 StreamReader 的任何其他方法在 ReadLine 之外的流中移动后将无法正常工作。这就是问题所在,因此您不应该从 StreamReader 继承。
        • @jahav 这就是重点。如果你知道你有一个 CountingReader,你就知道你可以访问 LineNumber,并且你也知道你不能使用 Seek() 或 ReadLine() 之外的任何东西。如果你只知道你有一个 StreamReader,那么你不知道 LineNumber 所以你不使用它。我看到并理解您关于使用私有 StreamReader 而不是继承的暗示点,但有时您需要通过子类化授予的多态性。
        【解决方案7】:

        关于 BaseStream 的观点是有效且重要的。但是,在某些情况下,您想阅读文本并知道您在文本中的位置。将它写成一个类以使其易于重用仍然很有用。

        我现在试着写这样一个类。它似乎工作正常,但速度相当慢。当性能不重要时应该没问题(它不是那么慢,见下文)。

        无论您是一次读取一个字符、一次读取一个缓冲区还是一次读取一行,我都使用相同的逻辑来跟踪文本中的位置。虽然我确信通过放弃它可以使其执行得更好,但它使实现变得更加容易......并且我希望遵循代码。

        我对ReadLine方法(我相信这是这个实现的最薄弱点)和StreamReader做了一个非常基础的性能比较,差别几乎是一个数量级。我使用我的 StreamReaderEx 类获得了 22 MB/s,但直接使用 StreamReader 的速度几乎是 9 倍(在我配备 SSD 的笔记本电脑上)。虽然这可能很有趣,但我不知道如何进行正确的阅读测试;也许使用 2 个相同的文件,每个文件都大于磁盘缓冲区,并交替读取它们..?至少我的简单测试在我多次运行时会产生一致的结果,无论哪个类首先读取测试文件。

        NewLine 符号默认为 Environment.NewLine 但可以设置为长度为 1 或 2 的任何字符串。读者仅将此符号视为换行符,这可能是一个缺点。至少我知道 Visual Studio 已经多次提示我打开的文件“有不一致的换行符”。

        请注意,我没有包含 Guard 类;这是一个简单的实用程序类,从上下文中应该知道如何替换它。您甚至可以删除它,但您会丢失一些参数检查,因此生成的代码将远离“正确”。例如,Guard.NotNull(s, "s") 只是检查 s 是否为空,如果是这种情况,则抛出 ArgumentNullException(参数名称为“s”,因此是第二个参数)。

        废话不多说,代码如下:

        公共类 StreamReaderEx : StreamReader { // 换行符(魔法值 -1:“未使用”)。 诠释新线1,新线2; // 读取的最后一个字符是 NewLine 符号的第一个字符,并且我们使用的是两个字符的符号。 bool insideNewLine; // StringBuilder 用于 ReadLine 实现。 StringBuilder lineBuilder = new StringBuilder(); public StreamReaderEx(string path, string newLine = "\r\n") : base(path) { 初始化(新线); } public StreamReaderEx(Stream s, string newLine = "\r\n") : base(s) { 初始化(新线); } 公共字符串换行符 { 得到 { 返回 "" + (char)newLine1 + (char)newLine2; } 私人集 { Guard.NotNull(value, "value"); Guard.Range(value.Length, 1, 2, "仅支持 1 到 2 个字符的换行符。"); newLine1 = 值[0]; newLine2 = (value.Length == 2 ? value[1] : -1); } } 公共 int LineNumber { 获取;私人套装; } 公共 int LinePosition { 获取;私人套装; } 公共覆盖 int Read() { int next = base.Read(); trackTextPosition(下一个); 下一个返回; } public override int Read(char[] buffer, int index, int count) { int n = base.Read(缓冲区,索引,计数); 对于 (int i = 0; 我

        【讨论】:

        • 哦,太好了,我的代码在中间被截断了。我会借此机会看看是否有人感兴趣;如果是这样,请告诉我,我会发布其余部分。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-11-11
        • 2018-06-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多