83.3 JSON에서 문자열 파싱하기

먼저 문자열부터 파싱해보겠습니다. 그림 83‑1은 JSON에서 문자열을 파싱하는 과정입니다.

▼ 그림 83‑1 JSON에서 문자열을 파싱하는 과정

파싱할 JSON 문서는 다음과 같으며 .c 파일이 있는 폴더에 example.json으로 저장합니다(파일은 GitHub 저장소의 Unit 83/83.3/json/json 폴더에 들어있습니다).

example.json

{
  "Title": "Inception",
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan"
}

example.json 파일은 { }에 문자열로 된 키-값으로 구성되어 있습니다. 여기서 공통적인 특징을 찾아볼까요?

  • 첫 문자는 {로 시작한다.
  • 문자열은 항상 "로 시작하여 "로 끝난다.

이제 공통적인 특징들을 이용하여 문자열을 처리해보겠습니다. 다음과 같이 JSON 파싱 함수를 작성합니다. 이 함수는 JSON 문서 문자열, 문서의 크기, JSON 구조체 포인터를 받습니다.

void parseJSON(char *doc, int size, JSON *json)    // JSON 파싱 함수
{
    int tokenIndex = 0;    // 토큰 인덱스
    int pos = 0;           // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{')   // 문서의 시작이 {인지 검사
        return;

    pos++;    // 다음 문자로

    while (pos < size)       // 문서 크기만큼 반복
    {
        switch (doc[pos])    // 문자의 종류에 따라 분기
        {
        case '"':            // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL)    // "가 없으면 잘못된 문법이므로 
                break;          // 반복을 종료

            int stringLength = end - begin;    // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1;    // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;
        }

        pos++; // 다음 문자로
    }
}

parseJSON 함수는 매개변수로 JSON 문서의 문자열 포인터 doc와 문서(파일) 크기 size, JSON 구조체의 포인터 json을 받습니다.

void parseJSON(char *doc, int size, JSON *json)    // JSON 파싱 함수

먼저 문서의 시작이 {인지 검사합니다. 만약 {로 시작하지 않으면 파싱을 중단합니다(맨 앞에 공백 문자나 개행 문자가 있을 수도 있지만 여기서는 무조건 {로 시작한다고 가정합니다). 그리고 검사가 끝났으면 pos를 1 증가시켜서 다음 문자를 처리합니다.

void parseJson(char *doc, int size, JSON *json)    // JSON 파싱 함수
{
    int tokenIndex = 0;    // 토큰 인덱스
    int pos = 0;           // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{')   // 문서의 시작이 {인지 검사
        return;

    pos++; // 다음 문자로

다음은 JSON 문서에서 문서의 시작 위치를 찾고, 다음 문자를 처리하는 과정입니다.

▼ 그림 83‑2 문서의 시작 부분 검사

이제 while 반복문으로 문서 크기만큼 반복하면서 switch로 문자의 종류에 따라 분기합니다.

    while (pos < size)       // 문서 크기만큼 반복
    {
        switch (doc[pos])    // 문자의 종류에 따라 분기
        {
            // 생략...
        }

        pos++;    // 다음 문자로
    }

doc[pos]에 들어있는 문자가 "이면 문자열이므로 큰따옴표 안의 문자열을 분리해냅니다. 여기서 doc는 char 포인터이므로 포인터 연산을 통해 문서의 중간 지점으로 이동할 수 있습니다. 먼저 맨 앞의 "를 제외한 문자열의 시작 위치를 구합니다. 그리고 strchr 함수로 문자열의 끝 위치인 다음 "의 위치를 구합니다. 만약 "가 없으면 잘못된 문법이므로 반복을 종료합니다.

        case '"':               // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL)    // "가 없으면 잘못된 문법이므로 
                break;          // 반복을 종료

다음은 JSON 문서에서 문자열의 시작 위치 begin과 끝 위치 end를 구하는 과정입니다.

▼ 그림 83‑3 문자열의 시작 위치와 끝 위치 구하기

끝 위치에서 시작 위치를 빼면 문자열의 실제 길이를 알 수 있겠죠? 이제 토큰 배열의 요소에 문자열을 저장합니다. 토큰 종류에는 TOKEN_STRING을 지정하여 토큰이 문자열이라는 것을 표시해주고, 문자열 포인터에는 문자열 길이 stringLength + NULL 공간만큼 메모리를 할당하고 0으로 초기화합니다.

            int stringLength = end - begin;    // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

그다음에는 memcpy 함수로 문자열 시작 위치에서 문자열 길이만큼만 복사하여 문자열을 토큰에 저장합니다. strcpy 함수는 NULL 직전까지 복사하기 때문에 우리가 원하는 만큼 문자열을 가져올 수 없습니다. 따라서 memcpy 함수로 복사할 크기를 제한해야 합니다.

모든 처리가 끝났으면 토큰 인덱스를 1 증가시키고, pos에는 문자열 길이와 "의 크기 1을 더해서 다음 문자열을 처리할 수 있도록 만듭니다.

            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1;    // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;

다음은 문서에서 문자열을 토큰에 저장하는 과정입니다.

▼ 그림 83‑4 문서에서 문자열을 토큰에 저장

토큰에 문자열을 저장할 때 malloc 함수로 동적 메모리를 할당했습니다. 한 번 할당한 메모리는 반드시 해제를 해줘야 하겠죠? 다음과 같이 freeJSON 함수는 토큰 개수만큼 반복하면서 토큰 종류가 문자열이면 동적 메모리를 해제합니다.

void freeJSON(JSON *json)    // JSON 해제 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++)            // 토큰 개수만큼 반복
    {
        if (json->tokens[i].type == TOKEN_STRING)    // 토큰 종류가 문자열이면
            free(json->tokens[i].string);            // 동적 메모리 해제
    }
}

이제 main 함수에서 지금까지 만든 함수들을 사용하여 JSON 문서를 파싱해보겠습니다.

int main()
{
    int size; // 문서 크기
    char *doc = readFile("example.json", &size);    // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    if (doc == NULL)
        return -1;

    JSON json = { 0, };             // JSON 구조체 변수 선언 및 초기화

    parseJSON(doc, size, &json);    // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);
    printf("Genre: %s\n", json.tokens[3].string);
    printf("Director: %s\n", json.tokens[5].string);

    freeJSON(&json);    // json에 할당된 동적 메모리 해제

    free(doc);    // 문서 동적 메모리 해제

    return 0;
}

먼저 파일에서 JSON 문서를 읽고, 문서 크기를 구합니다.

int size; // 문서 크기
char *doc = readFile("example.json", &size);    // 파일에서 JSON 문서를 읽음, 문서 크기를 구함

JSON 구조체로 변수를 선언한 뒤 0으로 초기화합니다. 그리고 parseJSON 함수를 호출하여 JSON 문서를 파싱합니다.

JSON json = { 0, };    // JSON 구조체 변수 선언 및 초기화

parseJSON(doc, size, &json);    // JSON 문서 파싱

json.tokens 배열에 키와 값들이 들어있습니다. 배열에 인덱스로 접근하여 값을 출력합니다(키를 지정하여 값을 가져오는 방법은 뒤에서 설명하겠습니다).

printf("Title: %s\n", json.tokens[1].string);       // 토큰에 저장된 문자열 출력(Title)
printf("Genre: %s\n", json.tokens[3].string);       // 토큰에 저장된 문자열 출력(Genre)
printf("Director: %s\n", json.tokens[5].string);    // 토큰에 저장된 문자열 출력(Director)

모든 처리가 끝났으면 freeJSON 함수를 호출하여 json 안에 할당된 동적 메모리를 해제하고, doc 문서 동적 메모리도 해제합니다.

freeJSON(&json);    // json 안에 할당된 동적 메모리 해제

free(doc);    // 문서 동적 메모리 해제

전체 소스 코드는 다음과 같습니다(파일은 GitHub 저장소의 Unit 82/82.3/json/json 폴더에 들어있습니다).

json.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>     // 파일 처리 함수가 선언된 헤더 파일
#include <stdlib.h>    // malloc, free 함수가 선언된 헤더 파일
#include <stdbool.h>   // bool, true, false가 정의된 헤더 파일
#include <string.h>    // strchr, memset, memcpy 함수가 선언된 헤더 파일

// 토큰 종류 열거형
typedef enum _TOKEN_TYPE {
    TOKEN_STRING,    // 문자열 토큰
    TOKEN_NUMBER,    // 숫자 토큰
} TOKEN_TYPE;

// 토큰 구조체
typedef struct _TOKEN {
    TOKEN_TYPE type;   // 토큰 종류
    union {            // 두 종류 중 한 종류만 저장할 것이므로 공용체로 만듦
        char *string;     // 문자열 포인터
        double number;    // 실수형 숫자
    };
    bool isArray;      // 현재 토큰이 배열인지 표시
} TOKEN;

#define TOKEN_COUNT 20    // 토큰의 최대 개수

// JSON 구조체
typedef struct _JSON {
    TOKEN tokens[TOKEN_COUNT]; // 토큰 배열
} JSON;

char *readFile(char *filename, int *readSize)    // 파일을 읽어서 내용을 반환하는 함수
{
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL)
        return NULL;

    int size;
    char *buffer;

    // 파일 크기 구하기
    fseek(fp, 0, SEEK_END);
    size = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 파일 크기 + NULL 공간만큼 메모리를 할당하고 0으로 초기화
    buffer = malloc(size + 1);
    memset(buffer, 0, size + 1);

    // 파일 내용 읽기
    if (fread(buffer, size, 1, fp) < 1)
    {
        *readSize = 0;
        free(buffer);
        fclose(fp);
        return NULL;
    }

    // 파일 크기를 넘겨줌
    *readSize = size;

    fclose(fp);    // 파일 포인터 닫기

    return buffer;
}

void parseJSON(char *doc, int size, JSON *json)    // JSON 파싱 함수
{
    int tokenIndex = 0;    // 토큰 인덱스
    int pos = 0;           // 문자 검색 위치를 저장하는 변수

    if (doc[pos] != '{')   // 문서의 시작이 {인지 검사
        return;

    pos++;    // 다음 문자로

    while (pos < size)       // 문서 크기만큼 반복
    {
        switch (doc[pos])    // 문자의 종류에 따라 분기
        {
        case '"':            // 문자가 "이면 문자열
        {
            // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
            char *begin = doc + pos + 1;

            // 문자열의 끝 위치를 구함. 다음 "의 위치
            char *end = strchr(begin, '"');
            if (end == NULL)    // "가 없으면 잘못된 문법이므로 
                break;          // 반복을 종료

            int stringLength = end - begin;    // 문자열의 실제 길이는 끝 위치 - 시작 위치

            // 토큰 배열에 문자열 저장
            // 토큰 종류는 문자열
            json->tokens[tokenIndex].type = TOKEN_STRING;
            // 문자열 길이 + NULL 공간만큼 메모리 할당
            json->tokens[tokenIndex].string = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

            // 문서에서 문자열을 토큰에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(json->tokens[tokenIndex].string, begin, stringLength);

            tokenIndex++; // 토큰 인덱스 증가

            pos = pos + stringLength + 1;    // 현재 위치 + 문자열 길이 + "(+ 1)
        }
        break;
        }

        pos++; // 다음 문자로
    }
}

void freeJSON(JSON *json)    // JSON 해제 함수
{
    for (int i = 0; i < TOKEN_COUNT; i++)            // 토큰 개수만큼 반복
    {
        if (json->tokens[i].type == TOKEN_STRING)    // 토큰 종류가 문자열이면
            free(json->tokens[i].string);            // 동적 메모리 해제
    }
}

int main()
{
    int size;    // 문서 크기
    
    // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
    char *doc = readFile("example.json", &size);
    if (doc == NULL)
        return -1;

    JSON json = { 0, };    // JSON 구조체 변수 선언 및 초기화

    parseJSON(doc, size, &json);    // JSON 문서 파싱

    printf("Title: %s\n", json.tokens[1].string);       // 토큰에 저장된 문자열 출력(Title)
    printf("Genre: %s\n", json.tokens[3].string);       // 토큰에 저장된 문자열 출력(Genre)
    printf("Director: %s\n", json.tokens[5].string);    // 토큰에 저장된 문자열 출력(Director)

    freeJSON(&json);    // json 안에 할당된 동적 메모리 해제

    free(doc);    // 문서 동적 메모리 해제

    return 0;
}

Visual Studio에서 Ctrl+F5 키를 눌러서 프로그램을 실행하면 다음과 같이 JSON 문서를 파싱한 내용이 출력됩니다.

실행 결과

Title: Inception
Genre: Sci-Fi
Director: Christopher Nolan