Lire des fichiers texte volumineux avec des flux en C #


96

J'ai la belle tâche de déterminer comment gérer les gros fichiers chargés dans l'éditeur de script de notre application (c'est comme VBA pour notre produit interne pour les macros rapides). La plupart des fichiers pèsent entre 300 et 400 Ko, ce qui est un chargement correct. Mais lorsqu'ils dépassent les 100 Mo, le processus est difficile (comme vous vous en doutez).

Ce qui se passe, c'est que le fichier est lu et poussé dans un RichTextBox qui est ensuite parcouru - ne vous inquiétez pas trop de cette partie.

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

[Reader].ReadToEnd()

ce qui pourrait prendre un certain temps.

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

Quelques hypothèses:

  • La plupart des fichiers auront entre 30 et 40 Mo
  • Le contenu du fichier est du texte (non binaire), certains sont au format Unix, d'autres sont DOS.
  • Une fois le contenu récupéré, nous déterminons quel terminateur est utilisé.
  • Personne n'est concerné une fois qu'il est chargé le temps qu'il faut pour le rendu dans la richtextbox. C'est juste la charge initiale du texte.

Maintenant pour les questions:

  • Puis-je simplement utiliser StreamReader, puis vérifier la propriété Length (donc ProgressMax) et émettre une lecture pour une taille de tampon définie et parcourir dans une boucle while WHILST à l' intérieur d'un travailleur en arrière-plan, afin de ne pas bloquer le thread d'interface utilisateur principal? Puis renvoyez le générateur de chaînes au thread principal une fois qu'il est terminé.
  • Le contenu ira à un StringBuilder. puis-je initialiser le StringBuilder avec la taille du flux si la longueur est disponible?

Est-ce que ce sont (selon vos opinions professionnelles) de bonnes idées? J'ai eu quelques problèmes dans le passé avec la lecture du contenu de Streams, car il manquera toujours les derniers octets ou quelque chose du genre, mais je poserai une autre question si tel est le cas.


29
30 à 40 Mo de fichiers de script? Saint maquereau! Je détesterais avoir à revoir le code que ...
dthorpe

Je sais que cette question est assez ancienne mais je l'ai trouvée l'autre jour et j'ai testé la recommandation pour MemoryMappedFile et c'est de loin la méthode la plus rapide. Une comparaison consiste à lire un fichier de 7616939 lignes de 345 Mo via une méthode readline prend plus de 12 heures sur ma machine tout en effectuant le même chargement et la lecture via MemoryMappedFile a pris 3 secondes.
csonon

Ce ne sont que quelques lignes de code. Voir cette bibliothèque que j'utilise pour lire des fichiers de 25 Go et plus. github.com/Agenty/FileReader
Vikash Rathee

Réponses:


175

Vous pouvez améliorer la vitesse de lecture en utilisant un BufferedStream, 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 de mars 2013

J'ai récemment écrit du code pour la lecture et le traitement (recherche de texte dans) des fichiers texte de 1 Go (beaucoup plus volumineux que les fichiers impliqués ici) et j'ai obtenu un gain de performances significatif en utilisant un modèle producteur / consommateur. La tâche de producteur a lu des lignes de texte à l'aide de BufferedStreamet les a transférées à une tâche de consommateur distincte qui a effectué la recherche.

J'ai utilisé cela comme une opportunité pour 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 en mémoire utilisé pour mettre en cache des 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 la lecture ou l'écriture, mais jamais les deux simultanément. Les méthodes Read et Write de BufferedStream gèrent automatiquement la mémoire tampon.

MISE À JOUR de décembre 2014: votre kilométrage peut varier

Sur la base des commentaires, FileStream doit utiliser un BufferedStream en interne. Au moment où cette réponse a été fournie pour la première fois, j'ai mesuré une amélioration significative des performances en ajoutant un BufferedStream. À l'époque, je ciblais .NET 3.x sur une plate-forme 32 bits. Aujourd'hui, en ciblant .NET 4.5 sur une plateforme 64 bits, je ne vois aucune amélioration.

en relation

Je suis tombé sur un cas où la diffusion d'un gros fichier CSV généré vers le flux de réponse à partir d'une action ASP.Net MVC était très lente. L'ajout d'un BufferedStream a amélioré les performances de 100 fois dans ce cas. Pour en savoir plus, voir Sortie sans tampon très lente


12
Mec, BufferedStream fait toute la différence. +1 :)
Marcus

