Lecture de gros fichiers texte avec des flux en C#

j'ai la tâche charmante de travailler sur la façon de gérer les grands fichiers étant chargés dans l'éditeur de script de notre application (il est comme VBA pour notre produit interne pour les macros rapides). La plupart des fichiers sont d'environ 300-400 KB ce qui est un chargement fin. Mais quand ils vont au-delà de 100 MB le processus a un moment difficile (comme vous pouvez vous y attendre).

ce qui se passe, c'est que le fichier est lu et placé dans une boîte RichTextBox qui est ensuite navigée - ne vous inquiétez pas trop cette partie.

le développeur qui a écrit le code initial utilise simplement un StreamReader et fait

[Reader].ReadToEnd()

qui pourrait prendre un certain temps à compléter.

ma tâche est de casser ce morceau de code, le lire en morceaux dans un tampon et montrer une barre de progression avec une option pour l'annuler.

Quelques hypothèses:

  • la plupart des fichiers seront 30-40 MB
  • le contenu du fichier est du texte (Pas binaire), certains sont au format Unix, certains sont DOS.
  • une fois le contenu récupéré, Nous déterminons quel terminator est utilisé.
  • personne n'est concerné une fois qu'il est chargé le temps qu'il faut pour rendre dans la richtextbox. C'est juste la charge initiale du texte.

maintenant pour les questions:

  • puis-je simplement utiliser StreamReader, puis vérifier le La propriété de longueur (donc ProgressMax) et de publier une lecture pour une taille de tampon définie et itérer à travers dans une boucle while tandis que à l'intérieur d'un travailleur d'arrière-plan, de sorte qu'il ne bloque pas le fil D'UI principal? Puis retournez le stringbuilder au thread principal une fois qu'il est terminé.
  • le contenu sera envoyé à un éditeur de String. puis-je initialiser le StringBuilder avec la taille du flux si la longueur est disponible?

sont ces (dans vos opinions professionnelles) de bonnes idées? J'ai eu quelques problèmes dans le passé avec la lecture de contenu de flux, parce qu'il manquera toujours les derniers octets ou quelque chose, mais je vais poser une autre question si c'est le cas.

80
demandé sur Peter Mortensen 2010-01-29 15:36:42

12 réponses

vous pouvez améliorer la vitesse de lecture en utilisant un souffle tampon, comme ceci:

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

mise à jour Mars 2013

j'ai récemment écrit du code pour la lecture et le traitement (recherche de texte) des fichiers texte de 1 Go-ish (beaucoup plus grand que les fichiers impliqués ici) et a réalisé un gain de performance significatif en utilisant un modèle producteur/consommateur. La tâche de producteur a lu dans les lignes de texte en utilisant le BufferedStream et les a remis à un tâche de consommation séparée qui a fait la recherche.

j'ai utilisé ceci comme une occasion d'apprendre TPL Dataflow, qui est très bien adapté pour coder rapidement ce modèle.

Pourquoi BufferedStream est plus rapide

un tampon est Un bloc d'octets dans la mémoire pour le cache de données, réduisant ainsi le nombre d'appels au système d'exploitation. Les tampons améliorent les performances de lecture et d'écriture. Un tampon peut être utilisé pour lire ou écrire, mais jamais les deux simultanément. Les méthodes de lecture et D'écriture de BufferedStream maintiennent automatiquement le tampon.

décembre 2014 mise à jour: votre kilométrage peut varier

basé sur les commentaires, FileStream devrait utiliser un BufferedStream à l'interne. Au moment où cette réponse a été fournie pour la première fois, j'ai mesuré une augmentation significative de la performance en ajoutant un BufferedStream. À l'époque, je ciblais .NET 3.X sur une plateforme 32 bits. Aujourd'hui, cibler .NET 4.5 sur une plateforme 64 bits, Je ne vois aucune amélioration.

Liées

je suis tombé sur un cas où la diffusion en continu d'un grand fichier CSV généré au flux de réponse à partir d'un ASP.Net L'action de MVC a été très lente. L'ajout d'un flux BufferedStream a amélioré les performances de 100x dans ce cas. Pour plus de voir sortie non tamponnée très lente

151
répondu Eric J. 2017-05-23 11:55:07

vous dites qu'on vous a demandé d'afficher une barre de progression pendant le chargement d'un gros fichier. Est-ce parce que les utilisateurs veulent vraiment voir le pourcentage exact de chargement de fichier, ou tout simplement parce qu'ils veulent un feedback visuel que quelque chose se passe?

