web-dev-qa-db-fra.com

Obtenez les 10 dernières lignes d'un très gros fichier texte> 10 Go

Quelle est la manière la plus efficace d'afficher les 10 dernières lignes d'un très gros fichier texte (ce fichier particulier fait plus de 10 Go). Je pensais simplement écrire une simple application C # mais je ne sais pas comment le faire efficacement.

62
Chris Conway

Lisez jusqu'à la fin du fichier, puis effectuez une recherche en arrière jusqu'à ce que vous trouviez dix sauts de ligne, puis continuez jusqu'à la fin en tenant compte des différents encodages. Assurez-vous de gérer les cas où le nombre de lignes dans le fichier est inférieur à dix. Vous trouverez ci-dessous une implémentation (en C # comme vous l'avez tagué), généralisée pour trouver le dernier numberOfTokens dans le fichier situé à path codé dans encoding où le séparateur de jeton est représenté par tokenSeparator; le résultat est renvoyé sous la forme d'un string (cela pourrait être amélioré en renvoyant un IEnumerable<string> qui énumère les jetons).

public static string ReadEndTokens(string path, Int64 numberOfTokens, Encoding encoding, string tokenSeparator) {

    int sizeOfChar = encoding.GetByteCount("\n");
    byte[] buffer = encoding.GetBytes(tokenSeparator);


    using (FileStream fs = new FileStream(path, FileMode.Open)) {
        Int64 tokenCount = 0;
        Int64 endPosition = fs.Length / sizeOfChar;

        for (Int64 position = sizeOfChar; position < endPosition; position += sizeOfChar) {
            fs.Seek(-position, SeekOrigin.End);
            fs.Read(buffer, 0, buffer.Length);

            if (encoding.GetString(buffer) == tokenSeparator) {
                tokenCount++;
                if (tokenCount == numberOfTokens) {
                    byte[] returnBuffer = new byte[fs.Length - fs.Position];
                    fs.Read(returnBuffer, 0, returnBuffer.Length);
                    return encoding.GetString(returnBuffer);
                }
            }
        }

        // handle case where number of tokens in file is less than numberOfTokens
        fs.Seek(0, SeekOrigin.Begin);
        buffer = new byte[fs.Length];
        fs.Read(buffer, 0, buffer.Length);
        return encoding.GetString(buffer);
    }
}
74
jason

Je l'ouvrirais probablement en tant que flux binaire, chercherais jusqu'à la fin, puis sauvegarderais à la recherche de sauts de ligne. Sauvegardez 10 (ou 11 selon cette dernière ligne) pour trouver vos 10 lignes, puis lisez jusqu'à la fin et utilisez Encoding.GetString sur ce que vous lisez pour le mettre sous forme de chaîne. Divisez comme vous le souhaitez.

22
ctacke

Queue? Tail est une commande unix qui affichera les dernières lignes d'un fichier. Il existe une version Windows dans le Kit de ressources Windows 2003 Server .

17
w4g3n3r

Comme les autres l'ont suggéré, vous pouvez aller à la fin du fichier et lire en arrière, efficacement. Cependant, c'est un peu délicat - en particulier parce que si vous avez un encodage de longueur variable (comme UTF-8), vous devez être rusé pour vous assurer d'obtenir des caractères "entiers".

17
Jon Skeet

Vous devriez pouvoir utiliser FileStream.Seek () pour vous déplacer à la fin du fichier, puis revenir en arrière, en recherchant\n jusqu'à ce que vous ayez suffisamment de lignes.

6
Lolindrath

Je ne suis pas sûr de son efficacité, mais dans Windows PowerShell, obtenir les dix dernières lignes d'un fichier est aussi simple que

Get-Content file.txt | Select-Object -last 10
6
Eric Ness

C'est ce que fait la commande unix tail. Voir http://en.wikipedia.org/wiki/Tail_ (Unix)

Il existe de nombreuses implémentations open source sur Internet et en voici une pour win32: Tail pour WIn32

4
zendar

