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:
Maintenant pour les questions:
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.
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
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é.
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é. :-)
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.
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.
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();
}
}
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);
}
}
}
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.
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.
Le lien ci-dessous contient le code qui permet de lire facilement un fichier:
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.