從零開始的 JSON 庫教程(六):解析對象解答篇

本文是《從零開始的 JSON 庫教程》的第六個單元解答篇。解答代碼位於 json-tutorial/tutorial06_answer。

(題圖 Photo by Gianni Scognamiglio)

1. 重構 lept_parse_string()

這個「提取方法」重構練習很簡單,只需要把原來調用 lept_set_string 的地方,改為寫入參數變數。因此,原來的 lept_parse_string() 和 答案中的 lept_parse_string_raw() 的 diff 僅是兩處:

130,131c130,131n< static int lept_parse_string(lept_context* c, lept_value* v) {n< size_t head = c->top, len;n---n> static int lept_parse_string_raw(lept_context* c, char** str, size_t* len) {n> size_t head = c->top;n140,141c140,141n< len = c->top - head;n< lept_set_string(v, (const char*)lept_context_pop(c, len), len);n---n> *len = c->top - head;n> *str = lept_context_pop(c, *len);n

以 TDD 方式開發軟體,因為有單元測試確保軟體的正確性,面對新需求可以安心重構,改善軟體架構。

2. 實現 lept_parse_object()

有了 lept_parse_array() 的經驗,實現 lept_parse_object() 幾乎是一樣的,分別只是每個迭代要多處理一個鍵和冒號。我們把這個實現過程分為 5 步曲。

第 1 步是利用剛才重構出來的 lept_parse_string_raw() 去解析鍵的字元串。由於 lept_parse_string_raw() 假設第一個字元為 ",我們要先作校檢,失敗則要返回 LEPT_PARSE_MISS_KEY 錯誤。若字元串解析成功,它會把結果存儲在我們的棧之中,需要把結果寫入臨時 lept_member 的 k 和 klen 欄位中:

static int lept_parse_object(lept_context* c, lept_value* v) {n size_t i, size;n lept_member m;n int ret;n /* ... */n m.k = NULL;n size = 0;n for (;;) {n char* str;n lept_init(&m.v);n /* 1. parse key */n if (*c->json != ") {n ret = LEPT_PARSE_MISS_KEY;n break;n }n if ((ret = lept_parse_string_raw(c, &str, &m.klen)) != LEPT_PARSE_OK)n break;n memcpy(m.k = (char*)malloc(m.klen + 1), str, m.klen);n m.k[m.klen] = 0;n /* 2. parse ws colon ws */n /* ... */n }n /* 5. Pop and free members on the stack */n /* ... */n}n

第 2 步是解析冒號,冒號前後可有空白字元:

/* 2. parse ws colon ws */n lept_parse_whitespace(c);n if (*c->json != :) {n ret = LEPT_PARSE_MISS_COLON;n break;n }n c->json++;n lept_parse_whitespace(c);n

第 3 步是解析任意的 JSON 值。這部分與解析數組一樣,遞歸調用 lept_parse_value(),把結果寫入臨時 lept_member 的 v 欄位,然後把整個 lept_member 壓入棧:

/* 3. parse value */n if ((ret = lept_parse_value(c, &m.v)) != LEPT_PARSE_OK)n break;n memcpy(lept_context_push(c, sizeof(lept_member)), &m, sizeof(lept_member));n size++;n m.k = NULL; /* ownership is transferred to member on stack */n

但有一點要注意,如果之前缺乏冒號,或是這裡解析值失敗,在函數返回前我們要釋放 m.k。如果我們成功地解析整個成員,那麼就要把 m.k 設為空指針,其意義是說明該鍵的字元串的擁有權已轉移至棧,之後如遇到錯誤,我們不會重覆釋放棧里成員的鍵和這個臨時成員的鍵。

第 4 步,解析逗號或右花括弧。遇上右花括弧的話,當前的 JSON 對象就解析完結了,我們把棧上的成員複製至結果,並直接返回:

/* 4. parse ws [comma | right-curly-brace] ws */n lept_parse_whitespace(c);n if (*c->json == ,) {n c->json++;n lept_parse_whitespace(c);n }n else if (*c->json == }) {n size_t s = sizeof(lept_member) * size;n c->json++;n v->type = LEPT_OBJECT;n v->u.o.size = size;n memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);n return LEPT_PARSE_OK;n }n else {n ret = LEPT_PARSE_MISS_COMMA_OR_CURLY_BRACKET;n break;n }n

最後,當 for (;;) 中遇到任何錯誤便會到達這第 5 步,要釋放臨時的 key 字元串及棧上的成員:

/* 5. Pop and free members on the stack */n free(m.k);n for (i = 0; i < size; i++) {n lept_member* m = (lept_member*)lept_context_pop(c,n sizeof(lept_member));n free(m->k);n lept_free(&m->v);n }n v->type = LEPT_NULL;n return ret;n

注意我們不需要先檢查 m.k != NULL,因為 free(NULL) 是完全合法的。

3. 釋放內存

使用工具檢測內存泄漏時,我們會發現以下這行代碼造成內存泄漏:

memcpy(v->u.o.m = (lept_member*)malloc(s), lept_context_pop(c, s), s);n

類似數組,我們也需要在 lept_free() 釋放 JSON 對象的成員(包括鍵及值):

void lept_free(lept_value* v) {n size_t i;n assert(v != NULL);n switch (v->type) {n /* ... */n case LEPT_OBJECT:n for (i = 0; i < v->u.o.size; i++) {n free(v->u.o.m[i].k);n lept_free(&v->u.o.m[i].v);n }n free(v->u.o.m);n break;n default: break;n }n v->type = LEPT_NULL;n}n

4. 總結

至此,你已實現一個完整的 JSON 解析器,可解析任何合法的 JSON。統計一下,不計算空行及注釋,現時 leptjson.c 只有 405 行代碼,lept_json.h 54 行,test.c 309 行。

另一方面,一些程序也需要生成 JSON。也許最初讀者會以為生成 JSON 只需直接 sprintf()/fprintf() 就可以,但深入了解 JSON 的語法之後,我們應該知道 JSON 語法還是需做一些處理,例如字元串的轉義、數字的格式等。然而,實現生成器還是要比解析器容易得多。而且,假設我們有一個正確的解析器,可以簡單使用 roundtrip 方式實現測試。請期待下回分解。

如果你遇到問題,有不理解的地方,或是有建議,都歡迎在評論或 issue 中提出,讓所有人一起討論。

5. 更新

  • 2016/11/22 @ray好 發現了 lept_parse_object() 中,拷貝鍵的字元串時,應該加入 0。已在此 commt 修正。

推薦閱讀:

「每日一題」JSON 是什麼?
如何從CFG轉換到State Machine?
乾貨 | 劫持各個瀏覽器中的JSON漏洞
【原創】R語言轉換並保存json文件--使用jsonlite包
json 在 Python 爬蟲的應用

TAG:JSON | C编程语言 | 教程 |