Lesen großer Textdateien mit Streams in C #


96

Ich habe die schöne Aufgabe, herauszufinden, wie man mit großen Dateien umgeht, die in den Skripteditor unserer Anwendung geladen werden (es ist wie VBA für unser internes Produkt für schnelle Makros). Die meisten Dateien haben eine Größe von 300-400 KB, was ein gutes Laden ist. Aber wenn sie über 100 MB hinausgehen, fällt es dem Prozess schwer (wie zu erwarten).

Was passiert ist, dass die Datei gelesen und in eine RichTextBox verschoben wird, die dann navigiert wird - machen Sie sich nicht zu viele Sorgen um diesen Teil.

Der Entwickler, der den ursprünglichen Code geschrieben hat, verwendet einfach einen StreamReader und tut dies

[Reader].ReadToEnd()

Das könnte eine ganze Weile dauern.

Meine Aufgabe ist es, diesen Code aufzubrechen, ihn in Blöcken in einen Puffer einzulesen und einen Fortschrittsbalken mit der Option zum Abbrechen anzuzeigen.

Einige Annahmen:

  • Die meisten Dateien sind 30-40 MB groß
  • Der Inhalt der Datei ist Text (nicht binär), einige sind im Unix-Format, einige sind DOS.
  • Sobald der Inhalt abgerufen ist, ermitteln wir, welcher Terminator verwendet wird.
  • Niemand ist besorgt, sobald es die Zeit geladen hat, die zum Rendern in der Richtextbox benötigt wird. Es ist nur das anfängliche Laden des Textes.

Nun zu den Fragen:

  • Kann ich einfach StreamReader verwenden, dann die Length-Eigenschaft (also ProgressMax) überprüfen und einen Lesevorgang für eine festgelegte Puffergröße ausgeben und in einer while-Schleife WHILST in einem Hintergrund-Worker durchlaufen , damit der Haupt-UI-Thread nicht blockiert wird? Kehren Sie dann den Stringbuilder nach Abschluss zum Hauptthread zurück.
  • Der Inhalt wird an einen StringBuilder gesendet. Kann ich den StringBuilder mit der Größe des Streams initialisieren, wenn die Länge verfügbar ist?

Sind das (Ihrer Meinung nach) gute Ideen? Ich hatte in der Vergangenheit einige Probleme beim Lesen von Inhalten aus Streams, da immer die letzten Bytes oder ähnliches fehlen, aber ich werde eine andere Frage stellen, wenn dies der Fall ist.


29
30-40 MB Skriptdateien? Heilige Makrele! Ich würde es hassen, Code überprüfen zu müssen, dass ...
dthorpe

Ich weiß, dass diese Fragen ziemlich alt sind, aber ich habe sie neulich gefunden und die Empfehlung für MemoryMappedFile getestet. Dies ist zweifellos die schnellste Methode. Ein Vergleich besteht darin, dass das Lesen einer Datei mit 7.616.939 Zeilen und 345 MB über eine Readline-Methode auf meinem Computer mehr als 12 Stunden dauert, während das gleiche Laden und das Lesen über MemoryMappedFile 3 Sekunden dauert.
Csonon

Es sind nur wenige Codezeilen. Sehen Sie sich diese Bibliothek an, die ich zum Lesen von 25 GB und mehr großen Dateien verwende. github.com/Agenty/FileReader
Vikash Rathee

Antworten:


175

Sie können die Lesegeschwindigkeit verbessern, indem Sie einen BufferedStream wie folgt verwenden:

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

    }
}

März 2013 UPDATE

Ich habe kürzlich Code zum Lesen und Verarbeiten (Suchen nach Text in) 1 GB-ischen Textdateien (viel größer als die hier beteiligten Dateien) geschrieben und durch die Verwendung eines Produzenten / Konsumenten-Musters einen signifikanten Leistungsgewinn erzielt. Die Produzentenaufgabe las Textzeilen mit dem ein BufferedStreamund übergab sie an eine separate Verbraucheraufgabe, die die Suche durchführte.

Ich nutzte dies als Gelegenheit, um den TPL-Datenfluss zu lernen, der sich sehr gut zum schnellen Codieren dieses Musters eignet.

Warum BufferedStream schneller ist

