C #에서 스트림을 사용하여 큰 텍스트 파일 읽기


96

응용 프로그램의 스크립트 편집기에로드되는 대용량 파일을 처리하는 방법을 알아내는 멋진 작업이 있습니다 ( 빠른 매크로를위한 내부 제품의 VBA 와 같습니다 ). 대부분의 파일은 약 300-400KB로 잘로드됩니다. 그러나 100MB를 초과하면 프로세스에 어려움이 있습니다 (예상대로).

무슨 일이 일어나고 있는지 파일을 읽고 RichTextBox로 밀어 넣은 다음 탐색합니다.이 부분에 대해 너무 걱정하지 마십시오.

초기 코드를 작성한 개발자는 단순히 StreamReader를 사용하여

[Reader].ReadToEnd()

완료하는 데 시간이 꽤 걸릴 수 있습니다.

내 작업은이 코드를 분할하고, 청크 단위로 버퍼로 읽고, 취소 옵션이있는 진행률 표시 줄을 표시하는 것입니다.

몇 가지 가정 :

  • 대부분의 파일은 30-40MB입니다.
  • 파일의 내용은 텍스트 (바이너리 아님)이고 일부는 Unix 형식이고 일부는 DOS입니다.
  • 내용이 검색되면 어떤 터미네이터가 사용되는지 알아냅니다.
  • 리치 텍스트 상자에서 렌더링하는 데 걸리는 시간이로드되면 아무도 걱정하지 않습니다. 텍스트의 초기로드 일뿐입니다.

이제 질문 :

  • StreamReader를 사용한 다음 Length 속성 (ProgressMax)을 확인하고 설정된 버퍼 크기에 대해 Read를 실행 하고 백그라운드 작업자 내부에서 WHILST 를 반복 하여 기본 UI 스레드를 차단하지 않도록 할 수 있습니까? 그런 다음 stringbuilder가 완료되면 메인 스레드로 반환합니다.
  • 내용은 StringBuilder로 이동합니다. 길이를 사용할 수있는 경우 스트림 크기로 StringBuilder를 초기화 할 수 있습니까?

(전문적인 의견으로는) 좋은 아이디어입니까? 이전에 Streams에서 콘텐츠를 읽는 데 몇 가지 문제가 있었는데, 항상 마지막 몇 바이트 또는 무언가를 놓칠 것이기 때문입니다. 그러나 이것이 사실이라면 다른 질문을 할 것입니다.


29
30-40MB 스크립트 파일? 이런 고등어! 나는 그것을 코드 검토해야 싫다 ...
dthorpe 2010

나는이 질문이 다소 오래되었다는 것을 알고 있지만 요 전에 그것을 발견하고 MemoryMappedFile에 대한 권장 사항을 테스트했으며 이것이 가장 빠른 방법입니다. 비교는 readline 메소드를 통해 7,616,939 행 345MB 파일을 읽는 것입니다. 동일한로드를 수행하고 MemoryMappedFile을 통해 읽는 데 3 초가 소요되는 동안 내 컴퓨터에서 12 시간 이상이 걸립니다.
csonon

단지 몇 줄의 코드 일뿐입니다. 25GB 및 더 큰 파일을 읽는 데 사용중인이 라이브러리를 참조하십시오. github.com/Agenty/FileReader
Vikash Rathee

답변:


175

다음과 같이 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를 대상으로했지만 개선되지 않았습니다.

관련

생성 된 대용량 CSV 파일을 ASP.Net MVC 작업에서 응답 스트림으로 스트리밍하는 것이 매우 느린 경우를 발견했습니다. 이 인스턴스에서 BufferedStream을 추가하면 성능이 100 배 향상되었습니다. 자세한 내용은 버퍼링되지 않은 출력 매우 느림을 참조하십시오.


12
야, BufferedStream이 모든 차이를 만듭니다. +1 :)
Marcus

2
IO 하위 시스템에서 데이터를 요청하는 데는 비용이 발생합니다. 회전 디스크의 경우 다음 데이터 청크를 읽기 위해 플래터가 제 위치로 회전 할 때까지 기다리거나 디스크 헤드가 움직일 때까지 기다려야 할 수 있습니다. SSD에는 속도를 늦추는 기계 부품이 없지만 액세스하는 데는 여전히 IO 당 운영 비용이 있습니다. 버퍼링 된 스트림은 StreamReader가 요청하는 것 이상을 읽어 OS에 대한 호출 수와 궁극적으로 개별 IO 요청 수를 줄입니다.
Eric J.

4
정말? 이것은 내 테스트 시나리오에서 차이가 없습니다. Brad Abrams 에 따르면 FileStream을 통해 BufferedStream을 사용하는 것에는 이점이 없습니다.
Nick Cox

