HOWTO · C++

C++로 CSV 파일 읽기

이 기사는 C++로 CSV 파일을 읽는 방법을 보여줍니다.

이 페이지의 내용

C++에서 간단한 쉼표 구분 파일을 읽으려면 std::getline로 한 번에 한 줄씩 읽은 다음, 두 번째 std::getline 호출로 그 줄을 필드로 나눕니다. 이 방법은 각 물리적 줄이 하나의 레코드이고 쉼표가 필드 구분자로만 쓰일 때 적합합니다. 아래 예제는 작은 입력 파일을 직접 만들기 때문에 별도의 데이터 파일을 준비하지 않고 컴파일해 실행할 수 있습니다.

std::getline와 std::istringstream로 C++에서 CSV 파일 읽기

프로그램은 먼저 grades.csv에 레코드 두 개를 쓴 다음, 파일을 읽기 모드로 엽니다. 바깥쪽 std::getline는 물리적 줄 하나를 std::string에 읽어 들입니다. std::istringstream는 그 줄로 스트림을 만들고, 안쪽 std::getline는 쉼표를 구분자로 사용해 각 필드를 추출합니다. 각 레코드는 std::vector<std::string>가 되고, 레코드 전체도 벡터에 담깁니다. 이렇게 하면 원래 순서를 유지할 수 있으며, 행이 순차적으로 이어지는 경우 정수 키를 사용하는 맵이 필요하지 않습니다.

다음 자체 완결형 프로그램을 실행해 보세요. 코드 블록의 레이블과 문자열은 바이트 단위로 보존되므로 영어로 표시됩니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

#include <algorithm>
#include <cctype>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include <vector>

std::string trimEnds(const std::string& value) {
    const auto first = std::find_if_not(value.begin(), value.end(),
        [](unsigned char ch) { return std::isspace(ch); });
    const auto last = std::find_if_not(value.rbegin(), value.rend(),
        [](unsigned char ch) { return std::isspace(ch); }).base();
    if (first >= last) {
        return "";
    }
    return std::string(first, last);
}

int main() {
    const std::string path = "grades.csv";
    {
        std::ofstream sample(path);
        if (!sample) {
            std::cerr << "Could not create input file\n";
            return 1;
        }
        sample << "Name, Score\nAda Lovelace, 98 \nGrace Hopper, 95\n";
    }

    std::ifstream input(path);
    if (!input) {
        std::cerr << "Could not open input file\n";
        return 1;
    }

    std::vector<std::vector<std::string>> rows;
    std::string record;
    while (std::getline(input, record)) {
        std::istringstream line(record);
        std::vector<std::string> fields;
        std::string field;
        while (std::getline(line, field, ',')) {
            fields.push_back(trimEnds(field));
        }
        rows.push_back(fields);
    }

    for (const auto& row : rows) {
        std::cout << row[0] << " | " << row[1] << '\n';
    }
    return 0;
}

성공적으로 실행했을 때 제공된 결과는 다음과 같습니다. 출력 블록의 레이블과 내용도 코드와 동일하게 영어로 표시됩니다.

Name | Score
Ada Lovelace | 98
Grace Hopper | 95

파일 생성이나 파일 열기에 실패하면 오류를 std::cerr에 출력하고 상태 코드 1을 반환하며, 정상적으로 실행되면 상태 코드 0을 반환합니다. 직접 프로그램을 작성할 때는 읽기 전에 입력 스트림이 정상적으로 열렸는지 확인하세요. 파일이 프로그램 외부에서 제공된다면 파일이 있다고 가정하지 말고 경로를 알린 뒤, 애플리케이션의 요구에 맞게 열기 실패를 처리하세요.

CSV 필드의 공백 처리

공백 처리는 분할에 자동으로 포함되는 동작이 아니라 데이터에 관한 선택입니다. trimEnds는 추출한 각 필드의 앞뒤 공백만 제거하고, Ada Lovelace처럼 값 안에 있는 단어 사이의 공백은 유지합니다. std::isspace를 호출하기 전에 값을 unsigned char로 변환하면 음수일 수 있는 일반 char 값을 문자 분류 함수에 전달하는 문제를 피할 수 있습니다.

어떤 파일은 쉼표 뒤의 공백을 서식으로 취급하지만, 다른 파일은 필드의 일부로 취급합니다. 보존해야 하는 데이터 규칙에 맞을 때만 공백을 제거하세요. 공백 문자를 모두 없애면 이름이나 주소 안의 단어까지 붙고, 앞뒤 공백을 다듬으면 필드의 시작과 끝에 있는 내용이 달라집니다. 원본 데이터에서 공백이 의미를 가진다면 읽은 값을 그대로 두거나 ASCII 공백만 다듬는 것처럼 더 제한적인 규칙을 적용하세요.

분할 반복문은 빈 필드에도 영향을 줍니다. 다른 내용 사이에 쉼표가 연속되면 그 사이에서 빈 문자열이 나옵니다. 줄 끝의 쉼표는 이 std::getline 반복문이 마지막 빈 필드를 추가하게 만들지 않으므로, 입력 형식에서 필요하다면 끝에 있는 구분자를 별도로 처리하세요. 예상 열에 따라 빈 값을 유지하거나 거부하면 됩니다. 헤더보다 필드 수가 적은 줄은 행의 요소에 접근하기 전에 명시적으로 처리해야 합니다. 위의 간단한 출력 반복문은 두 열이 모두 있다고 가정합니다.

구분자 기반 파싱의 한계

이 방법은 각 물리적 줄을 쉼표가 나오는 모든 위치에서 나눕니다. CSV의 따옴표 규칙은 해석하지 않습니다. 예를 들어 "Paris, France" 같은 값은 CSV 리더라면 따옴표 안의 텍스트를 하나의 필드로 처리해야 하지만, 여기서는 두 조각으로 나뉩니다. 따옴표로 묶인 필드 안의 이스케이프된 큰따옴표도 해석하지 않으며, 필드 안의 줄바꿈은 레코드 끝으로 잘못 처리됩니다. 레코드마다 필드 수가 다를 수도 있으므로 모든 줄의 쉼표 수가 같다고 가정하지 마세요.

각 물리적 줄이 하나의 레코드이고 따옴표 안에 구분자가 없다고 보장되는 단순하고 통제된 입력에는 이 방법을 사용할 수 있습니다. 일반적인 CSV 파일에는 입력 데이터의 방언에 맞는 따옴표와 이스케이프 규칙을 구현한 CSV 파서를 사용하세요. 파싱한 뒤에도 애플리케이션에 필요한 필드 수와 의미를 검증해야 합니다. 여기서 사용한 표준 스트림 연산은 파일과 줄을 읽지만, 그 자체로 CSV 형식의 전체 문법을 처리하지는 않습니다.