Je pense que le code suivant résoudra le problème avec des changements subtils de reclassement du codage

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();
4
Ahmed Said

Vous pouvez utiliser la version Windows de la commande tail et saisir simplement sa sortie dans un fichier texte avec le symbole> ou l'afficher à l'écran en fonction de vos besoins.

2
Jared

voici ma version. 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');
}
2
plan9assembler

Si vous ouvrez le fichier avec FileMode.Append, il cherchera la fin du fichier pour vous. Ensuite, vous pouvez rechercher le nombre d'octets que vous souhaitez et les lire. Cela peut ne pas être rapide, peu importe ce que vous faites, car c'est un fichier assez volumineux.

1
Steven Behnke

Une méthode utile est FileInfo.Length. Il donne la taille d'un fichier en octets.

Quelle structure est votre dossier? Êtes-vous sûr que les 10 dernières lignes seront proches de la fin du fichier? Si vous avez un fichier avec 12 lignes de texte et 10 Go de 0, regarder la fin ne sera pas vraiment aussi rapide. Là encore, vous devrez peut-être parcourir tout le fichier.

Si vous êtes sûr que le fichier contient de nombreuses chaînes courtes chacune sur une nouvelle ligne, recherchez jusqu'à la fin, puis revenez jusqu'à ce que vous ayez compté 11 fin de lignes. Ensuite, vous pouvez lire en avant pour les 10 lignes suivantes.

1
biozinc

Je pense que les autres affiches ont toutes montré qu'il n'y a pas de véritable raccourci.

Vous pouvez soit utiliser un outil tel que tail (ou powershell), soit écrire du code stupide qui recherche la fin du fichier et recherche ensuite n nouvelles lignes.

Il existe de nombreuses implémentations de tail sur le web - jetez un œil au code source pour voir comment ils le font. Tail est assez efficace (même sur des fichiers très très volumineux) et ils doivent donc avoir bien compris quand ils l'ont écrit!

1
Fortyrunner

En utilisant la réponse de Sisutil comme point de départ, vous pouvez lire le fichier ligne par ligne et les charger dans un Queue<String>. Il lit le fichier depuis le début, mais il a le mérite de ne pas essayer de lire le fichier en arrière. Cela peut être très difficile si vous avez un fichier avec un codage à largeur de caractère variable comme UTF-8 comme l'a souligné Jon Skeet. Il ne fait aucune hypothèse sur la longueur de ligne.

J'ai testé cela contre un fichier de 1,7 Go (je n'en avais pas un de 10 Go à portée de main) et cela a pris environ 14 secondes. Bien sûr, les mises en garde habituelles s'appliquent lors de la comparaison des temps de chargement et de lecture entre les ordinateurs.

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);    
0
Henry Daehnke

À l'aide de PowerShell, Get-Content big_file_name.txt -Tail 10 où 10 est le nombre de lignes de fond à récupérer.

Cela n'a aucun problème de performances. Je l'ai exécuté sur un fichier texte de plus de 100 Go et j'ai obtenu un résultat instantané.

0
webber55

Si vous avez un fichier qui a un format pair par ligne (comme un système daq), vous utilisez simplement streamreader pour obtenir la longueur du fichier, puis prenez l'une des lignes, (readline()).

Divisez la longueur totale par la longueur de la chaîne. Vous avez maintenant un nombre long général pour représenter le nombre de lignes dans le fichier.

La clé est que vous utilisez la readline() avant d'obtenir vos données pour votre tableau ou autre. Cela garantira que vous commencerez au début d'une nouvelle ligne et que vous n'obtiendrez aucune donnée résiduelle de la précédente.

StreamReader leader = new StreamReader(GetReadFile);
leader.BaseStream.Position = 0;
StreamReader follower = new StreamReader(GetReadFile);

int count = 0;
string tmper = null;
while (count <= 12)
{
    tmper = leader.ReadLine();
    count++;
}

long total = follower.BaseStream.Length; // get total length of file
long step = tmper.Length; // get length of 1 line
long size = total / step; // divide to get number of lines
long go = step * (size - 12); // get the bit location