2
@NickCox : 결과는 기본 IO 하위 시스템에 따라 다를 수 있습니다. 회전 디스크와 캐시에 데이터가없는 디스크 컨트롤러 (및 Windows에서 캐시되지 않은 데이터)에서는 속도가 엄청납니다. Brad의 칼럼은 2004 년에 작성되었습니다. 저는 최근에 실제적이고 과감한 개선을 측정했습니다.
Eric J.

3
이것은 다음과 같이 쓸모가 없습니다. stackoverflow.com/questions/492283/… FileStream은 이미 내부적으로 버퍼를 사용합니다.
Erwin Mayer

21

이 웹 사이트 에서 성능 및 벤치 마크 통계읽으면 텍스트 파일 을 읽는 가장 빠른 방법 (읽기, 쓰기 및 처리가 모두 다르기 때문에)이 다음 코드 스 니펫임을 알 수 있습니다.

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

약 9 개의 다른 방법이 모두 벤치마킹되었지만 다른 독자들이 언급 한 것처럼 버퍼링 된 리더수행하는 경우에도 대부분의 경우 앞서 나온 것 같습니다 .


2
이것은 19GB postgres 파일을 분리하여 여러 파일의 SQL 구문으로 변환하는 데 효과적이었습니다. 내 매개 변수를 올바르게 실행하지 않은 postgres 사람에게 감사드립니다. / sigh
Damon Drake

여기에서 성능 차이는 150MB보다 큰 파일과 같이 정말 큰 파일에 대해 보상을받는 것 같습니다 (또한 파일 StringBuilder을 메모리에로드하는 데 a를 사용해야합니다. 문자를 추가 할 때마다 새 문자열을 만들지 않기 때문에로드 속도가 빠름)
Joshua G

15

큰 파일이로드되는 동안 진행률 표시 줄을 표시하라는 요청을 받았다고합니다. 사용자가 진정으로 파일 로딩의 정확한 %를보고 싶어하기 때문입니까, 아니면 어떤 일이 일어나고 있다는 시각적 피드백을 원하기 때문입니까?

후자가 사실이면 솔루션이 훨씬 더 간단 해집니다. 그냥 할 reader.ReadToEnd()백그라운드 스레드에서, 대신 적절한 하나의 윤곽 형 진행률 표시 줄을 표시합니다.

제 경험상 이런 경우가 많기 때문에이 점을 올립니다. 데이터 처리 프로그램을 작성할 때 사용자는 확실히 % 완성도에 관심이 있지만, 단순하지만 느린 UI 업데이트의 경우 컴퓨터가 충돌하지 않았 음을 알고 싶어 할 가능성이 더 높습니다. :-)


2
하지만 사용자가 ReadToEnd 호출을 취소 할 수 있습니까?
Tim Scarborough

@ 팀, 잘 발견되었습니다. 이 경우 StreamReader루프로 돌아갑니다 . 그러나 진행률 표시기를 계산하기 위해 미리 읽을 필요가 없기 때문에 더 간단합니다.
Christian Hayter

8

바이너리 파일의 경우 내가 찾은 가장 빠른 방법은 이것입니다.

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

내 테스트에서는 수백 배 더 빠릅니다.


2
이것에 대한 확실한 증거가 있습니까? OP가 다른 답변보다 이것을 사용해야하는 이유는 무엇입니까? 깊은 약간을 파고 약간에게 자세한 내용을주십시오
딜런 Corriveau

7

백그라운드 작업자를 사용하고 제한된 수의 줄만 읽습니다. 사용자가 스크롤 할 때만 자세히 읽어보십시오.

그리고 ReadToEnd ()를 사용하지 마십시오. 그것은 당신이 생각하는 기능 중 하나입니다. "왜 그들이 그것을 만들었습니까?"; 그것은의 스크립트 키디 ' 작은 것들로 잘 간다 도우미,하지만 당신이 볼, 그것은 큰 파일 짜증 ...

StringBuilder를 사용하라고 말하는 사람들은 MSDN을 더 자주 읽어야합니다.

성능 고려 사항
Concat 및 AppendFormat 메서드는 모두 새 데이터를 기존 String 또는 StringBuilder 개체에 연결합니다. 문자열 개체 연결 작업은 항상 기존 문자열과 새 데이터에서 새 개체를 만듭니다. StringBuilder 개체는 새 데이터의 연결을 수용하기 위해 버퍼를 유지합니다. 공간을 사용할 수있는 경우 새 데이터가 버퍼 끝에 추가됩니다. 그렇지 않으면 새롭고 더 큰 버퍼가 할당되고 원래 버퍼의 데이터가 새 버퍼로 복사 된 다음 새 데이터가 새 버퍼에 추가됩니다. String 또는 StringBuilder 개체에 대한 연결 작업의 성능은 메모리 할당이 발생하는 빈도에 따라 다릅니다.
문자열 연결 작업은 항상 메모리를 할당하는 반면, StringBuilder 연결 작업은 StringBuilder 개체 버퍼가 너무 작아서 새 데이터를 수용 할 수없는 경우에만 메모리를 할당합니다. 따라서 고정 된 수의 String 개체가 연결되는 경우 연결 작업에 String 클래스가 더 적합합니다. 이 경우 개별 연결 작업은 컴파일러에 의해 단일 작업으로 결합 될 수도 있습니다. 임의의 수의 문자열이 연결되는 경우 연결 작업에 StringBuilder 개체가 선호됩니다. 예를 들어 루프가 임의의 수의 사용자 입력 문자열을 연결하는 경우입니다.

