C # 'ta akışlı büyük metin dosyalarını okuma


97

Uygulamamızın komut dosyası düzenleyicisine yüklenen büyük dosyaların nasıl işleneceğini bulmak gibi güzel bir görevim var ( hızlı makrolar için dahili ürünümüz için VBA gibi ). Çoğu dosya yaklaşık 300-400 KB boyutundadır ve bu iyi bir yükleme anlamına gelir. Ancak 100 MB'ın üzerine çıktıklarında süreç zor anlar yaşar (beklediğiniz gibi).

Olan şey, dosyanın okunması ve daha sonra gezinilecek olan bir RichTextBox'a taşınmasıdır - bu bölüm için çok fazla endişelenmeyin.

İlk kodu yazan geliştirici sadece bir StreamReader kullanıyor ve

[Reader].ReadToEnd()

tamamlanması biraz zaman alabilir.

Benim görevim, bu kod parçasını parçalamak, parçalar halinde bir arabelleğe okumak ve iptal etme seçeneği olan bir ilerleme çubuğu göstermektir.

Bazı varsayımlar:

  • Çoğu dosya 30-40 MB olacaktır
  • Dosyanın içeriği metindir (ikili değil), bazıları Unix formatı, bazıları DOS.
  • İçerikler alındıktan sonra, hangi sonlandırıcının kullanıldığını hesaplıyoruz.
  • Hiç kimse bir kez yüklendiğinde zengin metin kutusunda işlemek için gereken süreyi düşünmez. Bu sadece metnin ilk yüklemesi.

Şimdi sorular için:

  • StreamReader'ı basitçe kullanabilir miyim, sonra Length özelliğini (yani ProgressMax) kontrol edebilir ve bir set arabellek boyutu için Read yayınlayabilir ve bir while döngüsü içinde WHILST içinde yineleme yapabilir miyim , böylece ana UI iş parçacığını engellemesin? Ardından, tamamlandığında dizgi oluşturucuyu ana iş parçacığına döndürün.
  • İçerik bir StringBuilder'a gidecek. Uzunluk mevcutsa StringBuilder'ı akışın boyutuyla başlatabilir miyim?

Bunlar (profesyonel görüşlerinize göre) iyi fikirler mi? Geçmişte Akışlardan içerik okumakla ilgili birkaç sorun yaşadım çünkü her zaman son birkaç baytı veya başka bir şeyi kaçıracak, ancak durum buysa başka bir soru soracağım.


29
30-40MB komut dosyası dosyaları? Kutsal uskumru! Bu incelemeyi kodlamak zorunda kalmaktan nefret ederim ...
dthorpe

Bu soruların oldukça eski olduğunu biliyorum ama geçen gün buldum ve MemoryMappedFile için öneriyi test ettim ve bu en hızlı yöntem. Bir karşılaştırma, bir 7.616.939 satır 345MB dosyasını okuma hattı yöntemiyle okumak, makinemde aynı yükü gerçekleştirirken 12+ saat sürüyor ve MemoryMappedFile ile okuma 3 saniye sürdü.
csonon

Sadece birkaç satır kod. 25gb ve daha büyük dosyaları okumak için kullandığım bu kütüphaneye bakın. github.com/Agenty/FileReader
Vikash Rathee

Yanıtlar:


177

BufferedStream kullanarak okuma hızını artırabilirsiniz, örneğin:

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

    }
}

Mart 2013 GÜNCELLEME

Yakın zamanda 1 GB'lık metin dosyalarını (burada yer alan dosyalardan çok daha büyük olan) okumak ve işlemek için kod yazdım ve bir üretici / tüketici modeli kullanarak önemli bir performans kazancı elde ettim. Yapımcı görevi kullanarak metin satırlarını okudu BufferedStreamve aramayı yapan ayrı bir tüketici görevine verdi.

Bunu, bu kalıbı hızlı bir şekilde kodlamak için çok uygun olan TPL Dataflow'u öğrenmek için bir fırsat olarak kullandım.

BufferedStream neden daha hızlıdır?

