web-dev-qa-db-fra.com

Lecture de gros fichiers texte avec des flux en C #

J'ai la belle tâche de savoir comment gérer les gros fichiers en cours de chargement dans l'éditeur de script de notre application (c'est comme VBA pour notre produit interne pour les macros rapides ). La plupart des fichiers font entre 300 et 400 Ko, ce qui facilite le chargement. Mais quand ils dépassent 100 Mo, le processus est difficile (comme on peut s'y attendre).

En fait, le fichier est lu et inséré dans un RichTextBox qui est ensuite navigué - 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 à compléter.

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 feront entre 30 et 40 Mo
  • Le contenu du fichier est textuel (pas binaire), certains sont au format Unix, certains sont au format DOS.
  • Une fois le contenu récupéré, nous déterminons quel terminateur est utilisé.
  • Personne ne s'inquiète une fois que le temps nécessaire au rendu dans la richtext est chargé. C'est juste le chargement initial 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 la parcourir dans une boucle while [~ # ~] tandis que [~ # ~ ] dans un travailleur d’arrière-plan, afin de ne pas bloquer le thread principal de l’interface utilisateur? Ensuite, renvoyez le constructeur de chaînes au thread principal une fois qu'il est terminé.
  • Le contenu ira à un StringBuilder. Puis-je initialiser 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 déjà eu quelques problèmes avec la lecture de contenu à partir de Streams, car les derniers octets, ou quelque chose du genre, seront toujours oubliés, mais je poserai une autre question si tel est le cas.

86
Nicole Lee

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)
    {

    }
}

mars 2013 UPDATE

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 concernés ici) et j'ai réalisé un gain de performance significatif en utilisant un modèle producteur/consommateur. La tâche de producteur lisait des lignes de texte à l'aide de BufferedStream et les transmettait à une tâche de consommateur distincte qui effectuait la recherche.

J'ai utilisé cette opportunité pour apprendre le flux de données TPL, qui convient très bien 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 de lecture et d'écriture de BufferedStream maintiennent automatiquement la mémoire tampon.

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

Selon les commentaires, FileStream devrait utiliser 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 plate-forme 64 bits, je ne vois aucune amélioration.

Connexes

Je suis parvenu à un cas où la diffusion d'un fichier CSV généré volumineux 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 cette instance. Pour plus d'informations, voir sortie très rapide avec mémoire tampon

165
Eric J.

Si vous lisez le statistiques de performance et de benchmark sur ce site , vous verrez que le moyen le plus rapide de est de lire (parce que lire , écriture et 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é marquées au banc, mais celle-ci semble sortir la plupart du temps, même en exécutant le lecteur mis en mémoire tampon comme autre les lecteurs ont mentionné.

15
user4023224

Vous dites que vous avez été invité à afficher une barre de progression pendant le chargement d'un fichier volumineux. Est-ce parce que les utilisateurs veulent vraiment voir le pourcentage exact de chargement de fichier, ou simplement parce qu'ils veulent un retour visuel de ce qui se passe?

Si ce dernier est vrai, 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 barre correcte.

Je soulève ce point parce que, selon 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 chiffre% complet, mais pour les mises à jour simples mais lentes de l'interface utilisateur, ils auront plus tendance à vouloir simplement savoir que l'ordinateur ne s'est pas écrasé. :-)

14
Christian Hayter

Pour les fichiers binaires, voici le moyen le plus rapide de les lire.

 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.

8
StainlessBeer

Utilisez un arrière-plan et ne lisez qu'un nombre limité de lignes. Lire plus que 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 = script kiddies ' helper 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 sur les performances
Les méthodes Concat et AppendFormat concaténent toutes les nouvelles données dans un objet String ou StringBuilder existant. Une opération de concaténation d'objet String crée toujours un nouvel objet à partir de la chaîne existante et des nouvelles données. Un objet StringBuilder gère un tampon pour permettre la concaténation de nouvelles données. Les nouvelles données sont ajoutées à la fin de la mémoire 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 d'allocation de mémoire.
Une opération de concaténation de chaînes alloue toujours de la mémoire, tandis qu'une opération de concaténation de StringBuilder n'alloue de la mémoire que si le tampon d'objet de StringBuilder est trop petit 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 sont concaténés. 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 sont concaténées. par exemple, si une boucle concatène un nombre aléatoire de chaînes d'entrées utilisateur.

Cela signifie énorme allocation de mémoire, ce qui devient une utilisation importante du système de fichiers swap, qui simule des sections de votre disque dur agissant comme le RAM mémoire, mais un lecteur de disque dur est très lent.

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

6
Tufo

Cela devrait être suffisant pour 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();
    }
}
5
ChaosPandion

Consultez l'extrait de code suivant. Vous avez mentionné Most files will be 30-40 MB. Cela prétend lire 180 Mo en 1,4 secondes 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

4
James

Vous feriez peut-être mieux d’utiliser des fichiers mappés en mémoire ici .. La prise en charge des fichiers mappés en mémoire sera disponible dans .NET 4 (je pense… j’ai entendu cela par l’intermédiaire de quelqu'un qui en parle) , d’où ce wrapper qui utilise p/invoke pour faire le même travail.

Edit: Voir ici sur le MSDN pour son fonctionnement, voici le - blog entrée indiquant comment cela se passe dans le prochain .NET 4 lorsqu’il sortira en version. Le lien que j'ai donné précédemment est un wrapper autour du pinvoke pour y parvenir. Vous pouvez mapper l'intégralité du fichier dans la mémoire et l'afficher comme une fenêtre glissante lors du défilement du fichier.

3
t0mm13b

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.

De plus, comme vous voulez du texte, nous pouvons simplement utiliser BinaryReader pour lire les caractères, ce qui garantira l’alignement correct des tampons lors de la lecture de caractères multi-octets ( TF-8 , TF -16 , etc.).

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

1
Extremeswank

Mon fichier dépasse 13 Go: enter image description here

Le lien ci-dessous contient le code qui permet de lire facilement un fichier:

Lire un gros fichier texte

Plus d'informations

0
Alireza

Toutes les réponses sont excellentes! Cependant, pour ceux qui recherchent une réponse, celles-ci semblent quelque peu incomplètes.

Comme une chaîne standard ne peut contenir que des tailles X, 2 Go à 4 Go, en fonction de votre configuration, ces réponses ne répondent pas vraiment à la question du PO. Une méthode consiste à utiliser 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 scinder la ligne lors du traitement. La liste des chaînes peut maintenant contenir de très gros volumes de texte.

0
Rusty Nail