74. JSON
1. 키워드
- JSON(JavaScript Object Notation)
- 파싱(Parsing)과 파서(Parser)
2. JSON 파일 읽고 쓰기
- 데이터를 저장하거나 주고 받을 때 JSON 형식을 자주 사용한다.
- JSON은 JavaScript의 자료형을 텍스트로 표현한 포맷이다.
- 키-값 쌍으로 이루어져 있으며 사람이 쉽게 읽을 수 있도록 구성되어 있다.
- 먼저 JSON은 객체를
{}
(중괄호)로 표현한다.
- 여기서 키의 이름은 상황에 맞게 원하는대로 지으면 되는데, 보통 프로그램에서 원활하게 처리하기 위해 영문을 사용한다.
- 또한, JSON은 대소문자를 구분하므로
“name”
과“Name”
은 다른 키를 의미한다.
- 키와 값을 여러 개 쓰고 싶을 때는
,
(콤마)로 구분한다.
- 객체는
{}
로 표현하는데 객체 안에서 객체를 표현할 때도{}
를 쓰면 된다.
- 배열을 사용할 때는
[]
(대괄호)를 쓰면 된다.
{
"dayOfWeek": [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
"day": [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26, 27, 28, 29, 30, 31
]
}
- 배열 안에는 값뿐만 아니라 객체를 넣을 수 있다.
{
"dayOfWeek": [
{ "Sunday": 0 },
{ "Monday": 1 },
{ "Tuesday": 2 },
{ "Wednesday": 3 },
{ "Thursday": 4 },
{ "Friday": 5 },
{ "Saturday": 6 }
]
}
- JSON 문서의 내용이 모두 영문이라면 상관이 없지만, 한글이나 기타 다른 나라의 언어가 들어갈 때는 반드시 파일을 UTF-8 인코딩으로 저장해야 한다.
3. JSON 구조체 작성하기
- 특정 형식으로 된 텍스트 문서를 처리하여 프로그램에서 사용할 수 있도록 하는 것을 구문 분석 또는 파싱이라고 하며 파싱을 하는 프로그램을 파서라고 한다.
- 간단한 형식의 JSON 문서를 이용하여 텍스트 처리 방법을 알아보자.
- 먼저 JSON을 처리하기 위한 구조체를 만들어야 한다.
- 파싱은 문장을 분해하여 토큰(Token)으로 만드는 작업이므로 다음과 같이 토큰 구조체를 작성해야 한다.
- 토큰은 문자열 토큰과 숫자 토큰으로 나뉘는데 토큰 종류를 구분하기 위한
TOKEN_TYPE
열거형을 정의해 준다. - 그리고 토큰 구조체
TOKEN
은 토큰 종류, 문자열 포인터, 숫자 변수, 배열의 요소인지 표시하는 변수가 들어간다. - 여기서 토큰의 값은 문자열 또는 숫자 한 종류만 저장하므로
union
을 사용하여 공용체로 만든다.
// 토큰 종류 열거형
typedef enum _TOKEN_TYPE
{
TOKEN_STRING, // 문자열 토큰
TOKEN_NUMBER // 숫자 토큰
} TOKEN_TYPE;
// 토큰 구조체
typedef struct _TOKEN
{
TOKEN_TYPE type; // 토큰 종류
union // 두 종류 중 한 종류만 저장할 것이므로 공용체로 만듦
{
char *string; // 문자열 포인터
double number; // 실수형 숫자
};
bool isArray; // 현재 토큰이 배열인지 표시
} TOKEN;
- JSON 문서에 들어있는 숫자가 정수라고 하더라도
double
타입 변수에 저장한다. - 실수는 값을 그대로 사용하면 되고, 정수는 값을 꺼낸 뒤 정수로 형변환을 하면 된다.
- 이제 JSON 구조체를 정의한다.
- 간단하게 구현하기 위해 토큰은 배열로 만들었고, 개수를
20
개로 제한했다.
#define TOKEN_COUNT 20 // 토큰의 최대 개수
// JSON 구조체
typedef struct _JSON
{
TOKEN tokens[TOKEN_COUNT]; // 토큰 배열
} JSON;
- 지금까지 정의한 구조체로 JSON의 키-값에서 문자열 값과 숫자값 그리고 문자열 배열을 처리해 보자.
4. 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;
}
- 파일을 연 뒤 파일의 크기를 구한다.
- 그리고
파일 크기 + NULL
공간만큼 메모리를 할당하고0
으로 초기화한다. - 파일 읽기에 성공했다면
readSize
를 역참조하여 파일 크기를 넣어주고,buffer
를 반환한다. - 이렇게 하면
readFile
함수를 호출하는 쪽에서는 문자열 포인터를 반환값으로 받고, 파일 크기는 매개변수를 통해 받으므로 결괏값을 두 개 받는 효과를 낼 수 있다.
5. JSON에서 문자열 파싱하기
- 다음의 그림은 JSON에서 문자열을 파싱하는 과정이다.
- 파싱할 JSON 문서는 다음과 같으며
.c
파일이 있는 폴더에example.json
으로 저장한다.
example.json
파일은{}
에 문자열로 된 키-값으로 구성되어 있다.- 여기서 공통적인 특징은 다음과 같다.
1] 첫 문자는 {
로 시작한다.
2] 문자열은 항상 “
로 시작하여 “
로 끝난다.
- 이제 공통적인 특징들을 이용하여 문자열을 처리해 보자.
- 다음과 같이 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
을 받는다.
- 먼저 문서의 시작이
{
인지 검사한다. - 만약
{
로 시작하지 않으면 파싱을 중단한다. - 맨 앞에 공백 문자나 개행 문자가 있을 수도 있지만 여기서는 무조건
{
로 시작한다고 가정한다. - 그리고 검사가 끝났으면
pos
를1
증가시켜서 다음 문자를 처리한다.
void parseJSON(char *doc, int size, JSON *json) // JSON 파싱 함수
{
int tokenIndex = 0; // 토큰 인덱스
int pos = 0; // 문자 검색 위치를 저장하는 변수
if (doc[pos] != '{') // 문서의 시작이 {인지 검사
return;
pos++; // 다음 문자로
- 다음은 JSON 문서에서 문서의 시작 위치를 찾고, 다음 문자를 처리하는 과정이다.
- 이제
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
를 구하는 과정이다.
- 끝 위치에서 시작 위치를 빼면 문자열의 실제 길이를 알 수 있다.
- 이제 토큰 배열의 요소에 문자열을 저장한다.
- 토큰 종류에는
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
함수로 복사할 크기를 제한해야 한다. - 모든 처리가 끝났으면
tokenIndex
를1
증가시키고,pos
에는 문자열 길이와“
의 크기1
을 더해서 다음 문자열을 처리할 수 있도록 만든다.
// 문서에서 문자열을 토큰에 저장
// 문자열 시작 위치에서 문자열 길이만큼만 복사
memcpy(json->tokens[tokenIndex].string, begin, stringLength);
tokenIndex++; // 토큰 인덱스 증가
pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
}
break;
- 다음은 문서에서 문자열을 토큰에 저장하는 과정이다.
- 토큰에 문자열을
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 = { // JSON 구조체 변수 선언 및 초기화
0,
};
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 문서를 읽고, 문서 크기를 구한다.
- JSON 구조체로 변수를 선언한 뒤
0
으로 초기화한다. - 그리고
parseJSON
함수를 호출하여 JSON 문서를 파싱한다.
json.tokens
배열에 키와 값들이 들어있다.- 배열에 인덱스로 접근하여 값을 출력한다.
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
안에 할당된 동적 메모리를 해제하고,doc
문서 동적 메모리도 해제한다.
- 전체 소스 코드는 다음과 같다.
#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; // 문서 크기
char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
if (doc == NULL)
return -1;
JSON json = { // JSON 구조체 변수 선언 및 초기화
0,
};
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;
}
// Title: Inception
// Genre: Sci-Fi
// Director: Christopher Nolan
6. JSON에서 문자열 배열 파싱하기
- 이번에는 문자열 배열을 파싱해 보자.
- 다음의 그림은 JSON에서 문자열 배열을 파싱하는 과정이다.
- 다음과 같이
example.json
파일에 출연 배우 명단을Actors
키로 추가해 보자.
{
"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++; // 다음 문자로
}
}
- 즉, 다음의 그림과 같이 JSON 문서에서 배열의 시작 부분을 찾고, 다음 문자를 처리하게 된다.
- 이제
“
이 나오면 문자열이다. - 여기서는 문자열 배열만 처리할 것이다.
- 먼저 맨 앞의
“
를 제외한 문자열의 시작 위치를 구한다. - 그리고
strchr
함수로 문자열의 끝 위치인 다음“
의 위치를 구한다. - 만약
“
가 없으면 잘못된 문법이므로 반복을 종료한다.
// 여기서는 문자열 배열만 처리
if (doc[pos] == '"') // 문자가 "이면 문자열
{
// 문자열의 시작 위치를 구함. 맨 앞의 "를 제외하기 위해 + 1
char *begin = doc + pos + 1;
// 문자열의 끝 위치를 구함. 다음 "의 위치
char *end = strchr(begin, '"');
if (end == NULL) // "가 없으면 잘못된 문법이므로
break; // 반복을 종료
- 다음은 문자열 배열에서 문자열의 시작 위치
begin
과 끝 위치end
를 구하는 과정이다.
- 문자열의 실제 길이를 구한 뒤 토큰 배열의 요소에 문자열을 저장한다.
- 토큰 종류에는
TOKEN_STRING
을 지정하여 토큰이 문자열이라는 것을 표시해 주고, 문자열 포인터에는문자열 길이 + NULL
공간만큼 메모리를 할당하고0
으로 초기화한다. - 여기서 현재 문자열은 배열의 요소이므로
isArray
에true
를 지정해 준다.
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
함수로 문자열 시작 위치에서 문자열 길이만큼만 복사하여 문자열을 토큰에 저장한다. - 그리고 모든 처리가 끝나면
tokenIndex
를1
증가시키고,pos
에는 문자열 길이와“
의 크기1
을 더해서 다음 문자열을 처리할 수 있도록 만든다.
// 문서에서 문자열을 토큰에 저장
// 문자열 시작 위치에서 문자열 길이만큼만 복사
memcpy(json->tokens[tokenIndex].string, begin, stringLength);
tokenIndex++; // 토큰 인덱스 증가
pos = pos + stringLength + 1; // 현재 위치 + 문자열 길이 + "(+ 1)
}
pos++; // 다음 문자로
}
- 다음은 문자열 배열의 문자열을 토큰에 저장하는 과정이다.
- 이제
main
함수에서 JSON 문서의Actors
에 저장한 문자열 배열도 함께 출력할 수 있게 수정한다.
int main()
{
int size; // 문서 크기
char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
if (doc == NULL)
return -1;
JSON json = { // JSON 구조체 변수 선언 및 초기화
0,
};
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);
printf("Actors:\n");
printf(" %s\n", json.tokens[7].string);
printf(" %s\n", json.tokens[8].string);
printf(" %s\n", json.tokens[9].string);
printf(" %s\n", json.tokens[10].string);
printf(" %s\n", json.tokens[11].string);
freeJSON(&json); // json에 할당된 동적 메모리 해제
free(doc); // 문서 동적 메모리 해제
return 0;
}
// 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
과 같이 지정하면 배열의 요소를 출력할 수 있다.
7. JSON에서 숫자 파싱하기
- 지금까지 문자열을 파싱했으니 이번에는 숫자를 파싱해 보자.
- 다음의 그림은 JSON에서 숫자를 파싱하는 과정이다.
- 다음과 같이
example.json
파일에 출시 연도Year
, 상영 시간Runtime
, IMDB 평점imdbRating
키와 값을 추가한다.
{
"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 문서에서 숫자의 시작 부분을 찾는 모습이다.
- 숫자는 앞에
“
가 없으므로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
를 구하는 과정이다.
- 문자열의 실제 길이를 구한 뒤 버퍼에 문자열의 길이
stringLength + NULL
공간만큼 메모리를 할당하고,0
으로 초기화한다.
int stringLength = end - begin; // 문자열의 실제 길이는 끝 위치 - 시작 위치
// 문자열 길이 + NULL 공간만큼 메모리 할당
buffer = malloc(stringLength + 1);
// 할당한 메모리를 0으로 초기화
memset(buffer, 0, stringLength + 1);
- 이제 문서의 문자열을 버퍼에 저장한다.
- 단, 문자열을 그대로 사용할 수는 없으므로
atof
함수로 문자열(buffer
)을 숫자로 변환하여 토큰에 저장한다. - 또한, 토큰 종류에는
TOKEN_NUMBER
를 지정한다. - 모든 처리가 끝났으면 버퍼를 해제하고,
tokenIndex
를1
증가시킨다. - 그리고
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 문서에서 숫자를 토큰에 저장하는 과정이다.
- 이제
main
함수에서 숫자도 함께 출력해 보자.
int main()
{
int size; // 문서 크기
char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
if (doc == NULL)
return -1;
JSON json = { // JSON 구조체 변수 선언 및 초기화
0,
};
parseJSON(doc, size, &json); // JSON 문서 파싱
printf("Title: %s\n", json.tokens[1].string);
printf("Year: %d\n", (int)json.tokens[3].number);
printf("Runtime: %d\n", (int)json.tokens[5].number);
printf("Genre: %s\n", json.tokens[7].string);
printf("Director: %s\n", json.tokens[9].string);
printf("Actors:\n");
printf(" %s\n", json.tokens[11].string);
printf(" %s\n", json.tokens[12].string);
printf(" %s\n", json.tokens[13].string);
printf(" %s\n", json.tokens[14].string);
printf(" %s\n", json.tokens[15].string);
printf("imdbRating: %f\n", json.tokens[17].number);
freeJSON(&json); // json에 할당된 동적 메모리 해제
free(doc); // 문서 동적 메모리 해제
return 0;
}
// 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
은 실수를 그대로 출력했다.
8. 키로 값을 가져오는 함수 작성하기
- 지금까지 토큰 배열에 인덱스를 지정하여 값을 가져왔다.
- 이번에는 키를 지정하여 값을 가져오는 함수를 만들어보자.
- 먼저 키에 해당하는 문자열을 가져오는 함수인데 동작 방법은 간단하다.
- 토큰 개수만큼 반복하면서 토큰 종류가 문자열이면서 키와 일치하는지 검사한다.
- 일치한다면 바로 뒤의 토큰(
i + 1
)이 문자열인지 확인한 뒤 문자열을 반환한다. - 즉, JSON은 키-값 형식으로 구성되어 있으므로 토큰에서 키 바로 뒤의 요소는 항상 값이다.
char *getString(JSON *json, char *key) // 키에 해당하는 문자열을 가져오는 함수
{
for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
{
// 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치하면
if (json->tokens[i].type == TOKEN_STRING &&
strcmp(json->tokens[i].string, key) == 0)
{
// 바로 뒤의 토큰(i + 1)이 문자열이면
if (json->tokens[i + 1].type == TOKEN_STRING)
return json->tokens[i + 1].string; // 바로 뒤에 있는 토큰의 문자열 반환
}
}
return NULL; // 키를 찾지 못했으면 NULL을 반환
}
- 다음은 키에 해당하는 배열 중 인덱스를 지정하여 문자열을 가져오는 함수이다.
- 토큰에서 키 바로 뒤(
i + 1
)부터 배열의 요소가 나열되는데 여기에index
를 더하면 해당 요소를 가져올 수 있다. - 단, 배열의 요소를 가져오기 전에
isArray
가true
인지 확인해야 한다. - 그렇지 않으면 배열의 범위를 넘어서 다른 키 문자열을 가져오게 된다.
// 키에 해당하는 배열 중 인덱스를 지정하여 문자열을 가져오는 함수
char *getArrayString(JSON *json, char *key, int index)
{
for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
{
// 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치한다면
if (json->tokens[i].type == TOKEN_STRING &&
strcmp(json->tokens[i].string, key) == 0)
{
// 바로 뒤의 토큰(i + 1)부터 배열의 요소
// 인덱스를 지정한 토큰이 문자열이면서 배열이면
if (json->tokens[i + 1 + index].type == TOKEN_STRING &&
json->tokens[i + 1 + index].isArray == true)
return json->tokens[i + 1 + index].string; // 해당 토큰의 문자열 반환
}
}
return NULL; // 키를 찾지 못했으면 NULL을 반환
}
- 배열의 요소를 가져오려면 요소의 개수를 알아내는 것이 좀 더 편리하다.
- 다음은 키에 해당하는 문자열 배열의 요소 개수를 구하는 함수이다.
- 요소의 개수는 키 바로 뒤의 토큰(
i + 1
)부터isArray
가true
인 개수를 세어서 반환하면 된다.
int getArrayCount(JSON *json, char *key) // 키에 해당하는 배열의 요소 개수를 구하는 함수
{
for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
{
// 토큰 종류가 문자열이면서 토큰의 문자열이 키와 일치한다면
if (json->tokens[i].type == TOKEN_STRING &&
strcmp(json->tokens[i].string, key) == 0)
{
// 바로 뒤의 토큰(i + 1)부터 isArray가 true인 토큰의 개수를 세어서 반환
int j = 0;
while (json->tokens[i + 1 + j].isArray == true)
j++;
return j;
}
}
return 0; // 키를 찾지 못했으면 0을 반환
}
- 다음은 키에 해당하는 숫자를 가져오는 함수이다.
- 키 바로 뒤의 토큰(
i + 1
)이 숫자이면 해당 값을 반환하면 된다.
double getNumber(JSON *json, char *key) // 키에 해당하는 숫자를 가져오는 함수
{
for (int i = 0; i < TOKEN_COUNT; i++) // 토큰 개수만큼 반복
{
// 토큰 종류가 숫자이면서 토큰의 문자열이 키와 일치한다면
if (json->tokens[i].type == TOKEN_STRING &&
strcmp(json->tokens[i].string, key) == 0)
{
// 바로 뒤의 토큰(i + 1)이 숫자이면
if (json->tokens[i + 1].type == TOKEN_NUMBER)
return json->tokens[i + 1].number; // 바로 뒤에 있는 토큰의 숫자 반환
}
}
return 0.0; // 키를 찾지 못했으면 0.0을 반환
}
- 이제
main
함수에서 지금까지 만든 함수를 사용해서 값을 출력해 보자.
int main()
{
int size; // 문서 크기
char *doc = readFile("./example.json", &size); // 파일에서 JSON 문서를 읽음, 문서 크기를 구함
if (doc == NULL)
return -1;
JSON json = { // JSON 구조체 변수 선언 및 초기화
0,
};
parseJSON(doc, size, &json); // JSON 문서 파싱
printf("Title: %s\n", getString(&json, "Title")); // Title의 값 출력
printf("Year: %d\n", (int)getNumber(&json, "Year")); // Year의 값 출력
printf("Runtime: %d\n", (int)getNumber(&json, "Runtime")); // Runtime의 값 출력
printf("Genre: %s\n", getString(&json, "Genre")); // Genre의 값 출력
printf("Director: %s\n", getString(&json, "Director")); // Director의 값 출력
printf("Actors:\n");
int actors = getArrayCount(&json, "Actors"); // Actors 배열의 개수를 구함
for (int i = 0; i < actors; i++) // 배열의 요소 개수만큼 반복
printf(" %s\n", getArrayString(&json, "Actors", i)); // 인덱스를 지정하여 문자열을 가져옴
printf("imdbRating: %f\n", getNumber(&json, "imdbRating")); // imdbRating의 값 출력
freeJSON(&json); // json에 할당된 동적 메모리 해제
free(doc); // 문서 동적 메모리 해제
return 0;
}
// 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은 대소문자를 구분하므로
Title
과title
은 다른 키이다. - 그러므로
getString(&json, “Title”)
과 같이 대소문자를 확실하게 지정해 줘야 한다.
9. JSON 파일 쓰기
- 지금까지 JSON 파일의 내용을 읽고 분석하여 값을 출력했다.
- 이번에는 반대로 프로그램에서 JSON 문서를 생성해 보자.
- 다음의 소스 코드를 입력한 뒤 실행해 보자.
- 단순히 JSON 형식에 맞춰서 출력하므로 코드가 간단하다.
#define _CRT_SECURE_NO_WARNINGS // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h> // 파일 처리 함수가 선언된 헤더 파일
int main()
{
// JSON 문서에 저장할 데이터
char *title = "Inception";
int year = 2010;
int runtime = 148;
char *genre = "Sci-Fi";
char *director = "Christopher Nolan";
char actors[5][30] = {
"Leonardo DiCaprio",
"Joseph Gordon-Levitt",
"Ellen Page",
"Tom Hardy",
"Ken Watanabe"};
double imdbRating = 8.8;
FILE *fp = fopen("example.json", "w"); // 쓰기 모드로 파일 열기
// JSON 문법에 맞춰서 fprintf 함수로 값 출력
fprintf(fp, "{\n");
fprintf(fp, " \"Title\": \"%s\",\n", title);
fprintf(fp, " \"Year\": %d,\n", year);
fprintf(fp, " \"Runtime\": %d,\n", runtime);
fprintf(fp, " \"Genre\": \"%s\",\n", genre);
fprintf(fp, " \"Director\": \"%s\",\n", director);
fprintf(fp, " \"Actors\": [\n");
fprintf(fp, " \"%s\", \n", actors[0]);
fprintf(fp, " \"%s\", \n", actors[1]);
fprintf(fp, " \"%s\", \n", actors[2]);
fprintf(fp, " \"%s\", \n", actors[3]);
fprintf(fp, " \"%s\" \n", actors[4]);
fprintf(fp, " ],\n");
fprintf(fp, " \"imdbRating\": %.1f\n", imdbRating);
fprintf(fp, "}\n");
fclose(fp); // 파일 닫기
return 0;
}
- 프로그램을 실행하면
.c
파일이 있는 폴더에 다음과 같은 내용으로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 문서를 만드는 방법은 간단하다.
- JSON 문법에 맞춰서
fprintf
함수로 파일에 출력만 해주면 된다. - 여기서 주의할 점은
“”
안에“
를 사용하려면\”
와 같이 앞에\
를 붙여줘야 한다. - 지금까지 작성한 JSON 파서는 다음과 같은 한계를 가지고 있다.
1] 고정된 토큰 개수
2] 불 값(true
, false
) 파싱이 구현되지 않음
3] 숫자, 불 배열 파싱이 구현되지 않음
4] 처음부터 배열로 시작하는 문서를 지원하지 않음
5] 객체 안에 객체 또는 배열 들어가는 문법을 지원하지 않음
6] 배열 안에 객체 또는 배열이 들어가는 문법을 지원하지 않음
7] 키의 값을 가져올 때 매번 모든 토큰을 검사하는 비효율적인 구조
- 구문 분석(파서) 분야는 생각보다 복잡하고 난이도가 높은 분야이다.
- 현재는 C로 구현된 JSON 파서가 많이 나와있다.
- 이런 상태에서 JSON을 완벽하게 처리하는 파서를 다시 구현하는 일은 크게 의미가 없다.
References
- https://dojang.io/mod/page/view.php?id=720
- https://dojang.io/mod/page/view.php?id=721
- https://dojang.io/mod/page/view.php?id=722
- https://dojang.io/mod/page/view.php?id=724
- https://dojang.io/mod/page/view.php?id=725
- https://dojang.io/mod/page/view.php?id=726
- https://dojang.io/mod/page/view.php?id=727
- https://dojang.io/mod/page/view.php?id=728