Odczytywanie dużych plików tekstowych ze strumieniami w języku C #


96

Mam cudowne zadanie, jak radzić sobie z dużymi plikami ładowanymi do edytora skryptów naszej aplikacji (to jest jak VBA dla naszego wewnętrznego produktu do szybkich makr). Większość plików ma około 300-400 KB, co jest dobrym ładowaniem. Ale kiedy przekraczają 100 MB, proces jest trudny (jak można się spodziewać).

Dzieje się tak, że plik jest odczytywany i umieszczany w RichTextBox, po którym jest nawigowany - nie przejmuj się zbytnio tą częścią.

Deweloper, który napisał początkowy kod, po prostu używa StreamReader i robi

[Reader].ReadToEnd()

co może zająć trochę czasu.

Moim zadaniem jest rozbicie tego fragmentu kodu, odczytanie go fragmentami do bufora i wyświetlenie paska postępu z opcją anulowania.

Niektóre założenia:

  • Większość plików ma rozmiar 30-40 MB
  • Zawartość pliku jest tekstowa (nie binarna), niektóre są w formacie uniksowym, a niektóre w systemie DOS.
  • Po pobraniu zawartości ustalamy, jaki terminator jest używany.
  • Nikt nie przejmuje się po załadowaniu czasu potrzebnego na renderowanie w bogatym polu tekstowym. To tylko wstępne ładowanie tekstu.

A teraz pytania:

  • Czy mogę po prostu użyć StreamReader, a następnie sprawdzić właściwość Length (czyli ProgressMax) i wydać Read dla ustawionego rozmiaru buforu i wykonać iterację w pętli while WHILST wewnątrz procesu roboczego w tle, aby nie blokował głównego wątku interfejsu użytkownika? Następnie po zakończeniu zwróć program budujący ciąg do głównego wątku.
  • Zawartość trafi do StringBuilder. czy mogę zainicjować StringBuilder z rozmiarem strumienia, jeśli długość jest dostępna?

Czy są to (Twoim zdaniem) dobre pomysły? W przeszłości miałem kilka problemów z czytaniem treści ze strumieni, ponieważ zawsze pomija ostatnie kilka bajtów lub coś w tym stylu, ale zadam inne pytanie, jeśli tak jest.


29
30-40 MB plików skryptów? Święta makrela! Nie chciałbym mieć przeglądu kodu, który ...
dthorpe

Wiem, że to pytanie jest dość stare, ale znalazłem je pewnego dnia i przetestowałem zalecenie dla MemoryMappedFile i jest to najszybsza metoda. Porównanie polega na tym, że odczyt pliku 7616939 linii 345 MB metodą readline zajmuje ponad 12 godzin na moim komputerze, podczas gdy wykonanie tego samego ładowania i odczytu za pomocą MemoryMappedFile zajęło 3 sekundy.
csonon

To tylko kilka linijek kodu. Zobacz tę bibliotekę, której używam do odczytu 25 GB i innych dużych plików. github.com/Agenty/FileReader
Vikash Rathee

Odpowiedzi:


175

Możesz poprawić prędkość odczytu, używając BufferedStream, na przykład:

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

    }
}

Aktualizacja z marca 2013 r

Niedawno napisałem kod do odczytu i przetwarzania (wyszukiwania tekstu) plików tekstowych o rozmiarze 1 GB (znacznie większych niż pliki tutaj zaangażowane) i osiągnąłem znaczny wzrost wydajności, używając wzorca producent / konsument. Zadanie producenta czytało wiersze tekstu za pomocą BufferedStreami przekazało je osobnemu zadaniu konsumenta, które przeprowadziło wyszukiwanie.

Wykorzystałem to jako okazję do nauczenia się TPL Dataflow, który bardzo dobrze nadaje się do szybkiego kodowania tego wzorca.

Dlaczego BufferedStream jest szybszy

