Leer archivos de texto grandes con secuencias en C #

96

Tengo la hermosa tarea de averiguar cómo manejar archivos grandes que se cargan en el editor de scripts de nuestra aplicación (es como VBA para nuestro producto interno para macros rápidas). La mayoría de los archivos pesan alrededor de 300-400 KB, lo cual es una carga adecuada. Pero cuando superan los 100 MB, el proceso tiene dificultades (como era de esperar).

Lo que sucede es que el archivo se lee y se inserta en un RichTextBox que luego se navega; no se preocupe demasiado por esta parte.

El desarrollador que escribió el código inicial simplemente usa un StreamReader y hace

[Reader].ReadToEnd()

que podría tardar bastante en completarse.

Mi tarea es dividir este fragmento de código, leerlo en trozos en un búfer y mostrar una barra de progreso con una opción para cancelarlo.

Algunas suposiciones:

  • La mayoría de los archivos serán de 30 a 40 MB
  • El contenido del archivo es texto (no binario), algunos son formato Unix, algunos son DOS.
  • Una vez que se recupera el contenido, calculamos qué terminador se utiliza.
  • A nadie le preocupa una vez que se ha cargado, el tiempo que se tarda en renderizar en el cuadro de texto enriquecido. Es solo la carga inicial del texto.

Ahora para las preguntas:

  • ¿Puedo simplemente usar StreamReader, luego verificar la propiedad Length (es decir, ProgressMax) y emitir una lectura para un tamaño de búfer establecido e iterar en un ciclo while MIENTRAS dentro de un trabajador en segundo plano, para que no bloquee el hilo principal de la interfaz de usuario? Luego, regrese el generador de cadenas al hilo principal una vez que esté completo.
  • Los contenidos irán a un StringBuilder. ¿Puedo inicializar StringBuilder con el tamaño de la secuencia si la longitud está disponible?

¿Son estas (en su opinión profesional) buenas ideas? He tenido algunos problemas en el pasado con la lectura de contenido de Streams, porque siempre se perderán los últimos bytes o algo así, pero haré otra pregunta si este es el caso.

Nicole Lee
fuente
29
30-40 MB de archivos de script? ¡Caballa Santa! Odiaría tener que revisar el código que ...
dthorpe
Sé que esta pregunta es bastante antigua, pero la encontré el otro día y probé la recomendación para MemoryMappedFile y este es sin duda el método más rápido. Una comparación es leer un archivo de 7,616,939 líneas de 345 MB a través de un método de línea de lectura que toma más de 12 horas en mi máquina mientras que realizar la misma carga y lectura a través de MemoryMappedFile tomó 3 segundos.
csonon
Son solo unas pocas líneas de código. Vea esta biblioteca que estoy usando para leer archivos de 25 GB y más también. github.com/Agenty/FileReader
Vikash Rathee

Respuestas:

175

Puede mejorar la velocidad de lectura utilizando un BufferedStream, como este:

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

    }
}

ACTUALIZACIÓN de marzo de 2013

Recientemente escribí código para leer y procesar (buscando texto en) archivos de texto de 1 GB (mucho más grandes que los archivos involucrados aquí) y logré una ganancia significativa de rendimiento mediante el uso de un patrón de productor / consumidor. La tarea de productor leyó líneas de texto usando el BufferedStreamy las entregó a una tarea de consumidor separada que hizo la búsqueda.

Usé esto como una oportunidad para aprender TPL Dataflow, que es muy adecuado para codificar rápidamente este patrón.

Por qué BufferedStream es más rápido

Un búfer es un bloque de bytes en la memoria que se utiliza para almacenar datos en caché, lo que reduce la cantidad de llamadas al sistema operativo. Los búferes mejoran el rendimiento de lectura y escritura. Se puede usar un búfer para leer o escribir, pero nunca para ambos simultáneamente. Los métodos de lectura y escritura de BufferedStream mantienen automáticamente el búfer.

ACTUALIZACIÓN de diciembre de 2014: su millaje puede variar

Según los comentarios, FileStream debería usar un BufferedStream internamente. En el momento en que se proporcionó esta respuesta por primera vez, medí un aumento significativo del rendimiento al agregar un BufferedStream. En ese momento, estaba apuntando a .NET 3.x en una plataforma de 32 bits. Hoy, con el objetivo de .NET 4.5 en una plataforma de 64 bits, no veo ninguna mejora.

Relacionado

Me encontré con un caso en el que la transmisión de un archivo CSV grande y generado al flujo de respuesta desde una acción ASP.Net MVC fue muy lenta. Agregar un BufferedStream mejoró el rendimiento en 100 veces en este caso. Para obtener más información, consulte Salida sin búfer muy lenta