long cut = follower.BaseStream.Seek(go, SeekOrigin.Begin); // Go to that location
follower.BaseStream.Position = go;

string led = null;
string[] lead = null ;
List<string[]> samples = new List<string[]>();

follower.ReadLine();

while (!follower.EndOfStream)
{
    led = follower.ReadLine();
    lead = Tokenize(led);
    samples.Add(lead);
}
0
Gabe

Dans le cas où vous devez lire un nombre quelconque de lignes en sens inverse à partir d'un fichier texte, voici une classe compatible LINQ que vous pouvez utiliser. Il se concentre sur les performances et la prise en charge des fichiers volumineux. Vous pouvez lire plusieurs lignes et appeler Reverse () pour obtenir les dernières lignes dans l'ordre suivant:

tilisation:

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

Classe ReverseTextReader:

/// <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
}
0
Jon Person

Je viens d'avoir le même problème, un énorme fichier journal auquel il faut accéder via une interface REST. Bien sûr, le charger dans la mémoire et l'envoyer via http n'était pas une solution.

Comme l'a souligné Jon, cette solution a un cas d'utilisation très spécifique. Dans mon cas, je sais pour sûr (et vérifiez), que l'encodage est en utf-8 (avec BOM!) Et peut donc bénéficier de toutes les bénédictions de l'UTF. Ce n'est certainement pas une solution à usage général.

Voici ce qui a fonctionné extrêmement bien et rapidement pour moi (j'ai oublié de fermer le flux - corrigé maintenant):

    private string tail(StreamReader streamReader, long numberOfBytesFromEnd)
    {
        Stream stream = streamReader.BaseStream;
        long length = streamReader.BaseStream.Length;
        if (length < numberOfBytesFromEnd)
            numberOfBytesFromEnd = length;
        stream.Seek(numberOfBytesFromEnd * -1, SeekOrigin.End);

        int LF = '\n';
        int CR = '\r';
        bool found = false;

        while (!found) {
            int c = stream.ReadByte();
            if (c == LF)
                found = true;
        }

        string readToEnd = streamReader.ReadToEnd();
        streamReader.Close();
        return readToEnd;
    }

Nous cherchons d'abord quelque part vers la fin avec le BaseStream, et lorsque nous avons la bonne position de flux, lisez jusqu'à la fin avec le StreamReader habituel.

Cela ne permet pas vraiment de spécifier la quantité de lignes à la fin, ce qui n'est pas une bonne idée de toute façon, car les lignes pourraient être arbitrairement longues et donc, tuer à nouveau les performances. Je spécifie donc la quantité d'octets, lisez jusqu'à ce que nous arrivions à la première ligne nouvelle et lisez confortablement jusqu'à la fin. Théoriquement, vous pouvez également rechercher le CarriageReturn, mais dans mon cas, cela n'était pas nécessaire.

Si nous utilisons ce code, cela ne dérangera pas un thread d'écrivain:

        FileStream fileStream = new FileStream(
            filename,
            FileMode.Open,
            FileAccess.Read,
            FileShare.ReadWrite);

        StreamReader streamReader = new StreamReader(fileStream);
0

Ouvrez le fichier et commencez à lire les lignes. Après avoir lu 10 lignes, ouvrez un autre pointeur, en commençant à l'avant du fichier, de sorte que le deuxième pointeur est en retard de 10 lignes sur le premier. Continuez à lire, en déplaçant les deux pointeurs à l'unisson, jusqu'à ce que le premier atteigne la fin du fichier. Utilisez ensuite le deuxième pointeur pour lire le résultat. Il fonctionne avec n'importe quel fichier de taille, y compris vide et plus court que la longueur de la queue. Et il est facile de s'adapter à n'importe quelle longueur de queue. L'inconvénient, bien sûr, est que vous finissez par lire l'intégralité du fichier et c'est peut-être exactement ce que vous essayez d'éviter.

0
Sisiutl