Bufor to blok bajtów w pamięci służący do buforowania danych, co zmniejsza liczbę wywołań systemu operacyjnego. Bufory poprawiają wydajność odczytu i zapisu. Bufor może być używany do odczytu lub zapisu, ale nigdy do obu jednocześnie. Metody odczytu i zapisu BufferedStream automatycznie obsługują bufor.

AKTUALIZACJA z grudnia 2014 r .: Twój przebieg może się różnić

Na podstawie komentarzy FileStream powinien wewnętrznie używać BufferedStream . Kiedy po raz pierwszy podano tę odpowiedź, zmierzyłem znaczny wzrost wydajności, dodając BufferedStream. W tamtym czasie celowałem w .NET 3.x na platformie 32-bitowej. Dzisiaj, gdy celuję w .NET 4.5 na platformie 64-bitowej, nie widzę żadnej poprawy.

Związane z

Natknąłem się na przypadek, w którym przesyłanie strumieniowe dużego, wygenerowanego pliku CSV do strumienia odpowiedzi z akcji ASP.Net MVC było bardzo wolne. Dodanie BufferedStream poprawiło wydajność 100x w tym przypadku. Aby uzyskać więcej informacji, zobacz Niezbuforowane wyjście bardzo wolne


12
Koleś, BufferedStream robi różnicę. +1 :)
Marcus

2
Żądanie danych z podsystemu we / wy wiąże się z kosztami. W przypadku obracających się dysków może być konieczne poczekanie, aż talerz obróci się na miejsce, aby odczytać następny fragment danych lub, co gorsza, poczekać, aż głowica dysku się poruszy. Chociaż dyski SSD nie mają części mechanicznych, które spowalniają działanie, dostęp do nich nadal wiąże się z kosztem operacji we / wy. Buforowane strumienie odczytują więcej niż tylko żądania StreamReader, zmniejszając liczbę wywołań systemu operacyjnego i ostatecznie liczbę oddzielnych żądań we / wy.
Eric J.

4
Naprawdę? Nie ma to znaczenia w moim scenariuszu testowym. Według Brada Abramsa nie ma korzyści z używania BufferedStream zamiast FileStream.
Nick Cox

2
@NickCox: Twoje wyniki mogą się różnić w zależności od podstawowego podsystemu IO. Na obracającym się dysku i kontrolerze dysku, który nie ma danych w swojej pamięci podręcznej (a także danych, które nie są buforowane przez system Windows), przyspieszenie jest ogromne. Kolumna Brada została napisana w 2004 roku. Ostatnio zmierzyłem rzeczywistą, drastyczną poprawę.
Eric J.,

3
Jest to bezużyteczne według: stackoverflow.com/questions/492283/… FileStream już używa wewnętrznego bufora.
Erwin Mayer,

21

Jeśli przeczytasz statystyki wydajności i testów porównawczych w tej witrynie , zobaczysz, że najszybszym sposobem odczytania (ponieważ czytanie, pisanie i przetwarzanie są różne) pliku tekstowego jest następujący fragment kodu:

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

W sumie około 9 różnych metod zostało oznaczonych jako benchmark, ale ta wydaje się być lepsza przez większość czasu, nawet w przypadku czytelnika buforowanego, jak wspominali inni czytelnicy.


2
To działało dobrze przy rozbieraniu pliku postgres o rozmiarze 19 GB w celu przetłumaczenia go na składnię sql w wielu plikach. Dzięki postgres facetowi, który nigdy nie wykonał poprawnie moich parametrów. / westchnienie
Damon Drake

Różnica w wydajności wydaje się opłacać w przypadku naprawdę dużych plików, takich jak większe niż 150 MB (również naprawdę powinieneś użyć a StringBuilderdo ładowania ich do pamięci, ładuje się szybciej, ponieważ nie tworzy nowego ciągu za każdym razem, gdy dodajesz znaki)
Joshua G

15

Mówisz, że zostałeś poproszony o wyświetlenie paska postępu podczas ładowania dużego pliku. Czy to dlatego, że użytkownicy naprawdę chcą zobaczyć dokładny procent ładowania plików, czy po prostu dlatego, że chcą wizualnej informacji zwrotnej, że coś się dzieje?