Eric J.
fuente
12
Amigo, BufferedStream marca la diferencia. +1 :)
Marcus
2
La solicitud de datos de un subsistema de E / S tiene un costo. En el caso de los discos giratorios, es posible que deba esperar a que el plato gire a su posición para leer el siguiente fragmento de datos o, peor aún, esperar a que se mueva el cabezal del disco. Si bien los SSD no tienen partes mecánicas para ralentizar las cosas, todavía hay un costo por operación de IO para acceder a ellos. Los flujos almacenados en búfer leen más que lo que solicita StreamReader, lo que reduce la cantidad de llamadas al sistema operativo y, en última instancia, la cantidad de solicitudes de E / S separadas.
Eric J.
4
De Verdad? Esto no hace ninguna diferencia en mi escenario de prueba. Según Brad Abrams, no hay ningún beneficio al usar BufferedStream sobre un FileStream.
Nick Cox
2
@NickCox: sus resultados pueden variar según su subsistema de IO subyacente. En un disco giratorio y un controlador de disco que no tiene los datos en su caché (y también datos no almacenados en caché por Windows), la aceleración es enorme. La columna de Brad se escribió en 2004. Recientemente, midí las mejoras reales y drásticas.
Eric J.
3
Esto es inútil según: stackoverflow.com/questions/492283/… FileStream ya usa un búfer internamente.
Erwin Mayer
21

Si lee las estadísticas de rendimiento y de referencia en este sitio web , verá que la forma más rápida de leer (porque la lectura, la escritura y el procesamiento son todos diferentes) un archivo de texto es el siguiente fragmento de código:

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

Se evaluaron aproximadamente 9 métodos diferentes, pero ese parece adelantarse la mayor parte del tiempo, incluso superando al lector en búfer, como han mencionado otros lectores.


fuente
2
Esto funcionó bien para desmontar un archivo postgres de 19 GB para traducirlo a sintaxis sql en varios archivos. Gracias al chico de Postgres que nunca ejecutó mis parámetros correctamente. / suspiro
Damon Drake
La diferencia de rendimiento aquí parece ser rentable para archivos realmente grandes, como más de 150 MB (también debería usar un StringBuilderpara cargarlos en la memoria, se carga más rápido ya que no crea una nueva cadena cada vez que agrega caracteres)
Joshua G
15

Dice que le han pedido que muestre una barra de progreso mientras se carga un archivo grande. ¿Se debe a que los usuarios realmente quieren ver el% exacto de carga de archivos, o simplemente porque quieren comentarios visuales de que algo está sucediendo?

Si esto último es cierto, entonces la solución se vuelve mucho más simple. Solo hazlo reader.ReadToEnd()en un hilo de fondo y muestra una barra de progreso tipo marquesina en lugar de una adecuada.

Planteo este punto porque, en mi experiencia, este suele ser el caso. Cuando esté escribiendo un programa de procesamiento de datos, los usuarios definitivamente estarán interesados ​​en una cifra% completa, pero para las actualizaciones de IU simples pero lentas, es más probable que solo quieran saber que la computadora no se ha bloqueado. :-)

Christian Hayter
fuente
2
Pero, ¿puede el usuario cancelar la llamada ReadToEnd?
Tim Scarborough
@Tim, bien visto. En ese caso, volvemos al StreamReaderciclo. Sin embargo, seguirá siendo más sencillo porque no es necesario seguir leyendo para calcular el indicador de progreso.
Christian Hayter
8

Para archivos binarios, la forma más rápida de leerlos que he encontrado es esta.

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

En mis pruebas es cientos de veces más rápido.

Cerveza inoxidable
fuente
2
¿Tiene alguna evidencia sólida de esto? ¿Por qué OP debería usar esto sobre cualquier otra respuesta? Profundice un poco más y proporcione un poco más de detalles
Dylan Corriveau
7

Utilice un trabajador en segundo plano y lea solo un número limitado de líneas. Leer más solo cuando el usuario se desplaza.

Y trate de nunca usar ReadToEnd (). Es una de las funciones que piensas "¿por qué lo hicieron?"; es un ayudante de script kiddies que va bien con cosas pequeñas, pero como ves, apesta para archivos grandes ...

Los tipos que le dicen que use StringBuilder deben leer MSDN con más frecuencia:

Consideraciones de rendimiento
Los métodos Concat y AppendFormat concatenan datos nuevos a un objeto String o StringBuilder existente. Una operación de concatenación de objetos String siempre crea un nuevo objeto a partir de la cadena existente y los nuevos datos. Un objeto StringBuilder mantiene un búfer para adaptarse a la concatenación de nuevos datos. Los nuevos datos se agregan al final del búfer si hay espacio disponible; de lo contrario, se asigna un nuevo búfer más grande, los datos del búfer original se copian en el nuevo búfer y luego los nuevos datos se añaden al nuevo búfer. El rendimiento de una operación de concatenación para un objeto String o StringBuilder depende de la frecuencia con la que se produce una asignación de memoria.
Una operación de concatenación de String siempre asigna memoria, mientras que una operación de concatenación de StringBuilder solo asigna memoria si el búfer del objeto StringBuilder es demasiado pequeño para acomodar los nuevos datos. En consecuencia, la clase String es preferible para una operación de concatenación si se concatenan un número fijo de objetos String. En ese caso, el compilador podría incluso combinar las operaciones de concatenación individuales en una sola operación. Es preferible un objeto StringBuilder para una operación de concatenación si se concatenan un número arbitrario de cadenas; por ejemplo, si un bucle concatena un número aleatorio de cadenas de entrada del usuario.

