web-dev-qa-db-ja.com

C#のストリームで大きなテキストファイルを読み取る

アプリケーションのスクリプトエディターに読み込まれている大きなファイルを処理する方法を考え出すという素晴らしいタスクがあります(クイックマクロ用の内部製品の VBA のようなものです)。ほとんどのファイルは約300〜400 KBで、正常にロードされます。ただし、100MBを超えると、プロセスに苦労します(予想どおり)。

何が起こるかというと、ファイルが読み取られ、RichTextBoxに押し込まれます。RichTextBoxはナビゲートされます。この部分についてはあまり心配しないでください。

初期コードを作成した開発者は、単にStreamReaderを使用して、

[Reader].ReadToEnd()

完了するまでにかなり時間がかかる場合があります。

私の仕事は、このコードの一部を分割し、チャンクでバッファに読み込み、キャンセルするオプションを備えたプログレスバーを表示することです。

いくつかの仮定:

  • ほとんどのファイルは30〜40 MBになります
  • ファイルの内容はテキスト(バイナリではなく)、一部はUnix形式、一部はDOSです。
  • 内容が取得されると、どのターミネーターが使用されているかがわかります。
  • Richtextboxでのレンダリングにかかる​​時間をロードすれば、誰も心配しません。それはテキストの最初のロードです。

質問のために:

  • StreamReaderを使用してから、Lengthプロパティ(ProgressMax)を確認し、設定されたバッファーサイズのReadを発行し、whileループで繰り返し処理できますWHILE while in the background worker tメインUIスレッドをブロックしますか?それが完了したら、stringbuilderをメインスレッドに返します。
  • 内容はStringBuilderに送られます。長さが利用可能な場合、ストリームのサイズでStringBuilderを初期化できますか?

これらは(専門家の意見では)良いアイデアですか? Streamsからコンテンツを読み取る際に過去にいくつかの問題が発生しました。これは常に最後の数バイトまたは何かが欠落するためですが、その場合は別の質問をします。

86
Nicole Lee

次のように、BufferedStreamを使用して読み取り速度を改善できます。

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

    }
}

2013年3月の更新

私は最近、1GBのようなテキストファイル(ここに含まれるファイルよりもはるかに大きい)を読み取り、処理(テキストを検索)するためのコードを作成し、プロデューサー/コンシューマーパターンを使用してパフォーマンスを大幅に向上させました。プロデューサータスクはBufferedStreamを使用してテキスト行を読み取り、検索を実行した別のコンシューマータスクに渡しました。

これをTPL Dataflowを学ぶ機会として使用しました。これは、このパターンをすばやくコーディングするのに非常に適しています。

BufferedStreamの方が速い理由

バッファは、データをキャッシュするために使用されるメモリ内のバイトのブロックであり、それによりオペレーティングシステムへの呼び出しの数を減らします。バッファーにより、読み取りおよび書き込みのパフォーマンスが向上します。バッファーは、読み取りまたは書き込みのいずれかに使用できますが、両方を同時に使用することはできません。 BufferedStreamのReadメソッドとWriteメソッドは、バッファーを自動的に維持します。

2014年12月の更新:マイレージは異なる場合があります

コメントに基づいて、FileStreamは内部で BufferedStream を使用する必要があります。この回答が最初に提供された時点で、BufferedStreamを追加することで大幅なパフォーマンスの向上を測定しました。当時、私は32ビットプラットフォームで.NET 3.xをターゲットにしていました。今日、64ビットプラットフォームで.NET 4.5をターゲットにした場合、改善は見られません。

関連

ASP.Net MVCアクションから生成された大規模なCSVファイルをResponseストリームにストリーミングするのが非常に遅い場合に遭遇しました。このインスタンスでBufferedStreamを追加すると、パフォーマンスが100倍向上しました。詳細については nbuffered Output Very Slow を参照してください

165
Eric J.

このWebサイトのパフォーマンスとベンチマークの統計 を読むと、 read への最速の方法がわかります(読み取り、書き込み、処理がすべてであるため)異なる)テキストファイルは次のコードスニペットです。

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

約9種類のメソッドがすべてベンチマーキングされましたが、ほとんどの場合、他のリーダーが言及しているように、バッファーリーダーを実行しても、1つが出てくるようです。

15
user4023224

大きなファイルの読み込み中に進行状況バーを表示するように求められたと言います。それは、ユーザーがファイルのロードの正確な%を本当に見たいからなのか、それとも単に何かが起こっているという視覚的なフィードバックが欲しいからなのか?

後者が当てはまる場合、ソリューションははるかに簡単になります。バックグラウンドスレッドでreader.ReadToEnd()を実行し、適切なものの代わりにマーキータイプのプログレスバーを表示します。

