Lettura di file di testo di grandi dimensioni con flussi in C #

Ho il delizioso compito di capire come gestire i file di grandi dimensioni caricati nell’editor di script della nostra applicazione (è come VBA per il nostro prodotto interno per macro veloci). La maggior parte dei file ha dimensioni di circa 300-400 KB, il che è un buon caricamento. Ma quando vanno oltre i 100 MB, il processo ha difficoltà (come ci si aspetterebbe).

Quello che succede è che il file viene letto e spostato in un RichTextBox che viene poi navigato – non preoccuparti troppo di questa parte.

Lo sviluppatore che ha scritto il codice iniziale sta semplicemente usando uno StreamReader e sta facendo

[Reader].ReadToEnd() 

che potrebbe richiedere un po ‘di tempo per completare.

Il mio compito è rompere questo bit di codice, leggerlo in blocchi in un buffer e mostrare una barra di avanzamento con un’opzione per cancellarlo.

Alcune ipotesi:

  • La maggior parte dei file sarà 30-40 MB
  • Il contenuto del file è testo (non binario), alcuni sono in formato Unix, altri DOS.
  • Una volta recuperati i contenuti, risolviamo il terminatore utilizzato.
  • Nessuno è preoccupato dopo aver caricato il tempo necessario per eseguire il rendering in richtextbox. È solo il caricamento iniziale del testo.

Ora per le domande:

  • Posso semplicemente usare StreamReader, quindi controllare la proprietà Length (quindi ProgressMax) ed emettere un Read per una dimensione del buffer impostata e scorrere in un ciclo while WHILST all’interno di un worker in background, quindi non blocca il thread dell’interfaccia utente principale? Quindi restituisci il stringbuilder al thread principale una volta completato.
  • I contenuti andranno a un StringBuilder. posso inizializzare lo StringBuilder con le dimensioni del stream se la lunghezza è disponibile?

Sono queste (nelle tue opinioni professionali) buone idee? Ho avuto alcuni problemi in passato con la lettura di contenuti da Stream, perché mancheranno sempre gli ultimi pochi byte o qualcosa del genere, ma farò un’altra domanda se questo è il caso.

Puoi migliorare la velocità di lettura usando BufferedStream, come questo:

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

Marzo 2013 AGGIORNAMENTO

Recentemente ho scritto il codice per leggere e elaborare (cercare testo in) file di testo 1 GB (molto più grande dei file coinvolti qui) e ho ottenuto un significativo aumento delle prestazioni utilizzando un pattern produttore / consumatore. L’attività del produttore ha letto righe di testo utilizzando BufferedStream e le ha trasferite a un compito consumatore separato che ha effettuato la ricerca.

Ho usato questo come un’opportunità per imparare TPL Dataflow, che è molto adatto per codificare rapidamente questo modello.

Perché BufferedStream è più veloce

Un buffer è un blocco di byte in memoria utilizzato per memorizzare i dati nella cache, riducendo così il numero di chiamate al sistema operativo. I buffer migliorano le prestazioni di lettura e scrittura. Un buffer può essere usato per leggere o scrivere, ma mai entrambi contemporaneamente. I metodi di lettura e scrittura di BufferedStream mantengono automaticamente il buffer.

Dicembre 2014 AGGIORNAMENTO: il tuo chilometraggio può variare

Sulla base dei commenti, FileStream dovrebbe utilizzare internamente un BufferedStream . Al momento in cui questa risposta è stata fornita per la prima volta, ho misurato un significativo incremento delle prestazioni aggiungendo un BufferedStream. In quel momento stavo prendendo di mira .NET 3.x su una piattaforma a 32 bit. Oggi, con il targeting .NET 4.5 su una piattaforma a 64 bit, non vedo alcun miglioramento.

Relazionato

Mi sono imbattuto in un caso in cui lo streaming di un file CSV di grandi dimensioni generato nel stream di risposta da un’azione ASP.Net MVC era molto lento. Aggiunta di un BufferedStream miglioramento delle prestazioni di 100x in questa istanza. Per ulteriori informazioni, vedere Uscita non bufferata molto lenta

Dici che ti è stato chiesto di mostrare una barra di avanzamento mentre viene caricato un file di grandi dimensioni. È perché gli utenti vogliono davvero vedere la percentuale esatta di caricamento del file, o solo perché vogliono un riscontro visivo che qualcosa stia accadendo?

Se quest’ultimo è vero, allora la soluzione diventa molto più semplice. Basta reader.ReadToEnd() su un thread in background e visualizzare una barra di avanzamento di tipo tendone invece di una corretta.

Sollevo questo punto perché nella mia esperienza questo è spesso il caso. Quando stai scrivendo un programma di elaborazione dati, gli utenti saranno sicuramente interessati a una figura% completa, ma per gli aggiornamenti dell’interfaccia utente semplici ma lenti, è più probabile che vogliano sapere che il computer non si è bloccato. 🙂

Se leggi le statistiche relative al rendimento e al benchmark su questo sito web , vedrai che il modo più veloce per leggere (poiché la lettura, la scrittura e l’elaborazione sono tutte diverse) un file di testo è il seguente frammento di codice:

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

Tutti i 9 metodi diversi erano segnati su banco, ma quello sembra uscito in vantaggio nella maggior parte del tempo, persino eseguendo il lettore bufferato come altri utenti hanno menzionato.

Per i file binari, il modo più veloce di leggerli che ho trovato è questo.

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

