2009-08-13 24 views
56

Sto cercando di leggere alcuni file di testo, in cui ogni riga deve essere elaborata. Al momento sto solo usando uno StreamReader e poi leggendo ogni riga individualmente.Lettura di un file riga per riga in C#

Mi chiedo se ci sia un modo più efficiente (in termini di LoC e leggibilità) per farlo utilizzando LINQ senza compromettere l'efficienza operativa. Gli esempi che ho visto implicano il caricamento dell'intero file in memoria e quindi l'elaborazione. In questo caso, tuttavia, non credo che sarebbe molto efficiente. Nel primo esempio i file possono arrivare a circa 50k e, nel secondo esempio, non è necessario leggere tutte le righe del file (le dimensioni sono in genere < 10k).

Si potrebbe obiettare che al giorno d'oggi non ha importanza per questi file di piccole dimensioni, tuttavia credo che una sorta di approccio porti a un codice inefficiente.

Primo esempio:

// Open file 
using(var file = System.IO.File.OpenText(_LstFilename)) 
{ 
    // Read file 
    while (!file.EndOfStream) 
    { 
     String line = file.ReadLine(); 

     // Ignore empty lines 
     if (line.Length > 0) 
     { 
      // Create addon 
      T addon = new T(); 
      addon.Load(line, _BaseDir); 

      // Add to collection 
      collection.Add(addon); 
     } 
    } 
} 

Secondo esempio:

// Open file 
using (var file = System.IO.File.OpenText(datFile)) 
{ 
    // Compile regexs 
    Regex nameRegex = new Regex("IDENTIFY (.*)"); 

    while (!file.EndOfStream) 
    { 
     String line = file.ReadLine(); 

     // Check name 
     Match m = nameRegex.Match(line); 
     if (m.Success) 
     { 
      _Name = m.Groups[1].Value; 

      // Remove me when other values are read 
      break; 
     } 
    } 
} 
+2

50 K non è nemmeno abbastanza grande da essere inserito nell'heap di oggetti di grandi dimensioni. Lo streaming ha senso quando i file si trovano nell'intervallo di megabyte (o più grande), non in kilobyte. –

risposta

91

È possibile scrivere un lettore linea di LINQ a base abbastanza facilmente usando un blocco iteratore:

static IEnumerable<SomeType> ReadFrom(string file) { 
    string line; 
    using(var reader = File.OpenText(file)) { 
     while((line = reader.ReadLine()) != null) { 
      SomeType newRecord = /* parse line */ 
      yield return newRecord; 
     } 
    } 
} 

o per fare Jon felice:

static IEnumerable<string> ReadFrom(string file) { 
    string line; 
    using(var reader = File.OpenText(file)) { 
     while((line = reader.ReadLine()) != null) { 
      yield return line; 
     } 
    } 
} 
... 
var typedSequence = from line in ReadFrom(path) 
        let record = ParseLine(line) 
        where record.Active // for example 
        select record.Key; 

poi si deve ReadFrom(...) come sequenza pigramente valutata senza buffering, ideale per Where ecc

nota che se si utilizza OrderBy o lo standard GroupBy, dovrà buffer dei dati in memoria; se hai bisogno di raggruppamento e aggregazione, "PushLINQ" ha un codice di fantasia che ti consente di eseguire aggregazioni sui dati ma di scartarlo (senza buffering). La spiegazione di Jon is here.

+21

Bah, separazione delle preoccupazioni: separare la lettura della linea in un iteratore separato e utilizzare la proiezione normale :) –

+0

Molto più bello ... anche se specifico del file;) –

+0

Non penso che i tuoi esempi verranno compilati. "file" è già definito come parametro della stringa, quindi non è possibile effettuare tale dichiarazione nel blocco using. –

23

È più semplice leggere una riga e controllare se è nullo o meno il controllo di EndOfStream tutto il tempo.

Tuttavia, ho anche una classe LineReader in MiscUtil che rende tutto questo molto più semplice - in fondo si espone un file (o una Func<TextReader> come IEnumerable<string> che permette di fare LINQ roba su di esso Così si possono fare cose simili. :

var query = from file in Directory.GetFiles("*.log") 
      from line in new LineReader(file) 
      where line.Length > 0 
      select new AddOn(line); // or whatever 

il cuore di LineReader è questa implementazione di IEnumerable<string>.GetEnumerator:

public IEnumerator<string> GetEnumerator() 
{ 
    using (TextReader reader = dataSource()) 
    { 
     string line; 
     while ((line = reader.ReadLine()) != null) 
     { 
      yield return line; 
     } 
    } 
} 

Quasi tutto il resto della sorgente è solo dare modo flessibile s di impostare dataSource (che è un Func<TextReader>).

+0

Come chiudere il file? E rilasciare la risorsa? – ca9163d9

+0

@ dc7a9163d9: L'istruzione 'using' lo fa già - la chiamata' dataSource() 'aprirà il file, e quindi sarà eliminata alla fine dell'istruzione' using'. –

1

NOTA: È necessario prestare attenzione alla soluzione IEnumerable<T>, poiché il file verrà aperto per la durata dell'elaborazione.

Ad esempio, con la risposta di Marc Gravell:

foreach(var record in ReadFrom("myfile.csv")) { 
    DoLongProcessOn(record); 
} 

il file rimarrà aperto per tutta la lavorazione.

+1

Vero, ma "il file aperto per un lungo periodo, ma non il buffering" è spesso meglio di "un sacco di memoria hogged per un lungo periodo" –

+6

Questo è vero - ma in fondo hai tre scelte: carica il lotto in una volta sola (non riesce per i file di grandi dimensioni); tieni il file aperto (come dici tu); riaprire il file regolarmente (ha un numero di problemi). In molti, molti casi, ritengo che lo streaming e il mantenimento del file aperto siano la soluzione migliore. –

+0

Sì, probabilmente è una soluzione migliore per mantenere aperto il file, ma devi solo evitare l'implicazione –

0

Grazie a tutti per le vostre risposte! Ho deciso di andare con un mix, concentrandomi principalmente su Marc's, visto che dovrò solo leggere le righe da un file. Immagino tu possa dire che la separazione è necessaria ovunque, ma heh, la vita è troppo breve!

Per quanto riguarda la conservazione del file aperto, non si tratta di un problema in questo caso, in quanto il codice fa parte di un'applicazione desktop.

Infine ho notato che hai usato tutti una stringa minuscola. So che in Java c'è una differenza tra stringhe maiuscole e maiuscole, ma ho pensato che in C# la stringa minuscola fosse solo un riferimento alla stringa in maiuscolo?

public void Load(AddonCollection<T> collection) 
{ 
    // read from file 
    var query = 
     from line in LineReader(_LstFilename) 
     where line.Length > 0 
     select CreateAddon(line); 

    // add results to collection 
    collection.AddRange(query); 
} 

protected T CreateAddon(String line) 
{ 
    // create addon 
    T addon = new T(); 
    addon.Load(line, _BaseDir); 

    return addon; 
} 

protected static IEnumerable<String> LineReader(String fileName) 
{ 
    String line; 
    using (var file = System.IO.File.OpenText(fileName)) 
    { 
     // read each line, ensuring not null (EOF) 
     while ((line = file.ReadLine()) != null) 
     { 
      // return trimmed line 
      yield return line.Trim(); 
     } 
    } 
} 
+2

Perché stai passando la raccolta nel metodo di caricamento? Almeno chiamalo LoadInto se hai intenzione di farlo;) –

Problemi correlati