2009-08-13 49 views
56

Estoy tratando de leer algunos archivos de texto, donde cada línea debe ser procesada. En este momento solo estoy usando StreamReader, y luego leo cada línea individualmente.La lectura de un archivo línea por línea en C#

Me pregunto si hay una manera más eficiente (en términos de la Línea de Control y facilidad de lectura) para hacer esto utilizando LINQ sin comprometer la eficiencia operativa. Los ejemplos que he visto implican cargar todo el archivo en la memoria y luego procesarlo. En este caso, sin embargo, no creo que sea muy eficiente. En el primer ejemplo, los archivos pueden llegar hasta aproximadamente 50 mil, y en el segundo ejemplo, no todas las líneas del archivo que tenga que ser leído (tamaños son típicamente < 10k).

Se podría argumentar que hoy en día no tiene demasiada importancia para estos pequeños archivos, sin embargo, creo que el tipo de enfoque conduce a código ineficiente.

Primer ejemplo:

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

Segundo ejemplo:

// 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

50K no es aún lo suficientemente grande como para que sea en el montón de objetos grandes. La transmisión tiene sentido cuando tus archivos están en el rango de megabyte (o más grande), no en kilobytes. –

Respuesta

91

Puede escribir un lector de línea basado en LINQ con bastante facilidad utilizando un bloque de iterador:

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 para hacer Jon feliz:

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; 

entonces usted tiene ReadFrom(...) como una secuencia perezosamente evaluado sin búfer, perfecto para Where etc.

Tenga en cuenta que si se utiliza el estándar OrderBy o GroupBy, tendrá para amortiguar los datos en la memoria; si necesita agrupar y agregar, "PushLINQ" tiene un código elegante que le permite realizar agregaciones en los datos, pero descartarlo (sin almacenamiento en búfer). La explicación de Jon is here.

+21

Bah, separación de las preocupaciones - separe la línea de lectura en un iterador separada, y el uso de proyección normal :) –

+0

Mucho mejor ... aunque todavía archivo específico;) –

+0

No creo que sus ejemplos compilar. "archivo" ya está definido como un parámetro de cadena, por lo que no puede hacer esa declaración en el bloque de uso. –

23

Es más sencillo de leer una línea y comprobar si es o no es nula que para comprobar si hay EndOfStream todo el tiempo.

Sin embargo, también tengo una clase LineReader en MiscUtil que hace todo esto mucho más simple - básicamente se expone a un archivo (o una Func<TextReader> como IEnumerable<string> que le permiten hacer LINQ cosas sobre él para que pueda hacer cosas similares. :

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

el corazón de LineReader es esta implementación de IEnumerable<string>.GetEnumerator:

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

Casi todo el resto de la fuente es sólo dar forma flexible s de configuración dataSource (que es un Func<TextReader>).

+0

¿Cómo cerrar el archivo? ¿Y liberar el recurso? – ca9163d9

+0

@ dc7a9163d9: La instrucción 'using' ya lo hace: la llamada' dataSource() 'abrirá el archivo, por lo que se eliminará al final de la instrucción' using'. –

1

NOTA: Es necesario tener cuidado con la solución IEnumerable<T>, ya que resultará en el archivo está abierto durante la duración del proceso.

Por ejemplo, la respuesta de Marc Gravell:

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

el archivo permanecerá abierta durante todo el proceso.

+1

Es cierto, pero "archivo abierto durante mucho tiempo, pero sin almacenamiento en búfer" a menudo es mejor que "mucha memoria almacenada durante mucho tiempo" –

+6

Eso es cierto, pero básicamente tienes tres opciones: cargar el lote de una vez (falla para archivos grandes); mantener el archivo abierto (como mencionas); vuelva a abrir el archivo regularmente (tiene una serie de problemas). En muchos, muchos casos, creo que la transmisión y el mantenimiento del archivo abierto es la mejor solución. –

+0

sí, es probablemente una mejor solución para mantener el archivo abierto, pero sólo tiene que estar lejos de la implicación –

0

¡Gracias a todos por sus respuestas! Decidí ir con una mezcla, centrándome principalmente en la de Marc, ya que solo necesitaré leer líneas de un archivo. Supongo que se podría argumentar que la separación es necesaria en todas partes, pero ¡eh, la vida es muy corta!

En cuanto al mantener el archivo abierto, que no va a ser un problema en este caso, ya que el código es parte de una aplicación de escritorio.

Por último os notó toda cadena en minúsculas utilizado. Sé que en Java hay una diferencia entre cadenas en mayúsculas y no en mayúsculas, pero pensé que en C# la cadena en minúsculas era solo una referencia a String en mayúsculas.

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

¿Por qué está pasando la colección al método Load? Al menos llámalo LoadInto si vas a hacer eso;) –

Cuestiones relacionadas