si cette dernière est vraie, alors la solution devient beaucoup plus simple. Il suffit de faire reader.ReadToEnd() sur un thread d'arrière-plan, et d'afficher une barre de progression de type marquee au lieu d'une propre.

je soulève ce point parce que dans mon expérience, c'est souvent le cas. Quand vous écrivez un programme de traitement de données, alors les utilisateurs seront certainement intéressés par un pourcentage complet, mais pour les mises à jour D'UI simples mais lents, ils sont plus susceptibles de vouloir simplement savoir que l'ordinateur ne s'est pas écrasé. :- )

14
répondu Christian Hayter 2010-01-29 13:03:51

si vous lisez les statistiques de performance et de benchmark sur ce site web , vous verrez que le moyen le plus rapide de lire (parce que la lecture, l'écriture, et le traitement sont tous différents) un fichier texte est l'extrait de code suivant:

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

tout en haut environ 9 différentes méthodes ont été marquées sur le banc, mais que l'on semble sortir à l'avance la plupart du temps, même l'exécution du lecteur tamponné comme d'autres lecteurs l'ont mentionné.

13
répondu WorkRelated 2016-08-15 09:32:57

pour les fichiers binaires, le moyen le plus rapide de les lire est celui-ci.

 MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(file);
 MemoryMappedViewStream mms = mmf.CreateViewStream();
 using (BinaryReader b = new BinaryReader(mms))
 {
 }

dans mes tests, c'est des centaines de fois plus rapide.

7
répondu StainlessBeer 2015-06-28 22:52:38

utilisez un travailleur de fond et ne lisez qu'un nombre limité de lignes. Lire la suite lorsque l'utilisateur fait défiler.

et essayez de ne jamais utiliser ReadToEnd(). C'est une des fonctions que vous pensez "pourquoi ils l'ont fait?"; c'est un script kiddies' helper qui va bien avec les petites choses, mais comme vous voyez, il craint pour les gros dossiers...

ceux qui vous disent D'utiliser StringBuilder doivent lire le MSDN plus souvent:

Facteurs De Performance

Les méthodes de Concat et D'AppendFormat concatent toutes deux de nouvelles données à une chaîne ou à un objet StringBuilder existant. Une opération de concaténation D'un objet String crée toujours un nouvel objet à partir de la chaîne existante et des nouvelles données. Un objet StringBuilder maintient un tampon pour accommoder la concaténation de nouvelles données. De nouvelles données sont ajoutées à la fin de la mémoire tampon si de la place est disponible; autrement, une nouvelle mémoire tampon plus grande est attribué, les données du buffer original sont copiées dans le nouveau buffer, puis les nouvelles données sont ajoutées au nouveau buffer. La performance d'une opération de concaténation pour une chaîne ou un objet StringBuilder dépend de la fréquence d'une allocation de mémoire.

Une opération de concaténation de chaîne alloue toujours de la mémoire, alors qu'une opération de concaténation de StringBuilder n'alloue de la mémoire que si le tampon D'objet StringBuilder est trop petit pour accommoder les nouvelles données. Conséquent, la classe String est préférable pour une opération de concaténation si un nombre fixe d'objets String sont concaténées. Dans ce cas, les différentes opérations de concaténation pourraient même être combinées en une seule opération par le compilateur. Un objet StringBuilder est préférable pour une opération de concaténation si un nombre arbitraire de chaînes sont concaténées; par exemple, si une boucle concaténate un nombre aléatoire de chaînes d'entrée utilisateur.

qui signifie énorme allocation de mémoire, ce qui devient grande utilisation du système de fichiers de pagination, qui simule des sections de votre disque dur pour agir comme la mémoire vive, mais un disque dur est très lent.

L'option StringBuilder semble très bien pour ceux qui utilisent le système comme un mono-utilisateur, mais quand vous avez deux utilisateurs ou plus lisant de gros fichiers en même temps, vous avez un problème.

6
répondu Tufo 2015-06-28 22:27:58

ça devrait vous suffire pour commencer.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}
5
répondu ChaosPandion 2010-01-29 13:10:57

regardez l'extrait de code suivant. Vous avez mentionné Most files will be 30-40 MB . Cela prétend lire 180 Mo en 1,4 seconde sur un Noyau Quad Intel:

private int _bufferSize = 16384;

