¿Cómo saber la posición (número de línea) de un lector de flujo en un archivo de texto?

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

  •  06-07-2019
  •  | 
  •  

Pregunta

un ejemplo (que podría no ser la vida real, pero para aclarar mi punto):

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

}

GetLinePosition aquí hay un método de extensión imaginario de streamreader. ¿Es esto posible?

Por supuesto que podría seguir contando, pero esa no es la cuestión.

¿Fue útil?

Solución

Es extremadamente fácil proporcionar un contenedor de conteo de líneas para cualquier 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++;
        }
    }
}

Inconvenientes (en aras de la brevedad):

  1. No comprueba el argumento del constructor para nulo
  2. No reconoce formas alternativas de terminar las líneas. Será inconsistente con el comportamiento de ReadLine () al leer archivos separados por raw \ r o \ n.
  3. No anula los métodos de "bloqueo" de nivel como Read (char [], int, int), ReadBlock, ReadLine, ReadToEnd. La implementación de TextReader funciona correctamente, ya que enruta todo lo demás a Read (); sin embargo, se podría lograr un mejor rendimiento al
    • anula esos métodos mediante el enrutamiento de llamadas a _inner. en lugar de base.
    • pasando los caracteres leídos a AdvancePosition. Vea la implementación de ejemplo de 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;
}

Otros consejos

Leí esta publicación mientras buscaba una solución a un problema similar en el que necesitaba buscar el StreamReader para líneas particulares. Terminé creando dos métodos de extensión para obtener y establecer la posición en un StreamReader. En realidad, no proporciona un recuento de número de línea, pero en la práctica, solo tomo la posición antes de cada ReadLine () y si la línea es de interés, entonces mantengo la posición de inicio para configurarla más tarde para volver a la línea de esa manera :

var index = streamReader.GetPosition();
var line1 = streamReader.ReadLine();

streamReader.SetPosition(index);
var line2 = streamReader.ReadLine();

Assert.AreEqual(line1, line2);

y la parte importante:

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);
    }
}

Esto funciona bastante bien para mí y dependiendo de su tolerancia para usar la reflexión. Piensa que es una solución bastante simple.

Advertencias:

  1. Si bien he realizado algunas pruebas simples utilizando varias opciones de codificación System.Text., casi todos los datos que consumo con esto son archivos de texto simples (ASCII).
  2. Solo uso el método StreamReader.ReadLine () y aunque una breve revisión de la fuente de StreamReader parece indicar que esto seguirá funcionando cuando utilizo los otros métodos de lectura, realmente no he probado ese escenario.

No, no es realmente posible. El concepto de un "número de línea" se basa en los datos reales que ya se han leído, no solo en la posición. Por ejemplo, si buscara () al lector en una posición arbitraria, no leería esos datos, por lo que no podría determinar el número de línea.

La única forma de hacerlo es hacer un seguimiento de ti mismo.

No.

Tenga en cuenta que es posible buscar cualquier posición utilizando el objeto de flujo subyacente (que podría estar en cualquier punto de cualquier línea). Ahora considere lo que eso haría con cualquier conteo mantenido por StreamReader.

¿Debería ir el StreamReader y averiguar en qué línea está ahora? ¿Debería mantener un número de líneas leídas, independientemente de la posición dentro del archivo?

Hay más preguntas que solo estas que harían de esto una pesadilla para implementar, en mi humilde opinión.

Aquí hay un tipo que implementó un StreamReader con el método ReadLine () que registra la posición del archivo.

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

Supongo que uno debería heredar de StreamReader y luego agregar el método adicional a la clase especial junto con algunas propiedades (_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;
 }

Creo que hay un error menor en el código, ya que la longitud de la cadena se usa para calcular la posición del archivo en lugar de usar los bytes reales leídos (falta soporte para archivos codificados UTF8 y UTF16).

Vine aquí buscando algo simple. Si solo está usando ReadLine () y no le importa usar Seek () ni nada, simplemente haga una subclase simple de 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();
    }
}

y luego lo haces de la manera normal, digamos desde un objeto FileInfo llamado archivo

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

y acaba de leer la propiedad reader.LineNumber .

Los puntos ya hechos con respecto al BaseStream son válidos e importantes. Sin embargo, hay situaciones en las que desea leer un texto y saber en qué parte del texto se encuentra. Todavía puede ser útil escribir eso como una clase para facilitar su reutilización.

Intenté escribir una clase así ahora. Parece funcionar correctamente, pero es bastante lento. Debería estar bien cuando el rendimiento no es crucial (no es que lento, ver más abajo).

Utilizo la misma lógica para rastrear la posición en el texto, independientemente de si lee un carácter a la vez, un búfer a la vez o una línea a la vez. Si bien estoy seguro de que se puede lograr un mejor rendimiento al abandonar esto, hizo que sea mucho más fácil de implementar ... y, espero, seguir el código.

Hice una comparación de rendimiento muy básica del método ReadLine (que creo que es el punto más débil de esta implementación) con StreamReader, y la diferencia es casi un orden de magnitud. Obtuve 22 MB / s usando mi clase StreamReaderEx, pero casi 9 veces más usando StreamReader directamente (en mi computadora portátil equipada con SSD). Si bien puede ser interesante, no sé cómo hacer una prueba de lectura adecuada; tal vez usando 2 archivos idénticos, cada uno más grande que el búfer de disco, y leyéndolos alternativamente ... Al menos mi prueba simple produce resultados consistentes cuando la ejecuto varias veces, e independientemente de qué clase lea primero el archivo de prueba.

El símbolo NewLine se establece de manera predeterminada en Environment.NewLine pero se puede establecer en cualquier cadena de longitud 1 o 2. El lector considera solo este símbolo como una nueva línea, lo que puede ser un inconveniente. Al menos sé que Visual Studio me ha provocado un buen número de veces que un archivo que abro "tiene nuevas líneas inconsistentes".

Tenga en cuenta que no he incluido la clase Guardia; Esta es una clase de utilidad simple y debe ser obvio del contexto cómo reemplazarla. Incluso puede eliminarlo, pero perdería algunas comprobaciones de argumentos y, por lo tanto, el código resultante estaría más lejos de "correcto". Por ejemplo, Guard.NotNull (s, " s ") simplemente comprueba que s no es nulo, lanzando una excepción ArgumentNullException (con el nombre del argumento " s ", de ahí el segundo parámetro) si fuera el caso.

Bastante balbuceo, aquí está el código:

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 
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top