[c#] Ottieni le ultime 10 righe di file di testo molto grandi> 10 GB



Answers

Probabilmente lo aprirò come un flusso binario, cercherò la fine, quindi eseguirò il backup cercando interruzioni di riga. Esegui il backup di 10 (o 11 a seconda dell'ultima riga) per trovare le 10 linee, quindi leggi solo fino alla fine e usa Encoding.GetString su ciò che leggi per ottenere un formato stringa. Dividere come desiderato.

Question

Qual è il modo più efficace per visualizzare le ultime 10 righe di un file di testo molto grande (questo particolare file è di oltre 10 GB). Stavo pensando di scrivere una semplice app C # ma non sono sicuro di come farlo in modo efficace.




Un metodo utile è FileInfo.Length . Dà la dimensione di un file in byte.

Qual è la struttura del tuo file? Sei sicuro che le ultime 10 linee saranno vicine alla fine del file? Se hai un file con 12 righe di testo e 10 GB di 0, guardare la fine non sarà poi così veloce. Quindi, di nuovo, potrebbe essere necessario esaminare l'intero file.

Se sei sicuro che il file contiene numerose stringhe brevi su una nuova riga, cerca fino alla fine, quindi controlla di nuovo fino a che non hai contato 11 righe. Quindi puoi leggere in avanti per le prossime 10 righe.




ecco la mia versione. HTH

using (StreamReader sr = new StreamReader(path))
{
  sr.BaseStream.Seek(0, SeekOrigin.End);

  int c;
  int count = 0;
  long pos = -1;

  while(count < 10)
  {
    sr.BaseStream.Seek(pos, SeekOrigin.End);
    c = sr.Read();
    sr.DiscardBufferedData();

    if(c == Convert.ToInt32('\n'))
      ++count;
    --pos;
  }

  sr.BaseStream.Seek(pos, SeekOrigin.End);
  string str = sr.ReadToEnd();
  string[] arr = str.Split('\n');
}



Nel caso in cui sia necessario leggere un numero qualsiasi di righe al contrario da un file di testo, ecco una classe compatibile con LINQ che è possibile utilizzare. Si concentra su prestazioni e supporto per file di grandi dimensioni. Puoi leggere diverse righe e chiamare Reverse () per ottenere le ultime righe nell'ordine in avanti:

Uso :

var reader = new ReverseTextReader(@"C:\Temp\ReverseTest.txt");
while (!reader.EndOfStream)
    Console.WriteLine(reader.ReadLine());

ReverseTextReader Class :

/// <summary>
/// Reads a text file backwards, line-by-line.
/// </summary>
/// <remarks>This class uses file seeking to read a text file of any size in reverse order.  This
/// is useful for needs such as reading a log file newest-entries first.</remarks>
public sealed class ReverseTextReader : IEnumerable<string>
{
    private const int BufferSize = 16384;   // The number of bytes read from the uderlying stream.
    private readonly Stream _stream;        // Stores the stream feeding data into this reader
    private readonly Encoding _encoding;    // Stores the encoding used to process the file
    private byte[] _leftoverBuffer;         // Stores the leftover partial line after processing a buffer
    private readonly Queue<string> _lines;  // Stores the lines parsed from the buffer

    #region Constructors

    /// <summary>
    /// Creates a reader for the specified file.
    /// </summary>
    /// <param name="filePath"></param>
    public ReverseTextReader(string filePath)
        : this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), Encoding.Default)
    { }

    /// <summary>
    /// Creates a reader using the specified stream.
    /// </summary>
    /// <param name="stream"></param>
    public ReverseTextReader(Stream stream)
        : this(stream, Encoding.Default)
    { }

    /// <summary>
    /// Creates a reader using the specified path and encoding.
    /// </summary>
    /// <param name="filePath"></param>
    /// <param name="encoding"></param>
    public ReverseTextReader(string filePath, Encoding encoding)
        : this(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), encoding)
    { }

    /// <summary>
    /// Creates a reader using the specified stream and encoding.
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="encoding"></param>
    public ReverseTextReader(Stream stream, Encoding encoding)
    {          
        _stream = stream;
        _encoding = encoding;
        _lines = new Queue<string>(128);            
        // The stream needs to support seeking for this to work
        if(!_stream.CanSeek)
            throw new InvalidOperationException("The specified stream needs to support seeking to be read backwards.");
        if (!_stream.CanRead)
            throw new InvalidOperationException("The specified stream needs to support reading to be read backwards.");
        // Set the current position to the end of the file
        _stream.Position = _stream.Length;
        _leftoverBuffer = new byte[0];
    }

    #endregion

    #region Overrides

    /// <summary>
    /// Reads the next previous line from the underlying stream.
    /// </summary>
    /// <returns></returns>
    public string ReadLine()
    {
        // Are there lines left to read? If so, return the next one
        if (_lines.Count != 0) return _lines.Dequeue();
        // Are we at the beginning of the stream? If so, we're done
        if (_stream.Position == 0) return null;

        #region Read and Process the Next Chunk

        // Remember the current position
        var currentPosition = _stream.Position;
        var newPosition = currentPosition - BufferSize;
        // Are we before the beginning of the stream?
        if (newPosition < 0) newPosition = 0;
        // Calculate the buffer size to read
        var count = (int)(currentPosition - newPosition);
        // Set the new position
        _stream.Position = newPosition;
        // Make a new buffer but append the previous leftovers
        var buffer = new byte[count + _leftoverBuffer.Length];
        // Read the next buffer
        _stream.Read(buffer, 0, count);
        // Move the position of the stream back
        _stream.Position = newPosition;
        // And copy in the leftovers from the last buffer
        if (_leftoverBuffer.Length != 0)
            Array.Copy(_leftoverBuffer, 0, buffer, count, _leftoverBuffer.Length);
        // Look for CrLf delimiters
        var end = buffer.Length - 1;
        var start = buffer.Length - 2;
        // Search backwards for a line feed
        while (start >= 0)
        {
            // Is it a line feed?
            if (buffer[start] == 10)
            {
                // Yes.  Extract a line and queue it (but exclude the \r\n)
                _lines.Enqueue(_encoding.GetString(buffer, start + 1, end - start - 2));
                // And reset the end
                end = start;
            }
            // Move to the previous character
            start--;
        }
        // What's left over is a portion of a line. Save it for later.
        _leftoverBuffer = new byte[end + 1];
        Array.Copy(buffer, 0, _leftoverBuffer, 0, end + 1);
        // Are we at the beginning of the stream?
        if (_stream.Position == 0)
            // Yes.  Add the last line.
            _lines.Enqueue(_encoding.GetString(_leftoverBuffer, 0, end - 1));

        #endregion

        // If we have something in the queue, return it
        return _lines.Count == 0 ? null : _lines.Dequeue();
    }

    #endregion

    #region IEnumerator<string> Interface

    public IEnumerator<string> GetEnumerator()
    {
        string line;
        // So long as the next line isn't null...
        while ((line = ReadLine()) != null)
            // Read and return it.
            yield return line;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }

    #endregion
}



