HOWTO · Csharp

C#은 CSV 파일을 읽고 그 값을 배열에 저장합니다

이 방법 문서에서는 CSV 파일을 읽고 해당 값을 C#의 배열에 저장하는 다양한 방법을 보여줍니다. OpenRead(), ReadLine() 및 TextFieldParser와 같은 메소드를 소개합니다.

이 페이지의 내용

CSV 파일은 쉼표로 구분된 파일로, 체계적인 방식으로 데이터를 저장하는 데 사용됩니다. 일반적으로 데이터를 테이블 형식으로 저장합니다. 대부분의 비즈니스 조직은 데이터를 CSV 파일에 저장합니다.

C#에서는 CSV 파일에 대해 다양한 작업을 수행할 수 있습니다. CSV 파일을 읽고, 쓰고, 변경할 수 있습니다. 이 문서에서는 CSV 파일을 읽고 그 값을 배열에 저장하는 방법, 즉 StreamReader로 한 번에 한 줄씩 읽는 방법, File.ReadAllLines()로 모든 줄을 한 번에 불러오는 방법, 그리고 TextFieldParser로 필드를 파싱하는 방법에 중점을 둡니다.

처음 세 예제는 동일한 샘플 Data.csv 파일을 사용합니다.

Name,Age
Alice,30
Bob,25

StreamReader로 CSV 파일 읽기

StreamReader 클래스는 파일을 텍스트 스트림으로 읽습니다. File.OpenRead()는 파일을 열고, ReadLine()은 EndOfStream에 도달할 때까지 한 번에 한 줄을 반환합니다. 각 줄을 쉼표로 나누면 개별 값이 되며, 예제에서는 이를 열당 하나의 리스트에 저장합니다.

OpenRead() 및 ReadLine() 메서드를 사용하는 올바른 구문은 다음과 같습니다.

// OpenRead() Syntax
File.OpenRead(@"FilePath");
// ReadLine() Syntax
StreamReaderObject.ReadLine();

예제 코드:

using System.IO;
using System.Collections.Generic;
using System;

class ReadingCSV {
  static void Main(string[] args) {
    var reader = new StreamReader(File.OpenRead(@"D:\New folder\Data.csv"));
    List<string> listA = new List<string>();
    List<string> listB = new List<string>();
    while (!reader.EndOfStream) {
      var line = reader.ReadLine();
      var values = line.Split(',');

      listA.Add(values[0]);
      listB.Add(values[1]);
    }
    foreach (var column1 in listA) {
      Console.WriteLine(column1);
    }
    foreach (var column2 in listB) {
      Console.WriteLine(column2);
    }
  }
}

출력:

Name
Alice
Bob
Age
30
25

쉼표로 나누는 방법은 필드 자체에 쉼표가 포함되어 있지 않을 때 동작합니다. "Smith, John"과 같이 따옴표로 묶인 필드는 잘못된 위치에서 나뉘게 됩니다. 그런 파일에는 TextFieldParser를 사용하세요.

File.ReadAllLines로 모든 줄을 한 번에 읽기

파일이 메모리에 들어갈 만큼 작다면 File.ReadAllLines()가 배열로 가는 가장 짧은 길입니다. 파일 전체를 읽어 각 줄을 string[]의 요소로 반환합니다. 아래 예제는 각 줄을 나누고, 각 데이터 행을 자체 string[]로 저장한 뒤, 행 목록을 ToArray()로 가변 배열(jagged array)로 변환하므로 data[i]가 완전한 i번째 행이 됩니다. 루프는 헤더 행을 건너뛰기 위해 인덱스 1에서 시작하고, 빈 줄을 건너뛰며, 인덱싱 전에 필드 수를 확인하므로 짧거나 빈 줄 때문에 중단되지 않습니다.

예제 코드:

using System;
using System.Collections.Generic;
using System.IO;

class ReadingAllLines {
  static void Main() {
    string[] lines = File.ReadAllLines("Data.csv");
    List<string[]> rows = new List<string[]>();
    // Start at index 1 to skip the header row.
    for (int i = 1; i < lines.Length; i++) {
      if (string.IsNullOrWhiteSpace(lines[i])) {
        continue;
      }
      string[] values = lines[i].Split(',');
      if (values.Length < 2) {
        continue;
      }
      rows.Add(values);
    }
    string[][] data = rows.ToArray();
    foreach (string[] row in data) {
      Console.WriteLine(row[0] + ": " + row[1]);
    }
  }
}

출력:

Alice: 30
Bob: 25

트레이드오프는 메모리와 단순함 사이의 선택입니다. ReadAllLines()는 첫 줄이 처리되기 전에 파일 전체를 불러오고, 즉시 인덱싱하고 개수를 세고 다시 읽을 수 있는 배열을 반환합니다. StreamReader 루프는 한 번에 한 줄만 유지하므로, 행을 한 번 처리하고 버리는 매우 큰 파일에서는 더 안전한 선택으로 남습니다. 여러 번 살펴보는 작은 파일이라면 한 번에 모두 불러오는 쪽이 더 단순하고 충분히 빠릅니다.

TextFieldParser로 CSV 필드 파싱하기