2
Il y a un coût pour demander des données à un sous-système IO. Dans le cas de disques rotatifs, vous devrez peut-être attendre que le plateau tourne en position pour lire le prochain bloc de données, ou pire, attendre que la tête de disque bouge. Bien que les SSD ne disposent pas de pièces mécaniques pour ralentir les choses, il y a toujours un coût par opération d'E / S pour y accéder. Les flux tamponnés lisent plus que ce que le StreamReader demande, réduisant le nombre d'appels vers le système d'exploitation et, finalement, le nombre de demandes d'E / S séparées.
Eric J.

4
Vraiment? Cela ne fait aucune différence dans mon scénario de test. Selon Brad Abrams, il n'y a aucun avantage à utiliser BufferedStream sur un FileStream.
Nick Cox

2
@NickCox: vos résultats peuvent varier en fonction de votre sous-système d'E / S sous-jacent. Sur un disque rotatif et un contrôleur de disque qui n'a pas les données dans son cache (et aussi les données non mises en cache par Windows), l'accélération est énorme. La chronique de Brad a été écrite en 2004. J'ai récemment mesuré des améliorations réelles et drastiques.
Eric J.

3
Ceci est inutile selon: stackoverflow.com/questions/492283/... FileStream utilise déjà un tampon en interne.
Erwin Mayer

21

Si vous lisez les statistiques de performances et de référence sur ce site Web , vous verrez que le moyen le plus rapide de lire (car 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
    }
}

Au total, environ 9 méthodes différentes ont été évaluées au banc, mais celle-ci semble être en avance la plupart du temps, même en exécutant le lecteur tamponné comme d'autres lecteurs l'ont mentionné.


2
Cela a bien fonctionné pour séparer un fichier postgres de 19 Go afin de le traduire en syntaxe SQL dans plusieurs fichiers. Merci le gars postgres qui n'a jamais exécuté mes paramètres correctement. / soupir
Damon Drake

La différence de performances ici semble porter ses fruits pour les fichiers très volumineux, comme plus de 150 Mo (vous devriez également utiliser un StringBuilderpour les charger en mémoire, se charge plus rapidement car il ne crée pas de nouvelle chaîne à chaque fois que vous ajoutez des caractères)
Joshua G

15

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 fichiers, ou simplement parce qu'ils veulent un retour visuel indiquant que quelque chose se passe?

Si ce dernier est vrai, la solution devient beaucoup plus simple. Faites simplement reader.ReadToEnd()sur un fil d'arrière-plan et affichez une barre de progression de type rectangle au lieu d'une barre appropriée.

Je soulève ce point parce que d'après mon expérience, c'est souvent le cas. Lorsque vous écrivez un programme de traitement de données, les utilisateurs seront certainement intéressés par un pourcentage complet, mais pour les mises à jour d'interface utilisateur simples mais lentes, ils sont plus susceptibles de vouloir simplement savoir que l'ordinateur n'a pas planté. :-)


2
Mais l'utilisateur peut-il annuler l'appel ReadToEnd?
Tim Scarborough le

@Tim, bien repéré. Dans ce cas, nous revenons à la StreamReaderboucle. Cependant, ce sera toujours plus simple car il n'est pas nécessaire de lire à l'avance pour calculer l'indicateur de progression.
Christian Hayter

8

Pour les fichiers binaires, le moyen le plus rapide de les lire que j'ai trouvé est le suivant.

 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.


2
En avez-vous des preuves tangibles? Pourquoi OP devrait-il utiliser cela sur toute autre réponse? Veuillez creuser un peu plus et donner un peu plus de détails
Dylan Corriveau

7

Utilisez un travailleur d'arrière-plan et ne lisez qu'un nombre limité de lignes. En savoir plus uniquement lorsque l'utilisateur fait défiler.

Et essayez de ne jamais utiliser ReadToEnd (). C'est l'une des fonctions que vous pensez "pourquoi l'ont-ils fait?"; c'est un assistant de script pour les enfants qui va bien avec les petites choses, mais comme vous le voyez, ça craint pour les gros fichiers ...

Les gars qui vous disent d'utiliser StringBuilder doivent lire le MSDN plus souvent:

