83.3 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 문서에서 문서의 시작 위치를 찾고, 다음 문자를 처리하는 과정입니다.
이제 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 함수로 복사할 크기를 제한해야 합니다.
모든 처리가 끝났으면 토큰 인덱스를 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 = { 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