즉 , RAM 메모리처럼 작동하도록 하드 디스크 드라이브의 섹션을 시뮬레이션하는 스왑 파일 시스템을 많이 사용하게되는 엄청난 메모리 할당을 의미 하지만 하드 디스크 드라이브는 매우 느립니다.

StringBuilder 옵션은 단일 사용자로 시스템을 사용하는 사람에게는 괜찮아 보이지만 두 명 이상의 사용자가 동시에 대용량 파일을 읽는 경우 문제가 있습니다.


멀리서 너희들은 매우 빠르다! 불행히도 매크로의 작동 방식 때문에 전체 스트림을로드해야합니다. 내가 언급했듯이 리치 텍스트 부분에 대해 걱정하지 마십시오. 개선하고 싶은 초기 로딩입니다.
Nicole Lee

따라서 부분적으로 작업하고, 첫 번째 X 줄을 읽고, 매크로를 적용하고, 두 번째 X 줄을 읽고, 매크로를 적용하는 등의 작업을 수행 할 수 있습니다.이 매크로의 기능을 설명하면 더 정확하게 도움을 드릴 수 있습니다.
Tufo

5

이 정도면 시작하기에 충분합니다.

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]"를 루프 밖으로 옮길 것입니다. 매번 새 버퍼를 만들 필요는 없습니다. "while (count> 0)"앞에 넣으십시오.
Tommy Carlier

4

다음 코드 조각을 살펴보십시오. 을 (를) 언급하셨습니다 Most files will be 30-40 MB. 이것은 Intel Quad Core에서 1.4 초에 180MB를 읽는다고 주장합니다.

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

원본 기사


3
이러한 종류의 테스트는 신뢰할 수없는 것으로 악명이 높습니다. 테스트를 반복 할 때 파일 시스템 캐시에서 데이터를 읽습니다. 이는 디스크에서 데이터를 읽는 실제 테스트보다 적어도 한 자릿수 더 빠릅니다. 180MB 파일은 3 초 미만이 소요될 수 없습니다. 컴퓨터를 재부팅하고 실수에 대한 테스트를 한 번 실행하십시오.
Hans Passant

7
stringBuilder.Append 줄은 잠재적으로 위험하므로 stringBuilder.Append (fileContents, 0, charsRead); 스트림이 더 일찍 종료 된 경우에도 전체 1024자를 추가하지 않도록합니다.
Johannes Rudolph 2011

@JohannesRudolph, 귀하의 의견으로 버그가 해결되었습니다. 1024 숫자는 어떻게 구 했나요?
OfirD

3

여기서 메모리 매핑 된 파일을 처리하는 것이 더 나을 수 있습니다 . 메모리 매핑 된 파일 지원은 .NET 4 (내 생각에 ... 다른 사람을 통해 이야기를 들었습니다)에서 가능하므로 p를 사용하는이 래퍼 / 동일한 작업을 수행하도록 호출합니다.

편집 : MSDN 에서 작동 방식을 보려면 여기를 참조하십시오 . 릴리스로 출시 될 때 곧 출시 될 .NET 4에서 수행되는 방식을 나타내는 블로그 항목이 있습니다. 내가 이전에 제공 한 링크는이를 달성하기 위해 pinvoke를 둘러싼 래퍼입니다. 전체 파일을 메모리에 매핑하고 파일을 스크롤 할 때 슬라이딩 창처럼 볼 수 있습니다.


2

모든 훌륭한 답변! 그러나 답을 찾는 사람에게는 다소 불완전한 것으로 보입니다.

표준 문자열은 구성에 따라 크기 X, 2Gb ~ 4Gb 만 가능하므로 이러한 답변은 실제로 OP의 질문을 충족하지 않습니다. 한 가지 방법은 문자열 목록으로 작업하는 것입니다.

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

일부는 처리 할 때 토큰 화 및 분할을 원할 수 있습니다. 이제 문자열 목록에 매우 많은 양의 텍스트가 포함될 수 있습니다.


1

반복자는 이러한 유형의 작업에 적합 할 수 있습니다.

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를 사용하여 문자를 읽을 수 있습니다. 그러면 다중 바이트 문자 ( UTF-8 , UTF-16 등)를 읽을 때 버퍼가 올바르게 정렬됩니다 .

이 모든 작업은 백그라운드 작업, 스레드 또는 복잡한 사용자 지정 상태 시스템을 사용하지 않고 수행됩니다.


당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.