Utilizzando la risposta di Sisutil come punto di partenza, è possibile leggere il file riga per riga e caricarli in una Queue<String> . Legge il file dall'inizio, ma ha il pregio di non provare a leggere il file all'indietro. Questo può essere davvero difficile se si ha un file con una codifica a larghezza di caratteri variabile come UTF-8 come ha sottolineato Jon Skeet. Inoltre, non formula alcuna ipotesi sulla lunghezza della linea.

Ho provato questo contro un file da 1,7 GB (non ne avevo uno da 10 GB a portata di mano) e ci sono voluti circa 14 secondi. Naturalmente, si applicano i normali avvertimenti quando si confrontano i tempi di caricamento e di lettura tra i computer.

int numberOfLines = 10;
string fullFilePath = @"C:\Your\Large\File\BigFile.txt";
var queue = new Queue<string>(numberOfLines);

using (FileStream fs = File.Open(fullFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) 
using (BufferedStream bs = new BufferedStream(fs))  // May not make much difference.
using (StreamReader sr = new StreamReader(bs)) {
    while (!sr.EndOfStream) {
        if (queue.Count == numberOfLines) {
            queue.Dequeue();
        }

        queue.Enqueue(sr.ReadLine());
    }
}

// The queue now has our set of lines. So print to console, save to another file, etc.
do {
    Console.WriteLine(queue.Dequeue());
} while (queue.Count > 0);    



Come suggerito dagli altri, puoi andare alla fine del file e leggere all'indietro, in modo efficace. Tuttavia, è leggermente complicato, soprattutto perché se si ha una codifica a lunghezza variabile (come UTF-8) è necessario essere furbi per assicurarsi di ottenere caratteri "interi".




Penso che il seguente codice risolverà il problema con sottili cambiamenti che registrano la codifica

StreamReader reader = new StreamReader(@"c:\test.txt"); //pick appropriate Encoding
reader.BaseStream.Seek(0, SeekOrigin.End);
int count = 0;
while ((count < 10) && (reader.BaseStream.Position > 0))
{
    reader.BaseStream.Position--;
    int c = reader.BaseStream.ReadByte();
    if (reader.BaseStream.Position > 0)
        reader.BaseStream.Position--;
    if (c == Convert.ToInt32('\n'))
    {
        ++count;
    }
}
string str = reader.ReadToEnd();
string[] arr = str.Replace("\r", "").Split('\n');
reader.Close();



Apri il file e inizia a leggere le righe. Dopo aver letto 10 righe, apri un altro puntatore, partendo dalla parte anteriore del file, in modo che il secondo puntatore ritardi il primo di 10 righe. Continua a leggere, spostando i due puntatori all'unisono, fino a quando il primo raggiunge la fine del file. Quindi utilizzare il secondo puntatore per leggere il risultato. Funziona con qualsiasi file di dimensioni compreso vuoto e più corto della lunghezza della coda. Ed è facile da regolare per qualsiasi lunghezza di coda. Lo svantaggio, ovviamente, è che si finisce per leggere l'intero file e questo potrebbe essere esattamente quello che stai cercando di evitare.




Non sono sicuro di quanto sarà efficiente, ma in Windows PowerShell ottenere le ultime dieci righe di un file è facile come

Get-Content file.txt | Select-Object -last 10



Links