Eso significa una gran asignación de memoria, lo que se convierte en un gran uso del sistema de archivos de intercambio, que simula secciones de la unidad de disco duro para que actúen como la memoria RAM, pero una unidad de disco duro es muy lenta.

La opción StringBuilder se ve bien para quienes usan el sistema como un usuario único, pero cuando tiene dos o más usuarios leyendo archivos grandes al mismo tiempo, tiene un problema.

Tufo
fuente
lejos, ustedes son súper rápidos! desafortunadamente, debido a la forma en que funciona la macro, es necesario cargar toda la secuencia. Como mencioné, no se preocupe por la parte de texto enriquecido. Es la carga inicial que queremos mejorar.
Nicole Lee
para que pueda trabajar en partes, leer las primeras líneas X, aplicar la macro, leer las segundas X líneas, aplicar la macro, etc. Si explica lo que hace esta macro, podemos ayudarlo con más precisión
Tufo
5

Esto debería ser suficiente para comenzar.

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();
    }
}
ChaosPandion
fuente
4
Movería "var buffer = new char [1024]" fuera del bucle: no es necesario crear un nuevo búfer cada vez. Simplemente póngalo antes de "while (count> 0)".
Tommy Carlier
4

Eche un vistazo al siguiente fragmento de código. Has mencionado Most files will be 30-40 MB. Esto afirma leer 180 MB en 1.4 segundos en 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);
        }
    }
}

Artículo original

James
fuente
3
Este tipo de pruebas son notoriamente poco fiables. Leerá datos del caché del sistema de archivos cuando repita la prueba. Eso es al menos un orden de magnitud más rápido que una prueba real que lee los datos del disco. Un archivo de 180 MB no puede tardar menos de 3 segundos. Reinicie su máquina, ejecute la prueba una vez para obtener el número real.
Hans Passant
7
la línea stringBuilder.Append es potencialmente peligrosa, debe reemplazarla con stringBuilder.Append (fileContents, 0, charsRead); para asegurarse de no agregar 1024 caracteres completos incluso cuando la transmisión haya finalizado antes.
Johannes Rudolph
@JohannesRudolph, tu comentario me acaba de resolver un error. ¿Cómo se te ocurrió el número 1024?
HeyJude
3

Es posible que sea mejor utilizar el manejo de archivos mapeados en memoria aquí . El soporte de archivos mapeados en memoria estará disponible en .NET 4 (creo ... lo escuché a través de alguien más hablando de ello), de ahí este contenedor que usa p / invoca para hacer el mismo trabajo ..

Editar: Vea aquí en MSDN cómo funciona, aquí está la entrada del blog que indica cómo se hace en el próximo .NET 4 cuando salga como lanzamiento. El enlace que he dado anteriormente es un envoltorio alrededor del pinvoke para lograr esto. Puede mapear todo el archivo en la memoria y verlo como una ventana deslizante cuando se desplaza por el archivo.

t0mm13b
fuente
2

¡Todas excelentes respuestas! sin embargo, para alguien que busca una respuesta, estas parecen estar algo incompletas.

Como una cadena estándar solo puede tener un tamaño X, de 2 Gb a 4 Gb, según su configuración, estas respuestas realmente no cumplen con la pregunta del OP. Un método es trabajar con una lista de cadenas:

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

Algunos pueden querer tokenizar y dividir la línea al procesar. La lista de cadenas ahora puede contener grandes volúmenes de texto.

clavo oxidado
fuente
1

Un iterador puede ser perfecto para este tipo de trabajo:

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

Puede llamarlo usando lo siguiente:

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

A medida que se carga el archivo, el iterador devolverá el número de progreso de 0 a 100, que puede usar para actualizar su barra de progreso. Una vez finalizado el ciclo, StringBuilder contendrá el contenido del archivo de texto.

Además, debido a que desea texto, podemos usar BinaryReader para leer caracteres, lo que garantizará que sus búferes se alineen correctamente al leer cualquier carácter de varios bytes ( UTF-8 , UTF-16 , etc.).

Todo esto se hace sin utilizar tareas en segundo plano, subprocesos o complejas máquinas de estado personalizadas.

Extremeswank
fuente