Arabellek, verileri önbelleğe almak için kullanılan bellekteki bir bayt bloğudur, böylece işletim sistemine gelen çağrıların sayısını azaltır. Tamponlar okuma ve yazma performansını iyileştirir. Bir tampon hem okuma hem de yazma için kullanılabilir, ancak asla aynı anda ikisi birden kullanılamaz. BufferedStream'in Okuma ve Yazma yöntemleri, arabelleği otomatik olarak korur.

Aralık 2014 GÜNCELLEME: Kilometreniz Değişebilir

Yorumlara bağlı olarak, FileStream dahili olarak bir BufferedStream kullanıyor olmalıdır . Bu cevabın ilk kez verildiği sırada, Tamponlu Akış ekleyerek önemli bir performans artışı ölçtüm. O zamanlar 32 bit platformda .NET 3.x'i hedefliyordum. Bugün 64 bitlik bir platformda .NET 4.5'i hedef aldığımda herhangi bir gelişme görmüyorum.

İlişkili

ASP.Net MVC eyleminden Yanıt akışına büyük, oluşturulmuş bir CSV dosyası akışının çok yavaş olduğu bir durumla karşılaştım. Bir BufferedStream eklemek, bu örnekte performansı 100 kat artırdı. Daha fazla bilgi için Arabelleğe Alınmamış Çıktı Çok Yavaş bakın


12
Dostum, BufferedStream tüm farkı yaratıyor. +1 :)
Marcus

2
Bir IO alt sisteminden veri talep etmenin bir maliyeti vardır. Disklerin dönmesi durumunda, tabağın bir sonraki veri yığınını okumak için dönmesini beklemeniz veya daha kötüsü disk kafasının hareket etmesini beklemeniz gerekebilir. SSD'lerin işleri yavaşlatacak mekanik parçaları olmasa da, bunlara erişmek için hala IO başına bir maliyet vardır. Arabelleğe alınan akışlar, StreamReader'ın talep ettiklerinden daha fazlasını okur, işletim sistemine yapılan çağrıların sayısını ve nihayetinde ayrı IO isteklerinin sayısını azaltır.
Eric J.

4
Gerçekten mi? Bu benim test senaryomda hiçbir fark yaratmıyor. Brad Abrams'a göre BufferedStream'i bir FileStream üzerinden kullanmanın bir faydası yok.
Nick Cox

3
@NickCox: Sonuçlarınız, temel IO alt sisteminize göre değişebilir. Dönen bir diskte ve önbelleğinde verilere (ve ayrıca Windows tarafından önbelleğe alınmayan verilere) sahip olmayan bir disk denetleyicisinde hızlanma çok büyüktür. Brad'in köşesi 2004'te yazıldı. Son zamanlarda gerçek, önemli gelişmeler ölçtüm.
Eric J.

3
Şuna göre bu işe yaramaz: stackoverflow.com/questions/492283/… FileStream zaten dahili olarak bir arabellek kullanıyor.
Erwin Mayer

22

Eğer okursanız bu web sitesinde performans ve karşılaştırma istatistiklerini , en hızlı yolu olduğunu göreceksiniz okuma (çünkü okuma, yazma, ve işleme tüm farklı olabilir) bir metin dosyası aşağıdaki kod snippet'idir:

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

Yaklaşık 9 farklı yöntemin tümü karşılaştırmalı olarak işaretlendi, ancak bu , diğer okuyucuların da bahsettiği gibi tamponlu okuyucuyu gerçekleştirirken bile çoğu zaman öne çıkıyor gibi görünüyor .


2
Bu, 19 GB'lık bir postgres dosyasını birden çok dosyada sql sözdizimine çevirmek için ayırmak için iyi çalıştı. Parametrelerimi asla doğru bir şekilde uygulamayan postgres adama teşekkürler. / sigh
Damon Drake

Buradaki performans farkı, 150MB'den büyük dosyalar gibi gerçekten büyük dosyalar için karşılığını veriyor gibi görünüyor (ayrıca StringBuilderbunları belleğe yüklemek için gerçekten a kullanmalısınız , her karakter eklediğinizde yeni bir dize oluşturmadığı için daha hızlı yüklenir)
Joshua G

15

Büyük bir dosya yüklenirken bir ilerleme çubuğu göstermenizin istendiğini söylüyorsunuz. Bunun nedeni, kullanıcıların gerçekten dosya yüklemesinin tam yüzdesini görmek istemeleri mi yoksa sadece bir şeyler olduğuna dair görsel geri bildirim istemeleri mi?

