본문 바로가기
Program/C# .NET

[C# WPF] TEXT, CSV File 빠르게 읽기 / 속도 비교 / ReadStream

by 냠만 2024. 2. 8.

[파일 읽기를 해야 하는 경우]

진행 중인 프로젝트에서 별도의 DB를 사용하지 않는 경우 디바이스의 검사 결과를 별도 파일로 남기고, 해당 파일을 MES나 FDC EES등에서 읽어 결과를 집계해야 되는 경우가 존재한다. 아니면 검사기의 경우 로그로 별도의 검사결과를 남겨야 하는 경우에도 사용될 수 있다.

우선 소개되는 방법에는 읽기 쪽만 소개될 예정이지만 쓰기 쪽도 동일한 방식으로 사용하면 될 것 같다. 아니면 별도 읽기 쓰기 유틸 클래스를 만들어 싱글턴 패턴을 사용하거나 다중 스레드 환경의 경우 전역함수나 정적으로 사용하면 될 것 같다.

거두 절미하고 바로 파일 읽기에 보편적으로 사용되는 방식들을 소개하겠다.

 

[파일 읽기의 방법]

1. ExcelDataReader (ExcelReaderFactory.CreateCsvReader(stream))

2. ReadReadAllLines (File.ReadAllLines(filePath))

3. ReadStreamLine (reader.BaseStream.Position < reader.BaseStream.Length)

4. StreamBuffer (BufferedStream)

5. StreamReader (EndOfStream)

 

1. ExcelDataReader (ExcelReaderFactory.CreateCsvReader(stream))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
        public void UseExcelDataReader(string filePath)
        {
            using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read))
            {
                using (var reader = ExcelReaderFactory.CreateCsvReader(stream))
                {
                    do
                    {
                        while (reader.Read())
                        {
                            //reader.GetDouble(0);
                        }
                    } while (reader.NextResult());
 
                    var result = reader.AsDataSet();
                    DataSet dataTable = new DataSet(); 
                    dataTable = result.Clone();
                }
            }
        }
cs

ExcelReaderFactory class를 참조하여 아래 있는 함수를 사용하는 방법이다. File Stream을 구성하고 해당 스트림을 함수에 넣어줌으로써 동작한다. 스트림을 한 번에 가져와서 루프를 돌리지만 5가지 방법 중 가장 느리다. excel에 연관된 함수를 끌어와서 쓰는 경우는 대부분 퍼포먼스가 좋지 못한 것 같다.

 

2. ReadReadAllLines (File.ReadAllLines(filePath))

1
2
3
4
5
6
7
8
9
10
11
        public void UseReadReadAllLines(string filePath)
        {//
            var items = new List<ItemData>();
            foreach (var line in File.ReadAllLines(filePath))
            {
                var parts = line.Split(',');
                items.Add(new ItemData
                {
                });
            }
        }
cs

System.IO 라이브러리를 이용해 데이터의 라인 참조를 통해 사용하는 방법이다.

 

3. ReadStreamLine (reader.BaseStream.Position < reader.BaseStream.Length)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        public void UseReadStreamLine(string filePath)
        {
            var items = new List<ItemData>();
            using (var reader = new StreamReader(filePath))
            {
                while (reader.BaseStream.Position < reader.BaseStream.Length)
                {
                    var parts = reader.ReadLine().Split(',');
                    items.Add(new ItemData
                    {
                    });
                }
            }
        }
cs

StreamReader를 사용해서 Base position에서 스트림 길이만큼 루프를 돌면서 데이터를 사용하는 방법이다.

4. StreamBuffer (BufferedStream)

1
2
3
4
5
6
7
8
9
10
11
12
13
        public void UseStreamBuffer(string filePath)
        {//
            using (FileStream fs = File.Open(filePath, 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)
                {
 
                }
            }
        }
cs

StreamReader를 사용하는 방식으로 3번 방식과 동일하지만 파일을 다이렉트로 참조시키는 게 아닌 버퍼를 추가하여 해당 버퍼에 FileStream을 넣어놓고 참조시키는 방법이다.

프로그램을 짜다보면 퍼포먼스가 중요한 부분들이 있는데 버퍼의 개념은 어디에서도 다양하게 활용될 수 있으므로 활용법을 좀 더 공부해야겠다. 버퍼는 더블버퍼로도 구성될 수 있으며 이미지처리, 영상처리, 파일처리 등 다양한 연산분야에서 활용할 수 있다.

 

