Leer archivos de texto grandes con transmisiones en C #

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 tienen una capacidad de carga de 300-400 KB. Pero cuando van más allá de 100 MB, el proceso tiene dificultades (como era de esperar).

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

El desarrollador que escribió el código inicial simplemente está usando StreamReader y está haciendo

[Reader].ReadToEnd() 

que podría tomar un buen tiempo para completar.

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

Algunas suposiciones:

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

Ahora para las preguntas:

  • ¿Puedo simplemente usar StreamReader, luego verificar la propiedad Length (por lo tanto ProgressMax) y emitir una lectura para un tamaño de búfer establecido e iterar en un ciclo while WHILST dentro de un trabajador en segundo plano, para que no bloquee el hilo de UI principal? A continuación, devuelva el generador de cadenas al hilo principal una vez que se haya completado.
  • El contenido irá a un StringBuilder. ¿Puedo inicializar StringBuilder con el tamaño de la secuencia si la longitud está disponible?

¿Son estas (en sus opiniones profesionales) 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.

Puedes mejorar la velocidad de lectura usando 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) { } } 

Marzo de 2013 ACTUALIZACIÓN

Hace poco escribí código para leer y procesar (buscar texto en) archivos de texto de 1 GB-ish (mucho más grandes que los archivos involucrados aquí) y logré un aumento significativo en el rendimiento mediante el uso de un patrón productor / consumidor. La tarea de productor leyó en líneas de texto usando BufferedStream y se los entregó a una tarea de consumidor separada que realizó 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 utilizado para almacenar en caché los datos, lo que reduce el número de llamadas al sistema operativo. Los búferes mejoran el rendimiento de lectura y escritura. Se puede usar un buffer 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.

Diciembre de 2014 ACTUALIZACIÓN: su millaje puede variar

Según los comentarios, FileStream debería usar un BufferedStream internamente. En el momento en que se proporcionó por primera vez esta respuesta, medí un impulso de rendimiento significativo al agregar un BufferedStream. En ese momento apuntaba a .NET 3.x en una plataforma de 32 bits. Hoy, al apuntar a .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 gran archivo CSV generado a la secuencia de respuesta de una acción ASP.Net MVC fue muy lento. Agregar un rendimiento mejorado BufferedStream por 100x en esta instancia. Para más información vea Salida sin búfer muy lenta

Usted dice que se le ha pedido que muestre una barra de progreso mientras se está cargando un archivo grande. ¿Eso se debe a que los usuarios realmente desean ver el% de carga de archivos exacto, o simplemente porque quieren retroalimentación visual de que algo está sucediendo?

Si esto último es cierto, entonces la solución se vuelve mucho más simple. Simplemente haga reader.ReadToEnd() en una reader.ReadToEnd() de fondo, y muestre una barra de progreso tipo marquesina en lugar de una correcta.

Planteo este punto porque, en mi experiencia, este suele ser el caso. Cuando está escribiendo un progtwig 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 deseen saber que la computadora no se ha bloqueado. 🙂

Si lee el rendimiento y las estadísticas de referencia en este sitio web , verá que la manera más rápida de leer (porque leer, escribir y procesar 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 } } 

Todos los 9 métodos diferentes se marcaron en el banco, pero parece que la mayoría de las veces salen adelante, e incluso realizan la lectura del buffer como lo han mencionado otros lectores.

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

  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.

Utilice un trabajador de fondo y lea solo un número limitado de líneas. Lea 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 veis, apesta por archivos grandes …

Esos tipos que le dicen que use StringBuilder necesitan 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 datos nuevos. Un objeto StringBuilder mantiene un búfer para acomodar la concatenación de datos nuevos. Se añaden nuevos datos 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 se añaden los nuevos datos al nuevo búfer. La ejecución 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 cadenas siempre asigna memoria, mientras que una operación de concatenación StringBuilder solo asigna memoria si el búfer de objetos StringBuilder es demasiado pequeño para acomodar los datos nuevos. En consecuencia, la clase String es preferible para una operación de concatenación si se concatena un número fijo de objetos String. En ese caso, las operaciones de concatenación individuales incluso podrían combinarse en una única operación por el comstackdor. Un objeto StringBuilder es preferible 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 quién utiliza el sistema como usuario mono, pero cuando tiene dos o más usuarios que leen archivos grandes al mismo tiempo, tiene un problema.

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

Eche un vistazo al siguiente fragmento de código. Usted ha 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

Puede ser mejor que utilices aquí el manejo de archivos mapeados en memoria … El soporte de archivos mapeados en memoria estará disponible en .NET 4 (creo … lo escuché a través de alguien más hablando de eso), de ahí esta envoltura que usa p / invoca hacer el mismo trabajo …

Editar: mira aquí en MSDN para ver cómo funciona, aquí está la entrada del blog que indica cómo se hace en el próximo .NET 4 cuando se publique como versión. El enlace que he dado anteriormente es una envoltura alrededor del pinvoke para lograr esto. Puede asignar el archivo completo a la memoria y verlo como una ventana deslizante al desplazarse por el archivo.

Un iterador puede ser perfecto para este tipo de trabajo:

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

Puedes 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 que el ciclo ha terminado, StringBuilder contendrá el contenido del archivo de texto.

Además, como desea texto, podemos utilizar BinaryReader para leer en 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.

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

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

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

Algunos pueden desear Tokenise y dividir la línea cuando procesan. La lista de cadenas ahora puede contener grandes volúmenes de texto.

Sé que esta pregunta es bastante antigua, pero la encontré el otro día y he probado la recomendación para MemoryMappedFile y este es sin dudas el método más rápido. Una comparación es leer un archivo de línea de 7,616,939 345MB a través de un método de lectura que toma más de 12 horas en mi máquina mientras se realiza la misma carga y la lectura a través de MemoryMappedFile tomó 3 segundos.

Mi archivo tiene más de 13 GB: enter image description here

El siguiente enlace contiene el código que lee una porción de archivo fácilmente:

Lee un archivo de texto grande

Más información