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]에 들어있는 문자가 "이면 문자열이므로 큰따옴표 안의 문자열을 분리해냅니다. 여기서 docchar 포인터이므로 포인터 연산을 통해 문서의 중간 지점으로 이동할 수 있습니다. 먼저 맨 앞의 "를 제외한 문자열의 시작 위치를 구합니다. 그리고 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