Nei miei test è centinaia di volte più veloce.

Utilizzare un lavoratore in background e leggere solo un numero limitato di righe. Leggi di più solo quando l’utente scorre.

E prova a non usare mai ReadToEnd (). È una delle funzioni che pensi “perché l’hanno fatto?”; è un aiutante di script kiddies che va bene con piccole cose, ma come vedi, fa schifo per file di grandi dimensioni …

Quelli che ti dicono di usare StringBuilder devono leggere il MSDN più spesso:

Considerazioni sulle prestazioni
I metodi Concat e AppendFormat concatenano entrambi i nuovi dati a un object String o StringBuilder esistente. Un’operazione di concatenazione di oggetti stringa crea sempre un nuovo object dalla stringa esistente e dai nuovi dati. Un object StringBuilder mantiene un buffer per ospitare la concatenazione di nuovi dati. I nuovi dati vengono aggiunti alla fine del buffer se la stanza è disponibile; in caso contrario, viene allocato un nuovo buffer più grande, i dati dal buffer originale vengono copiati nel nuovo buffer, quindi i nuovi dati vengono aggiunti al nuovo buffer. Le prestazioni di un’operazione di concatenazione per un object String o StringBuilder dipendono dalla frequenza con cui si verifica un’allocazione di memoria.
Un’operazione di concatenazione delle stringhe alloca sempre la memoria, mentre un’operazione di concatenazione StringBuilder assegna solo memoria se il buffer dell’object StringBuilder è troppo piccolo per ospitare i nuovi dati. Di conseguenza, la class String è preferibile per un’operazione di concatenazione se un numero fisso di oggetti String viene concatenato. In tal caso, le singole operazioni di concatenazione potrebbero anche essere combinate in un’unica operazione dal compilatore. Un object StringBuilder è preferibile per un’operazione di concatenazione se un numero arbitrario di stringhe è concatenato; ad esempio, se un loop concatena un numero casuale di stringhe di input dell’utente.

Ciò significa un’enorme allocazione di memoria, che diventa un grande uso del sistema di file di scambio, che simula sezioni del disco rigido per agire come la memoria RAM, ma un’unità disco rigido è molto lenta.

L’opzione StringBuilder sembra a posto per chi usa il sistema come utente mono, ma quando hai due o più utenti che leggono file di grandi dimensioni allo stesso tempo, hai un problema.

Questo dovrebbe essere sufficiente per iniziare.

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

Dai un’occhiata al seguente frammento di codice. Hai menzionato che la Most files will be 30-40 MB . Questo afferma di leggere 180 MB in 1,4 secondi su 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); } } } 

Articolo originale

Potresti star meglio usare i file mappati in memoria che gestiscono qui . Il supporto per i file mappati in memoria sarà disponibile in .NET 4 (penso … l’ho sentito attraverso qualcun altro che ne parla), quindi questo wrapper che usa p / invoca per fare lo stesso lavoro ..

Modifica: vedi qui su MSDN per come funziona, ecco il post di blog che indica come è fatto nel prossimo .NET 4 quando uscirà come versione. Il collegamento che ho dato in precedenza è un wrapper attorno al pinvoke per raggiungere questo objective. È ansible mappare l’intero file in memoria e visualizzarlo come una finestra scorrevole durante lo scorrimento del file.

Un iteratore potrebbe essere perfetto per questo tipo di lavoro:

 public static IEnumerable 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); } } } } 

Puoi chiamarlo usando il seguente:

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

Quando il file viene caricato, l’iteratore restituirà il numero progressivo da 0 a 100, che è ansible utilizzare per aggiornare la barra di avanzamento. Una volta terminato il ciclo, StringBuilder conterrà il contenuto del file di testo.

Inoltre, poiché vuoi il testo, possiamo semplicemente usare BinaryReader per leggere i caratteri, il che assicurerà che i tuoi buffer si allineino correttamente durante la lettura di caratteri multibyte ( UTF-8 , UTF-16 , ecc.).

Tutto ciò è ansible senza utilizzare attività in background, thread o complesse macchine a stati personalizzati.

Tutte ottime risposte! tuttavia, per chi cerca una risposta, questi sembrano essere in qualche modo incompleti.

Come una stringa standard può solo di Dimensione X, da 2 Gb a 4 Gb a seconda della configurazione, queste risposte non soddisfano realmente la domanda dell’OP. Un metodo è quello di lavorare con un elenco di stringhe:

 List Words = new List(); using (StreamReader sr = new StreamReader(@"C:\Temp\file.txt")) { string line = string.Empty; while ((line = sr.ReadLine()) != null) { Words.Add(line); } } 

Alcuni potrebbero voler Tokenise e dividere la linea durante l’elaborazione. L’elenco delle stringhe ora può contenere volumi molto grandi di testo.

So che queste domande sono piuttosto vecchie ma l’ho trovato l’altro giorno e ho testato la raccomandazione per MemoryMappedFile e questo è senza dubbio il metodo più veloce. Un confronto è la lettura di un file 345MB linea 345MB tramite un metodo readline richiede più di 12 ore sulla mia macchina mentre si esegue lo stesso carico e si legge tramite MemoryMappedFile ci sono voluti 3 secondi.

Il mio file supera i 13 GB: inserisci la descrizione dell'immagine qui

Il link muggito contiene il codice che legge facilmente un file:

Leggi un grande file di testo

Maggiori informazioni