본문 바로가기
카테고리 없음

C언어로 CSV 파싱 구현하기 (토큰, 상태기계, 오류 처리)

by info95686 2025. 10. 20.

C언어로 CSV 파싱 구현하기
C언어로 CSV 파싱 구현하기

 

CSV(Comma-Separated Values)는 데이터를 표 형태로 저장하는 가장 단순한 형식 중 하나이지만, 실제 파싱 과정에서는 단순히 쉼표로 구분하는 수준을 넘어 복잡한 예외 처리가 필요합니다. 문자열 내부의 쉼표, 따옴표로 감싼 필드, 다양한 개행 문자 등은 단순 split() 함수로 처리할 수 없습니다. 이 글에서는 C언어로 CSV 파일을 안정적으로 파싱하는 방법을 다루며, 토큰 분리, 상태기계(State Machine) 기반의 파싱, 그리고 예외 상황에서의 에러 처리 방법을 단계별로 설명합니다.

CSV 파싱의 기본 구조와 토큰 분리 로직

CSV 파일의 각 줄은 하나의 레코드이고, 쉼표(,)는 필드를 구분합니다. 그러나 따옴표로 감싸진 문자열 안에 쉼표가 포함될 수 있고, 필드 내에서 개행이 허용되기도 합니다. 예를 들어 다음과 같은 파일을 생각해봅시다.

name,age,city
"Kim, Jihoon",29,Seoul
Lee,31,"Busan, Korea"

일반적인 strtok() 기반 파싱은 이 데이터를 정확히 처리하지 못합니다.

char line[1024];
char *token;
while (fgets(line, sizeof(line), fp)) {
    token = strtok(line, ",");
    while (token != NULL) {
        printf("필드: %s\n", token);
        token = strtok(NULL, ",");
    }
}

이 방식은 간단하지만, 따옴표 내부의 쉼표나 줄바꿈을 올바르게 인식하지 못해 데이터가 잘립니다. 따라서 CSV 파싱에는 단순한 분할 함수 대신, 상태를 추적하면서 문자를 한 글자씩 처리하는 로직이 필요합니다. 이를 위해 상태 기계(State Machine) 방식을 적용합니다.

상태 기계(State Machine)로 CSV 파싱 구현하기

상태 기계는 입력을 하나씩 읽으며 현재 상태에 따라 행동을 결정하는 구조입니다. CSV 파싱에는 네 가지 주요 상태를 정의할 수 있습니다.

  • STATE_FIELD_START: 새 필드를 시작하는 상태
  • STATE_IN_FIELD: 따옴표 없는 필드를 읽는 중
  • STATE_IN_QUOTES: 따옴표 안의 필드를 읽는 중
  • STATE_QUOTE_CLOSE: 따옴표가 닫힌 뒤 다음 처리 대기

각 상태는 문자의 종류(쉼표, 따옴표, 개행 등)에 따라 다음 상태로 전이합니다. 다음은 기본적인 CSV 파서의 구현 예입니다.

#include <stdio.h>
#include <string.h>

#define MAX_FIELD 256

typedef enum {
    STATE_FIELD_START,
    STATE_IN_FIELD,
    STATE_IN_QUOTES,
    STATE_QUOTE_CLOSE
} State;