Jeśli to ostatnie jest prawdą, rozwiązanie staje się znacznie prostsze. Po prostu zrób reader.ReadToEnd()to w wątku w tle i wyświetlaj pasek postępu typu marquee zamiast prawidłowego.

Podnoszę tę kwestię, ponieważ z mojego doświadczenia wynika, że ​​często tak jest. Kiedy piszesz program do przetwarzania danych, użytkownicy z pewnością będą zainteresowani% pełną liczbą, ale w przypadku prostych, ale powolnych aktualizacji interfejsu użytkownika bardziej prawdopodobne jest, że będą chcieli po prostu wiedzieć, że komputer się nie zawiesił. :-)


2
Ale czy użytkownik może anulować wywołanie ReadToEnd?
Tim Scarborough,

@Tim, dobrze zauważony. W takim razie wracamy do StreamReaderpętli. Jednak nadal będzie to prostsze, ponieważ nie trzeba czytać z wyprzedzeniem, aby obliczyć wskaźnik postępu.
Christian Hayter

8

W przypadku plików binarnych najszybszym sposobem ich odczytania jest to.

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

W moich testach jest setki razy szybszy.


2
Czy masz jakieś mocne dowody na to? Dlaczego OP miałby używać tego zamiast jakiejkolwiek innej odpowiedzi? Proszę poszukać trochę głębiej i podać więcej szczegółów
Dylan Corriveau

7

Użyj pracownika w tle i czytaj tylko ograniczoną liczbę wierszy. Czytaj więcej tylko wtedy, gdy użytkownik przewija.

I staraj się nigdy nie używać ReadToEnd (). Jest to jedna z funkcji, o której myślisz „dlaczego to zrobili?”; to pomocnik skryptów dla dzieciaków, który pasuje do małych rzeczy, ale jak widzisz, jest do bani w przypadku dużych plików ...

Ci faceci, którzy mówią ci, żebyś używał StringBuilder, muszą częściej czytać MSDN:

Zagadnienia dotyczące wydajności
Metody Concat i AppendFormat łączą nowe dane z istniejącym obiektem String lub StringBuilder. Operacja konkatenacji obiektu typu String zawsze tworzy nowy obiekt na podstawie istniejącego ciągu i nowych danych. Obiekt StringBuilder utrzymuje bufor, aby pomieścić konkatenację nowych danych. Nowe dane są dodawane na końcu bufora, jeśli miejsce jest dostępne; w przeciwnym razie przydzielany jest nowy, większy bufor, dane z oryginalnego bufora są kopiowane do nowego bufora, a następnie nowe dane są dołączane do nowego bufora. Wydajność operacji konkatenacji dla obiektu String lub StringBuilder zależy od tego, jak często występuje alokacja pamięci.
Operacja konkatenacji String zawsze przydziela pamięć, podczas gdy operacja konkatenacji StringBuilder przydziela pamięć tylko wtedy, gdy bufor obiektu StringBuilder jest zbyt mały, aby pomieścić nowe dane. W związku z tym klasa String jest preferowana w przypadku operacji konkatenacji, jeśli łączona jest stała liczba obiektów String. W takim przypadku poszczególne operacje konkatenacji mogą nawet zostać połączone w jedną operację przez kompilator. Obiekt StringBuilder jest preferowany w przypadku operacji konkatenacji, jeśli łączona jest dowolna liczba ciągów; na przykład, jeśli pętla łączy losową liczbę ciągów danych wejściowych użytkownika.

Oznacza to ogromną alokację pamięci, co staje się dużym wykorzystaniem systemu plików wymiany, który symuluje sekcje dysku twardego, aby zachowywały się jak pamięć RAM, ale dysk twardy jest bardzo wolny.

Opcja StringBuilder wygląda dobrze dla tych, którzy używają systemu jako pojedynczy użytkownik, ale jeśli masz dwóch lub więcej użytkowników czytających duże pliki w tym samym czasie, masz problem.