Ein Puffer ist ein Block von Bytes im Speicher, der zum Zwischenspeichern von Daten verwendet wird, wodurch die Anzahl der Aufrufe des Betriebssystems verringert wird. Puffer verbessern die Lese- und Schreibleistung. Ein Puffer kann entweder zum Lesen oder Schreiben verwendet werden, jedoch niemals beide gleichzeitig. Die Lese- und Schreibmethoden von BufferedStream verwalten den Puffer automatisch.

Dezember 2014 UPDATE: Ihr Kilometerstand kann variieren

Basierend auf den Kommentaren, sollte Filestream eine Verwendung sein BufferedStream intern. Als diese Antwort zum ersten Mal gegeben wurde, habe ich durch Hinzufügen eines BufferedStream einen signifikanten Leistungsschub gemessen. Zu der Zeit zielte ich auf .NET 3.x auf einer 32-Bit-Plattform. Wenn ich heute .NET 4.5 auf einer 64-Bit-Plattform ausrichte, sehe ich keine Verbesserung.

verbunden

Ich bin auf einen Fall gestoßen, in dem das Streaming einer großen, generierten CSV-Datei von einer ASP.Net MVC-Aktion in den Antwort-Stream sehr langsam war. Durch Hinzufügen eines BufferedStream wurde die Leistung in diesem Fall um das 100-fache verbessert. Weitere Informationen finden Sie unter Ungepufferte Ausgabe sehr langsam


12
Alter, BufferedStream macht den Unterschied. +1 :)
Marcus

2
Das Anfordern von Daten von einem E / A-Subsystem ist mit Kosten verbunden. Bei rotierenden Datenträgern müssen Sie möglicherweise warten, bis sich der Plattenteller in Position dreht, um den nächsten Datenblock zu lesen, oder, schlimmer noch, warten, bis sich der Datenträgerkopf bewegt. Während SSDs keine mechanischen Teile haben, um die Dinge zu verlangsamen, fallen immer noch Kosten pro E / A-Betrieb an, um darauf zuzugreifen. Gepufferte Streams lesen mehr als nur die Anforderungen des StreamReader, wodurch die Anzahl der Aufrufe des Betriebssystems und letztendlich die Anzahl der separaten E / A-Anforderungen verringert wird.
Eric J.

4
"Ja wirklich?" Dies macht in meinem Testszenario keinen Unterschied. Laut Brad Abrams hat die Verwendung von BufferedStream gegenüber einem FileStream keinen Vorteil.
Nick Cox

2
@NickCox: Ihre Ergebnisse können je nach zugrunde liegendem E / A-Subsystem variieren. Auf einer rotierenden Festplatte und einem Festplattencontroller, auf dem sich die Daten nicht im Cache befinden (und auch Daten, die nicht von Windows zwischengespeichert wurden), ist die Beschleunigung enorm. Brads Kolumne wurde 2004 geschrieben. Ich habe kürzlich tatsächliche, drastische Verbesserungen gemessen.
Eric J.

3
Dies ist nutzlos gemäß: stackoverflow.com/questions/492283/… FileStream verwendet bereits intern einen Puffer.
Erwin Mayer

21

Wenn Sie die Leistungs- und Benchmark-Statistiken auf dieser Website lesen , werden Sie feststellen, dass der schnellste Weg zum Lesen einer Textdatei (da Lesen, Schreiben und Verarbeiten unterschiedlich sind) der folgende Codeausschnitt ist:

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

Alle bis zu 9 verschiedenen Methoden wurden mit einem Benchmark versehen, aber diese scheint die meiste Zeit die Nase vorn zu haben, selbst wenn der gepufferte Leser ausgeführt wird, wie andere Leser erwähnt haben.


2
Dies funktionierte gut, um eine 19-GB-Postgres-Datei zu entfernen und sie in mehrere Dateien in SQL-Syntax zu übersetzen. Vielen Dank an Postgres, der meine Parameter nie richtig ausgeführt hat. / Seufzer
Damon Drake

Der Leistungsunterschied hier scheint sich für wirklich große Dateien auszuzahlen, z. B. größer als 150 MB (außerdem sollten Sie eine StringBuilderzum Laden in den Speicher verwenden, die schneller geladen werden, da nicht jedes Mal, wenn Sie Zeichen hinzufügen, eine neue Zeichenfolge erstellt wird)
Joshua G.

15

Sie sagen, Sie wurden aufgefordert, einen Fortschrittsbalken anzuzeigen, während eine große Datei geladen wird. Liegt das daran, dass die Benutzer wirklich den genauen Prozentsatz des Ladens von Dateien sehen möchten, oder nur daran, dass sie visuelles Feedback wünschen, dass etwas passiert?