İkincisi doğruysa, çözüm çok daha basit hale gelir. Sadece reader.ReadToEnd()bir arka plan iş parçacığı üzerinde yapın ve uygun olan yerine seçim çerçevesi tipi bir ilerleme çubuğu görüntüleyin.

Bu noktayı yükseltiyorum çünkü tecrübelerime göre bu genellikle böyledir. Bir veri işleme programı yazarken, kullanıcılar kesinlikle% tam bir rakamla ilgileneceklerdir, ancak basit ama yavaş UI güncellemeleri için, bilgisayarın çökmediğini bilmek istemeleri daha olasıdır. :-)


2
Ancak kullanıcı ReadToEnd çağrısını iptal edebilir mi?
Tim Scarborough

@Tim, iyi görüldü. Bu durumda, StreamReaderdöngüye geri döndük . Bununla birlikte, ilerleme göstergesini hesaplamak için okumaya gerek olmadığı için yine de daha basit olacaktır.
Christian Hayter

8

İkili dosyalar için bulduğum en hızlı okuma yolu bu.

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

Testlerimde yüzlerce kat daha hızlı.


2
Bunun hakkında kesin kanıtınız var mı? OP neden bunu başka bir yanıt yerine kullanmalı? Lütfen biraz daha derine
inin

7

Bir arka plan çalışanı kullanın ve yalnızca sınırlı sayıda satırı okuyun. Yalnızca kullanıcı kaydırdığında daha fazlasını okuyun.

Ve asla ReadToEnd () kullanmamaya çalışın. "Neden başardılar?" Diye düşündüğünüz işlevlerden biri; küçük şeylerle iyi giden bir senaryo çocuk yardımcısı, ama gördüğünüz gibi, büyük dosyalar için berbat ...

Size StringBuilder'ı kullanmanızı söyleyenlerin MSDN'yi daha sık okumaları gerekir:

Performansı Etkileyen Koşullar
Concat ve AppendFormat yöntemlerinin her ikisi de yeni verileri var olan bir String veya StringBuilder nesnesine birleştirir. Bir String nesnesi birleştirme işlemi her zaman mevcut dizeden ve yeni verilerden yeni bir nesne oluşturur. Bir StringBuilder nesnesi, yeni verilerin birleştirilmesini barındırmak için bir arabellek tutar. Oda varsa, arabelleğin sonuna yeni veriler eklenir; aksi takdirde, yeni, daha büyük bir arabellek tahsis edilir, orijinal arabellekteki veriler yeni arabelleğe kopyalanır, ardından yeni veriler yeni arabelleğe eklenir. Bir String veya StringBuilder nesnesi için birleştirme işleminin performansı, bellek ayırmanın ne sıklıkla gerçekleştiğine bağlıdır.
Bir String birleştirme işlemi her zaman bellek ayırırken, StringBuilder birleştirme işlemi yalnızca StringBuilder nesne arabelleği yeni verileri barındırmak için çok küçükse belleği ayırır. Sonuç olarak, sabit sayıda String nesnesi birleştirilirse, String sınıfı bir birleştirme işlemi için tercih edilir. Bu durumda, ayrı birleştirme işlemleri, derleyici tarafından tek bir işlemde birleştirilebilir. Bir StringBuilder nesnesi, rastgele sayıda dize birleştirilirse, birleştirme işlemi için tercih edilir; örneğin, bir döngü rastgele sayıda kullanıcı girdisi dizesini birleştirirse.

Bunun anlamı çok büyük bellek tahsisi, takas dosyaları sisteminin büyük kullanımını olur, ne sabit disk sürücüsünün Simülasyonu yapılan bölümleri RAM belleğe gibi davranmaya, ama bir sabit disk sürücüsü çok yavaştır.

StringBuilder seçeneği, sistemi tek kullanıcı olarak kullananlar için iyi görünüyor, ancak aynı anda büyük dosyaları okuyan iki veya daha fazla kullanıcınız olduğunda, bir sorununuz var.