daleko, jesteście super szybcy! niestety ze względu na sposób działania makra trzeba załadować cały strumień. Jak wspomniałem, nie przejmuj się częścią tekstową. To początkowe ładowanie, które chcemy ulepszyć.
Nicole Lee,

więc możesz pracować w częściach, czytać pierwsze X wierszy, zastosować makro, przeczytać drugie X wierszy, zastosować makro itd ... jeśli wyjaśnisz, co robi to makro, możemy Ci pomóc z większą precyzją
Tufo

5

To powinno wystarczyć, aby zacząć.

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
Usunąłbym z pętli "var buffer = new char [1024]": nie jest konieczne tworzenie nowego bufora za każdym razem. Po prostu wstaw go przed „while (count> 0)”.
Tommy Carlier

4

Spójrz na następujący fragment kodu. Wspomniałeś Most files will be 30-40 MB. To twierdzi, że odczytuje 180 MB w 1,4 sekundy na 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);
        }
    }
}

Oryginalny artykuł


3
Tego rodzaju testy są notorycznie niewiarygodne. Po powtórzeniu testu odczytasz dane z pamięci podręcznej systemu plików. To co najmniej jeden rząd wielkości szybciej niż rzeczywisty test, który odczytuje dane z dysku. Plik 180 MB nie może zająć mniej niż 3 sekundy. Zrestartuj swoją maszynę, przeprowadź test raz dla prawdziwej liczby.
Hans Passant

7
linia stringBuilder.Append jest potencjalnie niebezpieczna, musisz ją zamienić na stringBuilder.Append (fileContents, 0, charsRead); aby upewnić się, że nie dodajesz pełnych 1024 znaków, nawet jeśli strumień zakończył się wcześniej.
Johannes Rudolph

@JohannesRudolph, Twój komentarz właśnie rozwiązał problem. Jak wpadłeś na numer 1024?
OfirD

3

Lepiej byłoby skorzystać z obsługi plików mapowanych w pamięci tutaj .. Obsługa plików mapowanych w pamięci będzie dostępna w .NET 4 (myślę ... słyszałem, że ktoś o tym mówi), stąd ten wrapper, który używa p / wywołuje tę samą pracę.

Edycja: Zobacz tutaj w MSDN, jak to działa, oto wpis na blogu wskazujący, jak to się robi w nadchodzącej .NET 4, gdy pojawi się jako wydanie. Link, który podałem wcześniej, jest opakowaniem wokół pinvoke, aby to osiągnąć. Możesz zmapować cały plik do pamięci i przeglądać go jak przesuwne okno podczas przewijania pliku.


2

Wszystkie doskonałe odpowiedzi! Jednak dla kogoś, kto szuka odpowiedzi, wydaje się, że są one nieco niekompletne.

Standardowo łańcuch może mieć tylko rozmiar X, od 2 Gb do 4 Gb, w zależności od konfiguracji, te odpowiedzi tak naprawdę nie odpowiadają na pytanie OP. Jedną z metod jest praca z listą ciągów:

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

Niektórzy mogą chcieć tokenizować i dzielić linię podczas przetwarzania. Lista ciągów może teraz zawierać bardzo duże ilości tekstu.


1

Iterator może być idealny do tego typu pracy:

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

Możesz to nazwać za pomocą:

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

Po załadowaniu pliku iterator zwróci numer postępu od 0 do 100, którego możesz użyć do zaktualizowania paska postępu. Po zakończeniu pętli StringBuilder będzie zawierał zawartość pliku tekstowego.

Ponadto, ponieważ potrzebujesz tekstu, możemy po prostu użyć BinaryReader do czytania znakami, co zapewni prawidłowe wyrównanie buforów podczas odczytu dowolnych znaków wielobajtowych ( UTF-8 , UTF-16 itp.).

Wszystko to odbywa się bez korzystania z zadań w tle, wątków lub złożonych niestandardowych maszyn stanu.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.