Wenn letzteres zutrifft, wird die Lösung viel einfacher. Führen Sie einfach reader.ReadToEnd()einen Hintergrund-Thread aus und zeigen Sie einen Fortschrittsbalken vom Typ Laufschrift anstelle eines richtigen an.

Ich spreche diesen Punkt an, weil dies meiner Erfahrung nach häufig der Fall ist. Wenn Sie ein Datenverarbeitungsprogramm schreiben, sind Benutzer definitiv an einer% vollständigen Zahl interessiert. Bei einfachen, aber langsamen Aktualisierungen der Benutzeroberfläche möchten sie eher wissen, dass der Computer nicht abgestürzt ist. :-)


2
Aber kann der Benutzer den ReadToEnd-Aufruf abbrechen?
Tim Scarborough

@ Tim, gut entdeckt. In diesem Fall sind wir wieder auf dem StreamReaderLaufenden. Es wird jedoch immer noch einfacher, da Sie nicht weiterlesen müssen, um die Fortschrittsanzeige zu berechnen.
Christian Hayter

8

Für Binärdateien ist dies der schnellste Weg, sie zu lesen.

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

In meinen Tests ist es hunderte Male schneller.


2
Haben Sie irgendwelche harten Beweise dafür? Warum sollte OP dies gegenüber anderen Antworten verwenden? Bitte graben Sie etwas tiefer und geben Sie ein bisschen mehr Details
Dylan Corriveau

7

Verwenden Sie einen Hintergrundarbeiter und lesen Sie nur eine begrenzte Anzahl von Zeilen. Lesen Sie mehr nur, wenn der Benutzer einen Bildlauf durchführt.

Und versuchen Sie niemals, ReadToEnd () zu verwenden. Es ist eine der Funktionen, von denen Sie denken, "warum haben sie es geschafft?"; Es ist ein Skript-Kinderhelfer , der gut zu kleinen Dingen passt , aber wie Sie sehen, ist es für große Dateien zum Kotzen ...

Die Leute, die dir sagen, dass du StringBuilder verwenden sollst, müssen die MSDN öfter lesen:

Leistungsüberlegungen
Die Methoden Concat und AppendFormat verketten neue Daten mit einem vorhandenen String- oder StringBuilder-Objekt. Eine Verkettungsoperation für Zeichenfolgenobjekte erstellt immer ein neues Objekt aus der vorhandenen Zeichenfolge und den neuen Daten. Ein StringBuilder-Objekt verwaltet einen Puffer, um die Verkettung neuer Daten zu ermöglichen. Neue Daten werden an das Ende des Puffers angehängt, wenn Platz verfügbar ist. Andernfalls wird ein neuer, größerer Puffer zugewiesen, Daten aus dem ursprünglichen Puffer werden in den neuen Puffer kopiert, und die neuen Daten werden an den neuen Puffer angehängt. Die Leistung einer Verkettungsoperation für einen String oder ein StringBuilder-Objekt hängt davon ab, wie oft eine Speicherzuweisung erfolgt.
Eine String-Verkettungsoperation weist immer Speicher zu, während eine StringBuilder-Verkettungsoperation nur Speicher zuweist, wenn der StringBuilder-Objektpuffer zu klein ist, um die neuen Daten aufzunehmen. Folglich ist die String-Klasse für eine Verkettungsoperation vorzuziehen, wenn eine feste Anzahl von String-Objekten verkettet ist. In diesem Fall können die einzelnen Verkettungsoperationen vom Compiler sogar zu einer einzigen Operation kombiniert werden. Ein StringBuilder-Objekt ist für eine Verkettungsoperation vorzuziehen, wenn eine beliebige Anzahl von Zeichenfolgen verkettet wird. Zum Beispiel, wenn eine Schleife eine zufällige Anzahl von Zeichenfolgen von Benutzereingaben verkettet.

Das bedeutet eine enorme Speicherzuweisung, was zu einer großen Nutzung des Swap-Dateisystems führt, das Teile Ihres Festplattenlaufwerks simuliert, um sich wie der RAM-Speicher zu verhalten, aber ein Festplattenlaufwerk ist sehr langsam.

Die StringBuilder-Option sieht gut aus, wenn Sie das System als Mono-Benutzer verwenden. Wenn jedoch zwei oder mehr Benutzer gleichzeitig große Dateien lesen, tritt ein Problem auf.