私の経験ではこれがよくあることなので、私はこの点を上げます。データ処理プログラムを作成している場合、ユーザーは間違いなく完全な数値に関心がありますが、単純だが遅いUI更新では、コンピューターがクラッシュしていないことを知りたいだけです。 :-)

14

バイナリファイルの場合、私が見つけた最速の読み取り方法はこれです。

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

私のテストでは、何百倍も高速です。

8
StainlessBeer

バックグラウンドワーカーを使用し、限られた数の行のみを読み取ります。詳細は、ユーザーがスクロールしたときにのみ読んでください。

また、ReadToEnd()を使用しないでください。これは、「なぜそれを実現したのか」と思う機能の1つです。それは script kiddies ' 小さなものでうまくいくヘルパーですが、あなたが見るように、それは大きなファイルを吸う...

StringBuilderを使用するように言っている人は、MSDNをより頻繁に読む必要があります。

パフォーマンスの考慮事項
ConcatメソッドとAppendFormatメソッドは両方とも、新しいデータを既存のStringまたはStringBuilderオブジェクトに連結します。 Stringオブジェクトの連結操作は、常に既存の文字列と新しいデータから新しいオブジェクトを作成します。 StringBuilderオブジェクトは、新しいデータの連結に対応するためのバッファーを維持します。空きがある場合は、バッファの最後に新しいデータが追加されます。それ以外の場合は、新しい大きなバッファーが割り当てられ、元のバッファーのデータが新しいバッファーにコピーされてから、新しいデータが新しいバッファーに追加されます。 StringまたはStringBuilderオブジェクトの連結操作のパフォーマンスは、メモリ割り当てが発生する頻度に依存します。
String連結操作は常にメモリを割り当てますが、StringBuilder連結操作はStringBuilderオブジェクトバッファが小さすぎて新しいデータを収容できない場合にのみメモリを割り当てます。したがって、固定数のStringオブジェクトが連結される場合、連結操作にはStringクラスが適しています。その場合、個々の連結操作は、コンパイラーによって単一の操作に結合されることさえあります。 StringBuilderオブジェクトは、任意の数の文字列が連結される場合の連結操作に適しています。たとえば、ループがユーザー入力のランダムな数の文字列を連結する場合

つまり、hugeメモリの割り当て、つまりスワップファイルシステムの大規模な使用、つまりハードディスクドライブのセクションがRAMメモリ、ただし、ハードディスクドライブは非常に遅いです。

StringBuilderオプションは、システムをモノユーザーとして使用するユーザーには適していますが、2人以上のユーザーが同時に大きなファイルを読み取る場合、問題が発生します。

6
Tufo

これで開始できます。

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

次のコードスニペットをご覧ください。 Most files will be 30-40 MBに言及しました。これは、Intel Quad Coreで1.4秒で180µMBを読み取ると主張しています。

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

オリジナル記事

4
James

here ..を処理するメモリマップドファイルを使用する方が良いかもしれません。 、したがって、p/invokesを使用して同じジョブを実行するこのラッパー..

Edit:MSDN の動作を確認するには、 blog エントリをご覧くださいリリースとしてリリースされる次期.NET 4でどのように行われるかを示します。前に示したリンクは、これを実現するためのピンボークのラッパーです。ファイル全体をメモリにマップし、ファイルをスクロールするときにスライドウィンドウのように表示できます。

3
t0mm13b

イテレータは、このタイプの作業に最適です。

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

次を使用して呼び出すことができます。

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

ファイルがロードされると、イテレーターは0から100までの進行状況番号を返します。これを使用して、進行状況バーを更新できます。ループが終了すると、StringBuilderにはテキストファイルの内容が含まれます。

また、テキストが必要なため、BinaryReaderを使用して文字を読み込むことができます。これにより、マルチバイト文字( TF-8TF -16 など)。

これはすべて、バックグラウンドタスク、スレッド、または複雑なカスタムステートマシンを使用せずに実行されます。

1
Extremeswank

私のファイルは13 GBを超えています: enter image description here

以下のリンクには、ファイルを簡単に読み取るコードが含まれています。

大きなテキストファイルを読み取る

詳細

0
Alireza

すべての優れた答え!ただし、答えを探している人にとっては、これらはやや不完全なように見えます。

標準的な文字列はサイズX、構成に応じて2Gbから4Gbのみであるため、これらの回答はOPの質問を実際に満たしません。 1つの方法は、文字列のリストを操作することです。

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

処理中にトークン化して行を分割したい場合があります。文字列リストに大量のテキストを含めることができるようになりました。

0
Rusty Nail