Как узнать позицию (номер строки) средства чтения потоков в текстовом файле?

пример (это может быть не реальная жизнь, но чтобы подчеркнуть мою точку зрения):

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. Это возможно?

Конечно, я мог бы вести счет и сам, но не в этом вопрос.


person Peter    schedule 06.05.2009    source источник


Ответы (7)


Чрезвычайно легко предоставить оболочку для подсчета строк для любого 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. Не распознает альтернативные способы завершения строк. Будет несовместимо с поведением ReadLine() при чтении файлов, разделенных необработанными \r или \n.
  3. Does not override "block"-level methods like Read(char[], int, int), ReadBlock, ReadLine, ReadToEnd. TextReader implementation works correctly since it routes everything else to Read(); however, better performance could be achieved by
    • overriding those methods via routing calls to _inner. instead of base.
    • передача прочитанных символов в 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;
}
person Sinclair    schedule 16.01.2012
comment
Это также не учитывает использование Seek. - person Adam Robinson; 16.01.2012

Я наткнулся на этот пост, когда искал решение аналогичной проблемы, когда мне нужно было найти 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. Хотя я провел несколько простых тестов с использованием различных опций System.Text.Encoding, почти все данные, которые я использую, представляют собой простые текстовые файлы (ASCII).
  2. Я всегда использую только метод StreamReader.ReadLine(), и хотя краткий обзор исходного кода для StreamReader, кажется, указывает, что он все еще будет работать при использовании других методов чтения, я действительно не тестировал этот сценарий.
person Eamon    schedule 09.04.2014
comment
Работает с System.Text.Encoding.UTF8 - person CrazyIvan1974; 08.06.2017
comment
Вы должны добавить подчеркивание перед именами этих полей. Исходный код Net Core. - person Ender Look; 24.12.2019
comment
Пробовал это, но всякий раз, когда я делаю строку чтения с потоковым читателем, мне приходится сбрасывать позицию следующим образом: SetPosition (читатель, GetPosition (читатель)); есть ли лучший способ? - person John Ernest; 25.02.2020

Нет, не совсем возможно. Концепция «номера строки» основана на фактических данных, которые уже были прочитаны, а не только на позиции. Например, если вы будете искать () считыватель в произвольной позиции, он на самом деле не будет читать эти данные, поэтому он не сможет определить номер строки.

Единственный способ сделать это - следить за этим самостоятельно.

person Adam Robinson    schedule 06.05.2009

No.

Учтите, что можно искать любую позицию, используя базовый объект потока (который может находиться в любой точке любой строки). Теперь подумайте, как это повлияет на любой счетчик, хранящийся в StreamReader.

Должен ли StreamReader выяснить, на какой строке он сейчас находится? Должен ли он просто держать несколько прочитанных строк, независимо от положения в файле?

Есть больше вопросов, чем просто эти, которые сделают это кошмаром для реализации, имхо.

person Binary Worrier    schedule 06.05.2009
comment
С другой стороны, должны ли мы получать репутацию за повторение того, что уже было сказано? (НЕ говорю, что этот плакат сделал это, но в целом это, безусловно, возможно!) - person The Dag; 23.01.2012
comment
@The Dag: Не столько повторять, сколько говорить одновременно. . . Джинкс! (Кстати, ты хочешь купить даг?) - person Binary Worrier; 24.01.2012

Вот парень, который реализовал StreamReader с методом ReadLine(), который регистрирует позицию в файле.

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).

person Rolf Kristensen    schedule 06.10.2010

Я пришел сюда в поисках чего-то простого. Если вы просто используете 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();
    }
}

а затем вы делаете это обычным способом, скажем, из объекта FileInfo с именем file

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

и вы только что прочитали свойство reader.LineNumber.

person Andy Hubbard    schedule 18.02.2014
comment
Хороший ответ, но вы должны уточнить, что это будет работать, только если ReadLine является единственным методом, который вы вызываете. - person John Saunders; 19.02.2014
comment
Это решение, которое просто умоляет вызвать ошибки в будущем. Кто-то позже будет использовать метод Seek или что-то еще, и он больше не будет работать (или вы передадите его какому-то методу, который использует метод, отличный от ReadLine). Если класс может правильно использовать только ReadLine, он не должен наследовать от StreamReader. Я почти уверен, что это нарушает принцип подстановки Лисков. - person jahav; 01.02.2019
comment
@jahav Весь твой аргумент неверен. Seek() работает корректно. Все работает точно так же, за исключением того, что если вы используете только ReadLine(), вы можете увидеть LineNumber. Если код ожидает только StreamReader, то он не знает о LineNumber и все работает нормально. Если вы знаете, что у вас есть CountingReader, а не только StreamReader, то вы знаете ограничения (используйте только ReadLine), поэтому вы используете его соответствующим образом. Это предназначено для конкретного использования, и вы должны понимать и использовать свои инструменты надлежащим образом. - person Andy Hubbard; 16.03.2019
comment
@Andy Hubbard Seek будет работать правильно, но свойство LineNumber не будет работать правильно после Seek или любого другого метода перемещения StreamReader в потоке, отличном от ReadLine. В этом проблема, поэтому вы не должны наследовать от StreamReader. - person jahav; 17.03.2019
comment
@jahav В том-то и дело. Если вы знаете, что у вас есть CountingReader, вы знаете, что у вас есть доступ к LineNumber, и вы также знаете, что не можете использовать Seek() или что-то еще, кроме ReadLine(). Если вы знаете только, что у вас есть StreamReader, то вы не знаете о LineNumber, поэтому не используете его. Я вижу и понимаю вашу подразумеваемую точку зрения об использовании частного StreamReader вместо наследования, но иногда вам нужен полиморфизм, предоставляемый подклассом. - person Andy Hubbard; 23.03.2019
comment
Сценарий здесь таков: у меня есть какой-то плоский файл, и я хочу прочитать каждую строку и узнать, на какой строке я нахожусь, но методы, которые я вызываю, ожидают StreamReader, и я знаю, что эти методы используют только ReadLine(), потому что я сделал мое исследование и понимание инструментов, которые я использую. Если вы так сильно заботитесь о том, чтобы будущие разработчики не облажались, или что вы не удосужились убедиться, что используете только ReadLine(), тогда просто переопределите другие методы и создайте исключение NotImplementedException, чтобы ваш код зависал во время тестирования и вы можете решить проблему до релиза. - person Andy Hubbard; 23.03.2019
comment
Если хотите, потратьте три недели на реализацию кучи кода, который никогда не будет использоваться для достижения какой-то академической чистоты, вместо того, чтобы на самом деле выполнить свою задачу за три часа. Только не удивляйся, когда тебя уволят. - person Andy Hubbard; 23.03.2019