weit draußen seid ihr super schnell! Leider muss aufgrund der Funktionsweise des Makros der gesamte Stream geladen werden. Wie gesagt, mach dir keine Sorgen um den Richtext-Teil. Es ist das anfängliche Laden, das wir verbessern wollen.
Nicole Lee

So können Sie in Teilen arbeiten, erste X-Zeilen lesen, das Makro anwenden, die zweiten X-Zeilen lesen, das Makro anwenden usw. Wenn Sie erklären, was dieses Makro tut, können wir Ihnen genauer helfen
Tufo

5

Dies sollte ausreichen, um Ihnen den Einstieg zu erleichtern.

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

4
Ich würde den "var buffer = new char [1024]" aus der Schleife verschieben: Es ist nicht notwendig, jedes Mal einen neuen Puffer zu erstellen. Setzen Sie es einfach vor "while (count> 0)".
Tommy Carlier

4

Schauen Sie sich das folgende Code-Snippet an. Sie haben erwähnt Most files will be 30-40 MB. Dies behauptet, 180 MB in 1,4 Sekunden auf einem Intel Quad Core zu lesen:

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

Originaler Artikel


3
Diese Art von Tests ist notorisch unzuverlässig. Sie lesen Daten aus dem Dateisystem-Cache, wenn Sie den Test wiederholen. Das ist mindestens eine Größenordnung schneller als ein echter Test, bei dem die Daten von der Festplatte gelesen werden. Eine 180-MB-Datei kann möglicherweise nicht weniger als 3 Sekunden dauern. Starten Sie Ihren Computer neu und führen Sie den Test einmal für die tatsächliche Anzahl aus.
Hans Passant

7
Die Zeile stringBuilder.Append ist möglicherweise gefährlich. Sie müssen sie durch stringBuilder.Append (fileContents, 0, charsRead) ersetzen. um sicherzustellen, dass Sie keine vollen 1024 Zeichen hinzufügen, auch wenn der Stream früher beendet wurde.
Johannes Rudolph

@JohannesRudolph, dein Kommentar hat mir gerade einen Fehler behoben. Wie sind Sie auf die Nummer 1024 gekommen?
HeyJude

3

Sie könnten besser dran zu verwenden Memory-Mapped - Dateien verarbeitet werden hier .. Die Memory - Mapped - Datei - Unterstützung in .NET 4 sein um wird (ich glaube ... ich hörte , dass durch jemand anderes darüber zu reden), damit dieser Wrapper , der p verwendet Ich rufe auf, um den gleichen Job zu machen.

Bearbeiten: Sehen Sie hier auf dem MSDN, wie es funktioniert. Hier ist der Blogeintrag, der angibt, wie es in der kommenden .NET 4-Version gemacht wird, wenn es als Release herauskommt. Der Link, den ich zuvor gegeben habe, ist ein Wrapper um den Pinvoke, um dies zu erreichen. Sie können die gesamte Datei dem Speicher zuordnen und sie beim Scrollen durch die Datei wie ein Schiebefenster anzeigen.


2

Alles gute Antworten! Für jemanden, der nach einer Antwort sucht, scheinen diese jedoch etwas unvollständig zu sein.

Da ein Standard-String je nach Konfiguration nur die Größe X, 2 GB bis 4 GB haben kann, erfüllen diese Antworten die Frage des OP nicht wirklich. Eine Methode besteht darin, mit einer Liste von Zeichenfolgen zu arbeiten:

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

Einige möchten möglicherweise die Zeile bei der Verarbeitung markieren und teilen. Die Zeichenfolgenliste kann jetzt sehr große Textmengen enthalten.


1

Ein Iterator könnte für diese Art von Arbeit perfekt sein:

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

Sie können es wie folgt aufrufen:

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

Beim Laden der Datei gibt der Iterator die Fortschrittsnummer von 0 bis 100 zurück, mit der Sie Ihren Fortschrittsbalken aktualisieren können. Sobald die Schleife beendet ist, enthält der StringBuilder den Inhalt der Textdatei.

Da Sie Text möchten, können Sie auch BinaryReader zum Einlesen von Zeichen verwenden, um sicherzustellen, dass Ihre Puffer beim Lesen von Mehrbyte-Zeichen ( UTF-8 , UTF-16 usw.) korrekt ausgerichtet sind .

Dies alles geschieht ohne Hintergrundaufgaben, Threads oder komplexe benutzerdefinierte Zustandsautomaten.


Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.