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

StackOverflow https://stackoverflow.com/questions/829568

  •  06-07-2019
  •  | 
  •  

Вопрос

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

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

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

Это было полезно?

Решение

Создать оболочку для подсчета строк для любого 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. Не проверяет аргумент конструктора на ноль
  2. Не распознает альтернативные способы завершения линий.Будет несовместимо с поведением 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;
}

Другие советы

Я наткнулся на этот пост, когда искал решение аналогичной проблемы, когда мне нужно было найти 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", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | BindingFlags.DeclaredOnly);
    readonly static FieldInfo byteLenField = typeof(StreamReader).GetField("byteLen", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | BindingFlags.DeclaredOnly);
    readonly static FieldInfo charBufferField = typeof(StreamReader).GetField("charBuffer", System.Reflection.BindingFlags.NonPublic | System.Reflection.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 показывает, что он по-прежнему будет работать при использовании других методов чтения, я еще не проверял этот сценарий.

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

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

Нет.

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

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

Имхо, есть и другие вопросы, из-за которых реализация этого проекта станет кошмаром.

Вот парень, который реализовал 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).

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

Точки, уже сделанные в отношении 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 
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top