83.4 JSON에서 문자열 배열 파싱하기

이번에는 문자열 배열을 파싱해보겠습니다. 그림 83‑5는 JSON에서 문자열 배열을 파싱하는 과정입니다.

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

다음과 같이 example.json 파일에 출연 배우 명단을 Actors 키로 추가해봅시다(파일은 GitHub 저장소의 Unit 83/83.4/json/json 폴더에 들어있습니다).

example.json

{
  "Title": "Inception",
  "Genre": "Sci-Fi",
  "Director": "Christopher Nolan",
  "Actors": [
    "Leonardo DiCaprio",
    "Joseph Gordon-Levitt",
    "Ellen Page",
    "Tom Hardy",
    "Ken Watanabe"
  ]
}

JSON 문서에서 문자열 배열은 [ ]안에 "로 묶은 문자열이 콤마로 구분되어 들어있습니다. 따라서 이 특징을 이용하여 문자열 배열을 처리해보겠습니다.

앞에서 만든 소스 코드의 parseJSON 함수에서 case '[':를 추가합니다.

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 '"':            // 문자가 "이면 문자열
        {
           // 생략...
        }
        break;
        case '[':        // 문자가 [이면 배열
        {
            pos++;       // 다음 문자로

            while (doc[pos] != ']')    // 닫는 ]가 나오면 반복 종료
            {
                // 여기서는 문자열 배열만 처리
                if (doc[pos] == '"')   // 문자가 "이면 문자열
                {
                    // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 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);
                    // 현재 문자열은 배열의 요소
                    json->tokens[tokenIndex].isArray = true;
                    // 할당한 메모리를 0으로 초기화
                    memset(json->tokens[tokenIndex].string, 0, stringLength + 1);

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

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

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

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

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

doc[pos]에 들어있는 문자가 [이면 배열입니다. 먼저 pos를 1 증가시켜서 [ 다음 문자를 처리하면서 ]가 나올 때까지 while 루프를 반복합니다.

        case '[':        // 문자가 [이면 배열
        {
            pos++;       // 다음 문자로

            while (doc[pos] != ']')    // 닫는 ]가 나오면 반복 종료
            {
                // 생략...

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

즉, 그림 83‑6와 같이 JSON 문서에서 배열의 시작 부분을 찾고, 다음 문자를 처리하게 됩니다.

그림 83‑6 배열의 시작

이제 "이 나오면 문자열입니다(여기서는 문자열 배열만 처리하겠습니다). 먼저 맨 앞의 "를 제외한 문자열의 시작 위치를 구합니다. 그리고 strchr 함수로 문자열의 끝 위치인 다음 "의 위치를 구합니다. 만약 "가 없으면 잘못된 문법이므로 반복을 종료합니다.

                // 여기서는 문자열만 처리
                if (doc[pos] == '"')   // 문자가 "이면 문자열
                {
                    // 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
                    char *begin = doc + pos + 1;

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

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

그림 83‑7 문자열 배열에서 문자열의 시작 위치와 끝 위치 구하기

문자열의 실제 길이를 구한 뒤 토큰 배열의 요소에 문자열을 저장합니다. 토큰 종류에는 TOKEN_STRING을 지정하여 토큰이 문자열이라는 것을 표시해주고, 문자열 포인터에는 문자열 길이 + NULL 공간만큼 메모리를 할당하고 0으로 초기화합니다. 여기서 현재 문자열은 배열의 요소이므로 isArraytrue를 지정해줍니다.

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

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

이제 memcpy 함수로 문자열 시작 위치에서 문자열 길이만큼만 복사하여 문자열을 토큰에 저장합니다. 그리고 모든 처리가 끝나면 토큰 인덱스를 1 증가시키고, pos에는 문자열 길이와 "의 크기 1을 더해서 다음 문자열을 처리할 수 있도록 만듭니다.

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

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

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

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

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

그림 83‑8 문자열 배열의 문자열을 토큰에 저장

이제 main 함수에서 JSON 문서의 Actors에 저장한 문자열 배열도 함께 출력할 수 있게 수정합니다(전체 파일은 GitHub 저장소의 Unit 83/83.4/json/json 폴더에 들어있습니다).

json.c

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)
    printf("Actors:\n");
    printf("  %s\n", json.tokens[7].string);         // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[8].string);         // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[9].string);         // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[10].string);        // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[11].string);        // 토큰에 저장된 문자열 출력(Actors 배열의 요소)

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

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

    return 0;
}

Visual Studio에서 Ctrl+F5 키를 눌러서 프로그램을 실행하면 다음과 같이 JSON 문서의 Actors 배열도 함께 출력됩니다.

실행 결과

Title: Inception
Genre: Sci-Fi
Director: Christopher Nolan
Actors:
  Leonardo DiCaprio
  Joseph Gordon-Levitt
  Ellen Page
  Tom Hardy
  Ken Watanabe

JSON 문서를 파싱했을 때 배열의 요소는 토큰에서 키(Actors) 문자열 뒤에 연달아서 위치합니다. 따라서 json.tokens에 인덱스를 7, 8, 9, 10, 11과 같이 지정하면 배열의 요소를 출력할 수 있습니다. 좀 더 편리하게 배열의 요소를 출력하는 방법은 뒤에서 설명하겠습니다.