追踪StreamReader的行位置

21

我需要跟踪从流读取器中读取的行的位置。当我使用reader.ReadLine()时,我需要知道该行在文件中的位置,并且我还想能够从我之前跟踪的位置读取文件。

这是否可能?


你需要这个做什么,文件有多大?... - H H
4个回答

30
你可以通过以下三种方式之一来实现:
1)编写自己的StreamReader。这是一个很好的开始:如何在文本文件中知道StreamReader的位置(行号)? 2)StreamReader类有两个非常重要但私有的变量charPos和charLen,需要定位实际的“读取”位置而不仅仅是流的基础位置。你可以使用反射来获取所建议的值这里
Int32 charpos = (Int32) s.GetType().InvokeMember("charPos", 
BindingFlags.DeclaredOnly | 
BindingFlags.Public | BindingFlags.NonPublic | 
BindingFlags.Instance | BindingFlags.GetField
 ,null, s, null); 

Int32 charlen= (Int32) s.GetType().InvokeMember("charLen", 
BindingFlags.DeclaredOnly | 
BindingFlags.Public | BindingFlags.NonPublic | 
BindingFlags.Instance | BindingFlags.GetField
 ,null, s, null);

return (Int32)s.BaseStream.Position-charlen+charpos;
3)将整个文件读入字符串数组中。代码示例如下:
char[] CRLF = new char[2] { '\n', '\r' };
TextReader tr = File.OpenText("some path to file");
string[] fileLines = tr.ReadToEnd().Split(CRLF);
另一个可能性(与第3种方法类似)是读入行并将行存储在数组中。当您想要读取前一行时,只需使用该数组即可。

另一个可能性(与第3种方法类似)是读取每行并将其存储到数组中。当需要读取上一行时,只需使用该数组即可。


2
你确定当1024*N边界将扩展字符切成两部分时,选项2)是安全的吗? - H H
3
我选择了选项#2,但代码是错误的。我目前使用的代码在这里:https://dev59.com/PG035IYBdhLWcg3weP3f#17457085 - Granger
@HenkHolterman - 只要您将Position设置为有效位置并在更改后使用DiscardBufferedData(),就是安全的。只要您根据StreamReader实际读取的流中的“实际”位置设置Position,您就会处于有效的字符边界。 - Granger
2
当使用不会为字符到字节创建1对1编码的编码时,选项2将无法正常工作。请在此处查看我的解决方案:https://dev59.com/ZnRA5IYBdhLWcg3wyBD1#22975649(编辑:我的解决方案似乎与Granger的非常相似) - Eamon
如果您只想显示一个进度条来显示处理大型文本文件或资源时的大致位置,那么这已经足够了: int iProgress = (int)((sr.BaseStream.Position * 100L) / sr.BaseStream.Length); BaseStream.Position将略微超前于流读取器返回的实际行位置,但如果缓冲区大小与文件大小相比较小,则差异可以忽略不计。如果文件大小较小,则进度将很快到达100%,但处理速度也会很快,因此大多数用户不会在意。 - Berend Engelbrecht
显示剩余4条评论

9

跟踪 StreamReader 的实际位置(以字节为单位):

readonly static FieldInfo charPosField = typeof(StreamReader).GetField("charPos", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo charLenField = typeof(StreamReader).GetField("charLen", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo charBufferField = typeof(StreamReader).GetField("charBuffer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);

static long ActualPosition(StreamReader reader)
{
    var charBuffer = (char[])charBufferField.GetValue(reader);
    var charLen = (int)charLenField.GetValue(reader);
    var charPos = (int)charPosField.GetValue(reader);

    return reader.BaseStream.Position - reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen-charPos);
}

欢迎您。


2
对于.NET Core,所有字段名称都有下划线前缀,请参见https://source.dot.net/#System.Private.CoreLib/StreamReader.cs,b5fe1efcec14de32 - ygoe

3
也许这可以帮助你。
  public class StreamLineReader : IDisposable
    {
        const int BufferLength = 1024;

        Stream _Base;
        int _Read = 0, _Index = 0;
        byte[] _Bff = new byte[BufferLength];

        long _CurrentPosition = 0;
        int _CurrentLine = 0;

        /// <summary>
        /// CurrentLine number
        /// </summary>
        public long CurrentPosition { get { return _CurrentPosition; } }
        /// <summary>
        /// CurrentLine number
        /// </summary>
        public int CurrentLine { get { return _CurrentLine; } }
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="stream">Stream</param>
        public StreamLineReader(Stream stream) { _Base = stream; }
        /// <summary>
        /// Count lines and goto line number
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <returns>Return true if goTo sucessfully</returns>
        public bool GoToLine(int goToLine) { return IGetCount(goToLine, true) == goToLine; }
        /// <summary>
        /// Count lines and goto line number
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <returns>Return the Count of lines</returns>
        public int GetCount(int goToLine) { return IGetCount(goToLine, false); }
        /// <summary>
        /// Internal method for goto&Count
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <param name="stopWhenLine">Stop when found the selected line number</param>
        /// <returns>Return the Count of lines</returns>
        int IGetCount(int goToLine, bool stopWhenLine)
        {
            _Base.Seek(0, SeekOrigin.Begin);
            _CurrentPosition = 0;
            _CurrentLine = 0;
            _Index = 0;
            _Read = 0;

            long savePosition = _Base.Length;

            do
            {
                if (_CurrentLine == goToLine)
                {
                    savePosition = _CurrentPosition;
                    if (stopWhenLine) return _CurrentLine;
                }
            }
            while (ReadLine() != null);

            // GoToPosition

            int count = _CurrentLine;

            _CurrentLine = goToLine;
            _Base.Seek(savePosition, SeekOrigin.Begin);

            return count;
        }
        /// <summary>
        /// Read Line
        /// </summary>
        /// <returns></returns>
        public string ReadLine()
        {
            bool found = false;

            StringBuilder sb = new StringBuilder();
            while (!found)
            {
                if (_Read <= 0)
                {
                    // Read next block
                    _Index = 0;
                    _Read = _Base.Read(_Bff, 0, BufferLength);
                    if (_Read == 0)
                    {
                        if (sb.Length > 0) break;
                        return null;
                    }
                }

                for (int max = _Index + _Read; _Index < max; )
                {
                    char ch = (char)_Bff[_Index];
                    _Read--; _Index++;
                    _CurrentPosition++;

                    if (ch == '\0' || ch == '\n')
                    {
                        found = true;
                        break;
                    }
                    else if (ch == '\r') continue;
                    else sb.Append(ch);
                }
            }

            _CurrentLine++;
            return sb.ToString();
        }
        /// <summary>
        /// Free resources
        /// </summary>
        public void Dispose()
        {
            if (_Base != null)
            {
                _Base.Close();
                _Base.Dispose();
                _Base = null;
            }
        }
    }

使用:

 using (StreamLineReader st = new StreamLineReader(File.OpenRead("E:\\log.txt")))
        {
            bool ok = st.GoToLine(1);
            int count= st.GetCount(0);

            string w0 = st.ReadLine();
            string w1 = st.ReadLine();
            string w2 = st.ReadLine();
            string w3 = st.ReadLine();
        }

1
另一种可用的模式是在要更改位置时使用新的StreamReader。这适用于任何版本的.net,并且不需要太多代码。
using var stream = File.Open("file.txt");

using (var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: true) {
    reader.ReadLine();
}

stream.Seek(0, SeekLocation.Begin);

using (var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: true) {
    // read the same line again
    reader.ReadLine();
}

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