uzakta sizler süper hızlısınız! ne yazık ki, makronun çalışma şekli nedeniyle tüm akışın yüklenmesi gerekiyor. Bahsettiğim gibi zengin metin kısmı için endişelenmeyin. Geliştirmek istediğimiz ilk yükleme bu.
Nicole Lee

böylelikle parçalar halinde çalışabilir, ilk X satırlarını okuyabilir, makroyu uygulayabilir, ikinci X satırlarını okuyabilir, makroyu uygulayabilirsiniz ve benzeri ... Bu makronun ne yaptığını açıklarsanız, size daha kesin bir şekilde yardımcı olabiliriz
Tufo

5

Başlaman için bu yeterli olmalı.

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
"Var buffer = new char [1024]" değerini döngüden çıkarırdım: her seferinde yeni bir tampon oluşturmak gerekli değildir. "While (count> 0)" dan önce koy.
Tommy Carlier

4

Aşağıdaki kod parçacığına bir göz atın. Bahsettiniz Most files will be 30-40 MB. Bu, Intel Dört Çekirdekte 180 MB'yi 1,4 saniyede okur:

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

Orijinal makale


3
Bu tür testler herkesin bildiği gibi güvenilmezdir. Testi tekrar ettiğinizde dosya sistemi önbelleğinden verileri okuyacaksınız. Bu, verileri diskten okuyan gerçek bir testten en az bir kat daha hızlıdır. 180 MB'lık bir dosya muhtemelen 3 saniyeden az süremez. Makinenizi yeniden başlatın, gerçek sayı için testi bir kez çalıştırın.
Hans Passant

7
stringBuilder.Append satırı potansiyel olarak tehlikelidir, bunu stringBuilder.Append (fileContents, 0, charsRead) ile değiştirmeniz gerekir; Akış daha önce bitmiş olsa bile tam 1024 karakter eklemediğinizden emin olmak için.
Johannes Rudolph

@JohannesRudolph, yorumunuz az önce bir hatayı çözdü. 1024 sayısını nasıl buldunuz?
OfirD

3

Burada bellek eşlemeli dosyaların işlenmesini kullanmanız daha iyi olabilir .. Bellek eşlemeli dosya desteği .NET 4'te olacaktır (sanırım ... bunu başka birinin konuşmasından duydum), dolayısıyla p / aynı işi yapmaya çağırır ..

Düzenleme: Nasıl çalıştığını öğrenmek için MSDN'de buraya bakın , burada , yayın olarak çıktığında yakında çıkacak .NET 4'te nasıl yapıldığını gösteren blog girişi. Daha önce verdiğim bağlantı, bunu başarmak için pinvoke'un etrafındaki bir sarmalayıcıdır. Dosyanın tamamını belleğe eşleyebilir ve dosya içinde gezinirken kayan bir pencere gibi görüntüleyebilirsiniz.


3

Hepsi mükemmel cevaplar! ancak, cevap arayan biri için bunlar bir şekilde eksik görünmektedir.

Standart bir String, yapılandırmanıza bağlı olarak yalnızca Boyut X, 2Gb ila 4Gb arasında olabileceğinden, bu yanıtlar OP'nin sorusunu gerçekten karşılamıyor. Yöntemlerden biri, Dizelerin Listesi ile çalışmaktır:

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

Bazıları, işlem sırasında hattı belirtmek ve bölmek isteyebilir. Dize Listesi artık çok büyük hacimlerde Metin içerebilir.


1

Bir yineleyici, bu tür işler için mükemmel olabilir:

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

Aşağıdakileri kullanarak arayabilirsiniz:

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

Dosya yüklendikçe, yineleyici ilerleme numarasını 0'dan 100'e döndürür ve ilerleme çubuğunuzu güncellemek için kullanabilirsiniz. Döngü bittiğinde, StringBuilder metin dosyasının içeriğini içerecektir.

Ayrıca, metin istediğiniz için, karakterleri okumak için BinaryReader'ı kullanabiliriz, bu da çok baytlı karakterleri ( UTF-8 , UTF-16 , vb.) Okurken arabelleklerinizin doğru şekilde sıralanmasını sağlar .

Bunların hepsi arka plan görevleri, iş parçacıkları veya karmaşık özel durum makineleri kullanılmadan yapılır.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.