Considérations relatives aux performances
Les méthodes Concat et AppendFormat concaténent toutes deux les nouvelles données avec un objet String ou StringBuilder existant. Une opération de concaténation d'objets String crée toujours un nouvel objet à partir de la chaîne existante et des nouvelles données. Un objet StringBuilder gère une mémoire tampon pour accueillir la concaténation de nouvelles données. De nouvelles données sont ajoutées à la fin du tampon si de la place est disponible; sinon, un nouveau tampon plus grand est alloué, les données du tampon d'origine sont copiées dans le nouveau tampon, puis les nouvelles données sont ajoutées au nouveau tampon. Les performances d'une opération de concaténation pour un objet String ou StringBuilder dépendent de la fréquence à laquelle une allocation de mémoire se produit.
Une opération de concaténation String alloue toujours de la mémoire, tandis qu'une opération de concaténation StringBuilder alloue uniquement de la mémoire si la mémoire tampon d'objet StringBuilder est trop petite pour accueillir les nouvelles données. Par conséquent, la classe String est préférable pour une opération de concaténation si un nombre fixe d'objets String est concaténé. Dans ce cas, les opérations de concaténation individuelles peuvent 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 est concaténé; par exemple, si une boucle concatène un nombre aléatoire de chaînes d'entrée utilisateur.

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

L'option StringBuilder convient parfaitement à ceux qui utilisent le système en tant que mono-utilisateur, mais lorsque deux utilisateurs ou plus lisent des fichiers volumineux en même temps, vous rencontrez un problème.


loin, vous êtes super rapides! Malheureusement, en raison du fonctionnement de la macro, tout le flux doit être chargé. Comme je l'ai mentionné, ne vous inquiétez pas pour la partie richtext. C'est le chargement initial que nous voulons améliorer.
Nicole Lee

afin que vous puissiez travailler par parties, lire les premières X lignes, appliquer la macro, lire les secondes X lignes, appliquer la macro, etc. si vous expliquez ce que fait cette macro, nous pouvons vous aider avec plus de précision
Tufo

5

Cela devrait suffire à vous aider à démarrer.

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

4
Je déplacerais le "var buffer = new char [1024]" hors de la boucle: il n'est pas nécessaire de créer un nouveau buffer à chaque fois. Mettez-le juste avant "while (count> 0)".
Tommy Carlier le

4

Jetez un œil à 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 Intel Quad Core:

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


3
Ces types de tests sont notoirement peu fiables. Vous lirez les données du cache du système de fichiers lorsque vous répéterez le test. C'est au moins un ordre de grandeur plus rapide qu'un vrai test qui lit les données sur le disque. Un fichier de 180 Mo ne peut pas prendre moins de 3 secondes. Redémarrez votre machine, exécutez le test une fois pour le nombre réel.
Hans Passant

7
la ligne stringBuilder.Append est potentiellement dangereuse, vous devez la remplacer par stringBuilder.Append (fileContents, 0, charsRead); pour vous assurer de ne pas ajouter 1 024 caractères complets même lorsque le flux s'est terminé plus tôt.
Johannes Rudolph

@JohannesRudolph, votre commentaire vient de me résoudre un bug. Comment avez-vous trouvé le nombre 1024?
OfirD

3

Vous feriez peut-être mieux d'utiliser la gestion des fichiers mappés en mémoire ici . / invoque pour faire le même travail.

Edit: Voir ici sur le MSDN pour savoir comment cela fonctionne, voici l' entrée de blog indiquant comment cela est fait dans le prochain .NET 4 lorsqu'il sortira en version. Le lien que j'ai donné plus tôt est un wrapper autour du pinvoke pour y parvenir. Vous pouvez mapper le fichier entier dans la mémoire et l'afficher comme une fenêtre coulissante lors du défilement du fichier.


2

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

Comme une chaîne standard ne peut que de taille X, de 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 tokeniser et diviser la ligne lors du traitement. La liste de chaînes peut désormais contenir de très gros volumes de texte.


1

Un itérateur peut ê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 en utilisant ce qui 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();

Au fur et à mesure du chargement du fichier, 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, le StringBuilder contiendra le contenu du fichier texte.

De plus, comme vous voulez du texte, nous pouvons simplement utiliser BinaryReader pour lire les caractères, ce qui garantira que vos tampons s'alignent correctement lors de la lecture de caractères multi-octets ( UTF-8 , UTF-16 , etc.).

Tout cela se fait sans utiliser de tâches d'arrière-plan, de threads ou de machines à états personnalisées complexes.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.