TextFieldParser 클래스는 Microsoft.VisualBasic.FileIO 네임스페이스에 정의되어 있습니다. 구분 기호가 있는 파일을 필드 단위로 파싱하며 따옴표로 묶인 필드를 이해합니다. SetDelimiters()로 구분 기호를 설정한 다음 ReadFields()를 호출하면 현재 줄의 필드를 문자열 배열로 얻을 수 있으며, 파서는 호출할 때마다 다음 줄로 이동합니다. .NET Framework 프로젝트에서는 아래 프로그램을 실행하기 전에 Microsoft.VisualBasic에 대한 참조를 추가하세요. 최신 .NET에서는 이 네임스페이스가 공유 프레임워크의 일부입니다.

이 파서를 사용하는 올바른 구문은 다음과 같습니다.

TextFieldParser ParserName = new TextFieldParser(PathString);

예제 코드:

using System;
using System.Collections.Generic;
using Microsoft.VisualBasic.FileIO;

class ReadingCSV {
  public static void Main() {
    var path = @"D:\New folder\Data.csv";
    List<string> listA = new List<string>();
    List<string> listB = new List<string>();
    using (TextFieldParser csvReader = new TextFieldParser(path)) {
      csvReader.CommentTokens = new string[] { "#" };
      csvReader.SetDelimiters(new string[] { "," });
      csvReader.HasFieldsEnclosedInQuotes = true;

      // Skip the row with the column names
      csvReader.ReadLine();

      while (!csvReader.EndOfData) {
        // Read current line fields, pointer moves to the next line.
        string[] fields = csvReader.ReadFields();
        listA.Add(fields[0]);
        listB.Add(fields[1]);
      }
    }
    foreach (var column1 in listA) {
      Console.WriteLine(column1);
    }
    foreach (var column2 in listB) {
      Console.WriteLine(column2);
    }
  }
}

출력:

Alice
Bob
30
25

이번에는 처음의 ReadLine()로 헤더 행을 건너뛰고, 수동 나누기가 아닌 파서가 각 필드가 끝나는 지점을 판단하므로 필드 안의 따옴표로 묶인 쉼표가 그대로 유지됩니다.

따옴표로 묶인 필드와 이스케이프된 따옴표 처리하기

실제 내보내기에서는 필드를 따옴표로 묶습니다. 구분 기호를 포함하는 필드는 큰따옴표로 감싸고, 따옴표로 묶인 필드 안의 큰따옴표는 두 번 써서 표현합니다. 아래 샘플 People.csv 파일은 두 규칙을 모두 사용합니다.

Name,Note
"Smith, John","Likes ""quoted"" text"
Jones,Plain note

Split(',')는 이런 규칙을 모릅니다. 이름 안의 쉼표 위치에서 첫 데이터 줄을 3조각으로 자릅니다. TextFieldParser는 규칙을 적용합니다. 둘러싼 따옴표를 제거하고, 겹친 따옴표를 하나로 합치므로 필드가 의도한 그대로 돌아옵니다. 이 프로그램은 먼저 Split(',')의 조각 수를 출력한 다음 파싱된 필드를 출력합니다.

예제 코드:

using System;
using Microsoft.VisualBasic.FileIO;

class ReadingQuotedCsv {
  public static void Main() {
    string rawLine = "\"Smith, John\",\"Likes \"\"quoted\"\" text\"";
    Console.WriteLine("Split(',') pieces: " + rawLine.Split(',').Length);
    using (TextFieldParser parser = new TextFieldParser("People.csv")) {
      parser.SetDelimiters(new string[] { "," });
      parser.HasFieldsEnclosedInQuotes = true;

      // Skip the row with the column names
      parser.ReadLine();

      while (!parser.EndOfData) {
        string[] fields = parser.ReadFields();
        Console.WriteLine(fields[0] + " | " + fields[1]);
      }
    }
  }
}

출력:

Split(',') pieces: 3
Smith, John | Likes "quoted" text
Jones | Plain note

직접 작성한 나누기에서 헤더의 열 수보다 많은 필드가 나온다면, 원인은 대개 따옴표로 묶인 필드입니다. 조각을 나중에 고치려 하지 말고, 그런 파일은 TextFieldParser로 전환하세요.

CSV 파일을 읽을 때 흔히 발생하는 오류

  • values[1]에서의 IndexOutOfRangeException: 끝에 있는 빈 줄이나 열이 빠진 행은 코드가 기대하는 것보다 적은 필드로 나뉩니다. 공백뿐인 줄을 건너뛰고, ReadAllLines() 예제처럼 인덱싱 전에 필드 수를 확인하세요.

  • FileNotFoundException 또는 DirectoryNotFoundException: 프로그램을 실행하는 컴퓨터에 해당 경로가 없습니다. D:\New folder\Data.csv 같은 하드코딩된 절대 경로는 작성자의 컴퓨터에서만 동작합니다. 상대 경로를 사용하거나 Path.Combine()으로 경로를 구성하고, 디버깅할 때 File.Exists()로 확인하세요.

  • 헤더 행이 데이터로 나타나는 경우: 첫 번째 StreamReader 예제는 Name과 Age를 실제 행과 함께 저장합니다. 파일에 헤더가 있으면 다른 예제들처럼 루프 전에 첫 줄을 건너뛰세요.

  • 파일이 잠긴 상태로 남는 경우: 닫히지 않은 StreamReader는 가비지 컬렉터가 실행될 때까지 핸들을 유지합니다. 리더를 using 블록에 선언하면 예외로 중단되더라도 루프가 끝나자마자 파일이 닫힙니다.