private void ReadFile(string filename)
{
    StringBuilder stringBuilder = new StringBuilder();
    FileStream fileStream = new FileStream(filename, FileMode.Open, FileAccess.Read);

    using (StreamReader streamReader = new StreamReader(fileStream))
    {
        char[] fileContents = new char[_bufferSize];
        int charsRead = streamReader.Read(fileContents, 0, _bufferSize);

        // Can't do much with 0 bytes
        if (charsRead == 0)
            throw new Exception("File is 0 bytes");

        while (charsRead > 0)
        {
            stringBuilder.Append(fileContents);
            charsRead = streamReader.Read(fileContents, 0, _bufferSize);
        }
    }
}

Article Original

4
répondu James 2015-06-28 22:29:17

il serait peut-être préférable d'utiliser des fichiers mappés en mémoire traitant ici .. Le support du fichier mappé en mémoire sera autour de .NET 4 (je pense...J'ai entendu que par quelqu'un d'autre parler de lui), d'où ce wrapper qui utilise p/invoque pour faire le même travail..

Edit: Voir ici sur le MSDN de comment cela fonctionne, voici le blog entrée indiquant comment c'est fait dans le à venir .NET 4 quand il sort en tant que version. Le lien que j'ai donné plus tôt est un enveloppement autour de la pinvoke pour atteindre ceci. Vous pouvez mapper le fichier entier dans la mémoire, et le voir comme une fenêtre coulissante en faisant défiler le fichier.

3
répondu t0mm13b 2017-09-16 22:28:52

un itérateur pourrait être parfait pour ce type de travail:

public static IEnumerable<int> LoadFileWithProgress(string filename, StringBuilder stringData)
{
    const int charBufferSize = 4096;
    using (FileStream fs = File.OpenRead(filename))
    {
        using (BinaryReader br = new BinaryReader(fs))
        {
            long length = fs.Length;
            int numberOfChunks = Convert.ToInt32((length / charBufferSize)) + 1;
            double iter = 100 / Convert.ToDouble(numberOfChunks);
            double currentIter = 0;
            yield return Convert.ToInt32(currentIter);
            while (true)
            {
                char[] buffer = br.ReadChars(charBufferSize);
                if (buffer.Length == 0) break;
                stringData.Append(buffer);
                currentIter += iter;
                yield return Convert.ToInt32(currentIter);
            }
        }
    }
}

vous pouvez l'appeler comme suit:

string filename = "C:\myfile.txt";
StringBuilder sb = new StringBuilder();
foreach (int progress in LoadFileWithProgress(filename, sb))
{
    // Update your progress counter here!
}
string fileData = sb.ToString();

lorsque le fichier est chargé, l'itérateur renvoie le numéro de progression de 0 à 100, que vous pouvez utiliser pour mettre à jour votre barre de progression. Une fois la boucle terminée, StringBuilder contiendra le contenu du fichier texte.

aussi, parce que vous voulez du texte, nous pouvons juste utiliser BinaryReader pour lisez en caractères, ce qui vous permettra de vous assurer que vos tampons s'alignent correctement lors de la lecture de tout caractère à plusieurs octets ( UTF-8 , UTF-16 , etc.).

tout cela se fait sans tâches d'arrière-plan, threads, ou machines d'état complexes personnalisées.

1
répondu Extremeswank 2015-06-28 22:32:19

mon fichier est supérieur à 13 Go: enter image description here

le lien de bellow contient le code qui lit un morceau de fichier facilement:

Lire un gros fichier texte

plus d'information

1
répondu Alireza 2018-08-18 18:40:45

toutes excellentes réponses! cependant, pour quelqu'un qui cherche une réponse, celles-ci semblent quelque peu incomplètes.

comme une chaîne de caractères standard ne peut que de taille X, 2 Go à 4 Go selon votre configuration, ces réponses ne répondent pas vraiment à la question de L'OP. Une méthode consiste à travailler avec une liste de chaînes:

List<string> Words = new List<string>();

using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt"))
{

string line = string.Empty;

while ((line = sr.ReadLine()) != null)
{
    Words.Add(line);
}
}

certains peuvent vouloir Tokenise et diviser la ligne lors du traitement. La liste des Chaînes peut maintenant contenir de très gros volumes de texte.

0
répondu Rusty Nail 2018-01-22 05:58:25

je sais que ces questions est assez ancienne, mais je l'ai trouvé l'autre jour et ont testé la recommandation pour MemoryMappedFile et ce est hands down la méthode la plus rapide. Une comparaison est la lecture d'un fichier ligne 345MB 7.616.939 via une méthode readline prend 12+ heures sur ma machine tout en exécutant la même charge et lire via MemoryMappedFile a pris 3 secondes.

0
répondu csonon 2018-02-13 14:03:02