Замечания, уже сделанные в отношении BaseStream, действительны и важны. Однако бывают ситуации, когда вы хотите прочитать текст и знать, где в тексте вы находитесь. Все еще может быть полезно записать это как класс, чтобы упростить повторное использование.

Я попытался написать такой класс сейчас. Вроде работает правильно, но довольно медленно. Это должно быть хорошо, когда производительность не имеет решающего значения (она не настолько медленная, см. ниже).

Я использую ту же логику для отслеживания положения в тексте независимо от того, читаете ли вы символ за раз, один буфер за раз или одну строку за раз. Хотя я уверен, что это можно сделать лучше, отказавшись от этого, это значительно упростило реализацию... и, я надеюсь, следование коду.

Я сделал очень простое сравнение производительности метода ReadLine (который, как мне кажется, является самым слабым местом этой реализации) с StreamReader, и разница составляет почти порядок. Я получил 22 МБ/с, используя класс StreamReaderEx, но почти в 9 раз больше, используя StreamReader напрямую (на моем ноутбуке с SSD). Хотя это может быть интересно, я не знаю, как правильно провести тест на чтение; возможно, используя 2 одинаковых файла, каждый из которых больше, чем дисковый буфер, и читая их поочередно ..? По крайней мере, мой простой тест дает стабильные результаты, когда я запускаю его несколько раз и независимо от того, какой класс сначала читает тестовый файл.

Символ NewLine по умолчанию имеет значение Environment.NewLine, но может быть установлен на любую строку длиной 1 или 2. Читатель рассматривает только этот символ как новую строку, что может быть недостатком. По крайней мере, я знаю, что Visual Studio довольно много раз подсказывала мне, что файл, который я открываю, «имеет несогласованные новые строки».

Обратите внимание, что я не включил класс Guard; это простой служебный класс, и из контекста должно быть понятно, как его заменить. Вы даже можете удалить его, но вы потеряете проверку некоторых аргументов, и, таким образом, полученный код будет более далеким от «правильного». Например, Guard.NotNull(s, "s") просто проверяет, что s не равно null, вызывая ArgumentNullException (с именем аргумента "s", следовательно, второй параметр), если это так.

Хватит болтать, вот код:


public class StreamReaderEx : StreamReader
{
    // NewLine characters (magic value -1: "not used").
    int newLine1, newLine2;

    // The last character read was the first character of the NewLine symbol AND we are using a two-character symbol.
    bool insideNewLine;

    // StringBuilder used for ReadLine implementation.
    StringBuilder lineBuilder = new StringBuilder();


    public StreamReaderEx(string path, string newLine = "\r\n") : base(path)
    {
        init(newLine);
    }


    public StreamReaderEx(Stream s, string newLine = "\r\n") : base(s)
    {
        init(newLine);
    }


    public string NewLine
    {
        get { return "" + (char)newLine1 + (char)newLine2; }
        private set
        {
            Guard.NotNull(value, "value");
            Guard.Range(value.Length, 1, 2, "Only 1 to 2 character NewLine symbols are supported.");

            newLine1 = value[0];
            newLine2 = (value.Length == 2 ? value[1] : -1);
        }
    }


    public int LineNumber { get; private set; }
    public int LinePosition { get; private set; }


    public override int Read()
    {
        int next = base.Read();
        trackTextPosition(next);
        return next;
    }


    public override int Read(char[] buffer, int index, int count)
    {
        int n = base.Read(buffer, index, count);
        for (int i = 0; i 
person The Dag    schedule 23.01.2012
comment
О, отлично, мой код был просто обрезан посередине. Я воспользуюсь возможностью, чтобы узнать, заинтересован ли кто-нибудь; если да, дайте мне знать, и я опубликую остальные. - person The Dag; 23.01.2012