5. StreamReader (EndOfStream)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    public void ReadEndOfStream(string filePath)
        {
            StreamReader sr = new StreamReader(filePath);
            string headers = "";
            DataTable dt = new DataTable();
 
            dt.Columns.Add(headers);
            dt.Columns.Add(headers);
            dt.Columns.Add(headers);
 
            while (!sr.EndOfStream)
            {
                string[] rows = Regex.Split(sr.ReadLine(), ",(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)");
                DataRow dr = dt.NewRow();
                for (int i = 0; i < dt.Columns.Count; i++)
                {
                    dr[i] = rows[i];
 
                }
                dt.Rows.Add(dr);
            }
        }
cs

데이터 파일을 읽어 Table로 관리해야 될 필요가 있을 때 주로 사용하는 방법이다.

퍼포먼스는 DataTable에 Row를 추가하며 데이터를 입력하기 때문에 상대적으로 느리긴 하지만 버퍼를 모를 시절에 유용하게 사용했던 방법이다. 해당 방식을 사용해도 웬만한 처리속도는 보장된다.

 

 

[테스트 방식과 가장 효율적인 방법]

테스트는 약 2MB CSV 파일 읽기로 진행되었다. 파일 구성은 X,Y,Z 좌표계로 이루어진 숫자파일로 맨 위에는 헤더가 존재한다. 데이터 길이는 약 5만 줄로 보통 사용되는 데이터 양을 기입하였다. 사용자에 따라 테스트 환경이나 필요 데이터 수의 카운트는 다를 수 있으니 참조하여 사용하면 될 것 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
 public partial class MainWindow : Window
    {
        static Utility.Eyevision.EyeControlClient EyeClient = new EyeControlClient();
        static EyeControlServer EyeServer = new EyeControlServer();
        public Stopwatch stopwatch = new Stopwatch();
        public MainWindow()
        {
            InitializeComponent();
            string filePath = @"C:\Data\Result\testcsv.csv";
 
            stopwatch.Restart();
            CSVReader.Instance.UseExcelDataReader(filePath);
            stopwatch.Stop();
            Console.WriteLine("ExcelDataReader : " + stopwatch.ElapsedMilliseconds.ToString());
 
            stopwatch.Restart();
            CSVReader.Instance.UseReadReadAllLines(filePath);
            stopwatch.Stop();
            Console.WriteLine("ReadReadAllLines : " + stopwatch.ElapsedMilliseconds.ToString());
 
            stopwatch.Restart();
            CSVReader.Instance.UseReadStreamLine(filePath);
            stopwatch.Stop();
            Console.WriteLine("ReadStreamLine : " + stopwatch.ElapsedMilliseconds.ToString());
 
            stopwatch.Restart();
            CSVReader.Instance.UseStreamBuffer(filePath);
            stopwatch.Stop();
            Console.WriteLine("StreamBuffer : " + stopwatch.ElapsedMilliseconds.ToString());
 
            stopwatch.Restart();
            CSVReader.Instance.ReadEndOfStream(filePath);
            stopwatch.Stop();
            Console.WriteLine("ReadEndOfStream : " + stopwatch.ElapsedMilliseconds.ToString());
        }
    }
cs

테스트에는 위 소스가 사용되었다. 단순히 Stopwatch를 돌리는 방식으로 시간을 측정했고, 파일은 모두 동일하게 위의 csv파일을 읽어왔다.

읽고 나서 별다른 처리를 한게 아닌 함수 리턴값을 받는 시간을 기점으로 측정되었다. (ms단위)

 

 

단순 테스트 결과에서는 Stream 버퍼를 이용한 방식이 가장 퍼포먼스가 좋았다.

물론 테스트 소스에서는 어떤 방법에서는 데이터 테이블을 사용하고, 어디선 단순히 읽기만 진행하고 하는 약간의 차이는 있다. 사용자가 구성하기 나름이지만 그래도 유용한 결과를 얻었다고 생각한다.

 

[결론]

물론 PC 하드웨어 구성과 성능에 따라 편차가 존재할 수 있으며 데이터 활용 방법에 따라서도 차이가 발생할 수 있다고 생각한다. 아무리 하드웨어가 스펙이 좋아졌다 해도 처리해야 될 데이터 양 역시 압도적으로 증가하고 있기 때문에 언제 어떤 상황이 발생해도 더 좋은 방법을 찾으려 하는 노력은 계속되어야 할 것 같다.

공부하자! :)