void parse_csv_line(const char *line) {
    char field[MAX_FIELD] = {0};
    int idx = 0;
    State state = STATE_FIELD_START;
    char c;

    for (int i = 0; (c = line[i]) != '\0'; i++) {
        switch (state) {
            case STATE_FIELD_START:
                if (c == '"') {
                    state = STATE_IN_QUOTES;
                } else if (c == ',') {
                    printf("[빈 필드]\n");
                } else {
                    field[idx++] = c;
                    state = STATE_IN_FIELD;
                }
                break;

            case STATE_IN_FIELD:
                if (c == ',') {
                    field[idx] = '\0';
                    printf("필드: %s\n", field);
                    idx = 0;
                    state = STATE_FIELD_START;
                } else {
                    field[idx++] = c;
                }
                break;

            case STATE_IN_QUOTES:
                if (c == '"') {
                    state = STATE_QUOTE_CLOSE;
                } else {
                    field[idx++] = c;
                }
                break;

            case STATE_QUOTE_CLOSE:
                if (c == '"') {
                    field[idx++] = '"';
                    state = STATE_IN_QUOTES;
                } else if (c == ',') {
                    field[idx] = '\0';
                    printf("필드: %s\n", field);
                    idx = 0;
                    state = STATE_FIELD_START;
                } else if (c == '\n' || c == '\r') {
                    break;
                } else {
                    fprintf(stderr, "오류: 잘못된 문법(%c)\n", c);
                    return;
                }
                break;
        }
    }

    if (idx > 0) {
        field[idx] = '\0';
        printf("필드: %s\n", field);
    }
}

이 코드는 CSV의 기본 문법을 준수하면서 따옴표 내부의 쉼표를 무시하고, 이스케이프된 따옴표("")도 정확히 처리합니다. 또한 개행 문자 \r\n\n을 구분해 처리할 수 있습니다. 실제로 파일을 줄 단위로 읽어 parse_csv_line()에 전달하면 완전한 CSV 파서가 완성됩니다.

오류 처리와 견고한 파서 만들기

CSV 파일은 표준을 지키지 않는 경우가 많습니다. 일부 데이터는 따옴표를 닫지 않거나, 필드 사이에 공백을 넣거나, 인코딩이 깨진 상태로 제공되기도 합니다. 이런 예외 상황을 감지하고 처리하는 로직이 필요합니다.

1. 따옴표 누락 검출

파싱이 끝났을 때 상태가 STATE_IN_QUOTES라면 닫히지 않은 따옴표가 있다는 뜻입니다. 이런 경우 에러 로그를 남기고 해당 라인을 무시합니다.

2. 공백 트리밍

필드 앞뒤의 공백은 의도하지 않은 포맷 문제일 수 있습니다. 다음과 같이 간단한 trim 함수를 만들어 처리합니다.

void trim(char *str) {
    char *end;
    while (*str == ' ') str++;
    end = str + strlen(str) - 1;
    while (end > str && *end == ' ') *end-- = '\0';
}

3. 인코딩 및 개행 처리

UTF-8 CSV 파일은 다바이트 문자가 포함될 수 있으므로, 인코딩 변환 도구(iconv 등)를 사용하는 것이 좋습니다. 또한 Windows의 \r\n 개행을 Linux 환경에서 읽을 때는 \r 문자를 제거하는 전처리 로직을 추가해야 합니다.

4. 에러 로그 관리

대용량 CSV 파일에서 파싱 오류가 발생하면 stderr 출력만으로는 문제를 추적하기 어렵습니다. 로그 파일을 만들어 오류 정보를 저장하면 디버깅에 유리합니다.

FILE *log = fopen("error.log", "a");
fprintf(log, "Line %d: Unclosed quote\n", line_num);
fclose(log);

결론: 단순하지만 완벽한 CSV 파서는 어렵다

CSV는 표준이 단순하지만 예외 처리가 매우 많습니다. C언어로 안정적인 파서를 만들려면 다음 세 가지 원칙을 따라야 합니다.

  • 입력 상태를 명확히 정의하고 상태 전이를 코드로 표현한다.
  • 모든 따옴표, 공백, 개행 상황을 예외로 다룬다.
  • 오류가 발생하면 즉시 로그를 남기고 프로그램을 중단하지 않는다.

이러한 접근 방식을 익히면 CSV뿐 아니라 JSON, XML, 로그 포맷 등 복잡한 텍스트 데이터도 쉽게 다룰 수 있습니다. 상태 기반 문자열 처리는 견고한 파서를 만드는 핵심이며, 이는 시스템 프로그래밍 전반에서 매우 중요한 사고방식입니다.