83.5 JSON에서 숫자 파싱하기

지금까지 문자열을 파싱했으니 이번에는 숫자를 파싱해보겠습니다. 그림 83‑9는 JSON에서 숫자를 파싱하는 과정입니다.

그림 83‑9 JSON에서 숫자를 파싱하는 과정

다음과 같이 example.json 파일에 출시 연도 Year, 상영 시간Runtime, IMDB 평점 imdbRating키와 값을 추가합니다. 파일은 GitHub 저장소의 Unit 83/83.5/json/json 폴더에 들어있습니다). 참고로 IMDB는 영화 전문 데이터베이스 사이트입니다. http://www.imdb.com).

example.json

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

JSON에서 숫자 값은 "로 묶지 않으며 숫자가 그대로 들어갑니다. 따라서 이 특징을 이용하여 숫자를 처리해보겠습니다.

앞에서 만든 소스 코드의 parseJSON 함수에서 case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': 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 '[':            // 문자가 [이면 배열
        {
            // 생략...
        }
        break;
        case '0': case '1': case '2': case '3': case '4': case '5':    // 문자가 숫자이면
        case '6': case '7': case '8': case '9': case '-':              // -는 음수일 때
        {
            // 문자열의 시작 위치를 구함
            char *begin = doc + pos;
            char *end;
            char *buffer;

            // 문자열의 끝 위치를 구함. ,가 나오거나
            end = strchr(doc + pos, ',');
            if (end == NULL)
            {
                // } 가 나오면 문자열이 끝남
                end = strchr(doc + pos, '}');
                if (end == NULL)    // }가 없으면 잘못된 문법이므로
                    break;          // 반복을 종료
            }

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

            // 문자열 길이 + NULL 공간만큼 메모리 할당
            buffer = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(buffer, 0, stringLength + 1);

            // 문서에서 문자열을 버퍼에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(buffer, begin, stringLength);

            // 토큰 종류는 숫자
            json->tokens[tokenIndex].type = TOKEN_NUMBER;  
            // 문자열을 숫자로 변환하여 토큰에 저장
            json->tokens[tokenIndex].number = atof(buffer);

            free(buffer);    // 버퍼 해제

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

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

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

JSON 문서는 텍스트 문서이므로 안에 저장된 숫자는 사람이 보기에는 숫자이지만 실제로는 문자열입니다. 따라서 case '0':과 같이 숫자를 문자로 처리해야 합니다. 숫자가 여러 자리라 하더라도 첫 번째 문자만 숫자이면 나머지 자리도 숫자로 처리하면 됩니다. 그리고 숫자가 음수일 수도 있으므로 case '-':와 같이 -도 함께 처리해줍니다.

        case '0': case '1': case '2': case '3': case '4': case '5':    // 문자가 숫자이면
        case '6': case '7': case '8': case '9': case '-':              // -는 음수일 때
        {

다음은 JSON 문서에서 숫자의 시작 부분을 찾는 모습입니다.

그림 83‑10 숫자의 시작

숫자는 앞에 "가 없으므로 doc + pos가 시작 위치입니다. 마찬가지로 뒤에 "가 없으므로 , 또는 }가 나오면 숫자의 끝으로 봅니다. 만약 ,} 둘다 없으면 잘못된 문법이므로 반복을 종료합니다.

            // 문자열의 시작 위치를 구함
            char *begin = doc + pos;
            char *end;
            char *buffer;

            // 문자열의 끝 위치를 구함. ,가 나오거나
            end = strchr(doc + pos, ',');
            if (end == NULL)
            {
                // } 가 나오면 문자열이 끝남
                end = strchr(doc + pos, '}');
                if (end == NULL)    // }가 없으면 잘못된 문법이므로
                    break;          // 반복을 종료
            }

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

그림 83‑11 숫자(문자열)의 시작 위치와 끝 위치 구하기

문자열의 실제 길이를 구한 뒤 버퍼에 문자열의 길이 stringLength + NULL 공간만큼 메모리를 할당하고, 0으로 초기화합니다.

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

            // 문자열 길이 + NULL 공간만큼 메모리 할당
            buffer = malloc(stringLength + 1);
            // 할당한 메모리를 0으로 초기화
            memset(buffer, 0, stringLength + 1);

이제 문서의 문자열을 버퍼에 저장합니다. 단, 문자열을 그대로 사용할 수는 없으므로 atof 함수로 문자열(buffer)을 숫자로 변환하여 토큰에 저장합니다. 또한, 토큰 종류에는 TOKEN_NUMBER를 지정합니다.

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

            // 문서에서 문자열을 버퍼에 저장
            // 문자열 시작 위치에서 문자열 길이만큼만 복사
            memcpy(buffer, begin, stringLength);

            // 토큰 종류는 숫자
            json->tokens[tokenIndex].type = TOKEN_NUMBER;  
            // 문자열을 숫자로 변환하여 토큰에 저장
            json->tokens[tokenIndex].number = atof(buffer);

            free(buffer);    // 버퍼 해제

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

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

다음은 JSON 문서에서 숫자를 토큰에 저장하는 과정입니다.

그림 83‑12 문서에서 숫자를 토큰에 저장

이제 main 함수에서 숫자도 함께 출력해보겠습니다(전체 파일은 GitHub 저장소의 Unit 83/83.5/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("Year: %d\n", (int)json.tokens[3].number);    // 토큰에 저장된 숫자 출력(Year)
    printf("Runtime: %d\n", (int)json.tokens[5].number); // 토큰에 저장된 숫자 출력(Runtime)
    printf("Genre: %s\n", json.tokens[7].string);        // 토큰에 저장된 문자열 출력(Genre)
    printf("Director: %s\n", json.tokens[9].string);     // 토큰에 저장된 문자열 출력(Director)
    printf("Actors:\n");
    printf("  %s\n", json.tokens[11].string);            // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[12].string);            // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[13].string);            // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[14].string);            // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("  %s\n", json.tokens[15].string);            // 토큰에 저장된 문자열 출력(Actors 배열의 요소)
    printf("imdbRating: %f\n", json.tokens[17].number);  // 토큰에 저장된 숫자 출력(imdbRating)

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

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

    return 0;
}

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

실행 결과

Title: Inception
Year: 2010
Runtime: 148
Genre: Sci-Fi
Director: Christopher Nolan
Actors:
  Leonardo DiCaprio
  Joseph Gordon-Levitt
  Ellen Page
  Tom Hardy
  Ken Watanabe
imdbRating: 8.800000

json.tokens에서 숫자를 가져왔을 때 Year, Runtime(int)를 사용하여 정수로 변환했고, imdbRating은 실수를 그대로 출력했습니다. JSON 문서의 숫자는 정수로 사용하고 싶으면 앞에 (int)를 붙여서 자료형 변환을 해주고, 실수로 사용하고 싶으면 그대로 출력하면 됩니다.