- 写在前面:该系列内容重点用于复习C的知识点,将整合叶劲峰老师的《从零开始的JSON库教程》等内容作为个人学习的笔记。
- 学习地址:从零开始的 JSON 库教程 - 知乎
- 参考地址:从零开始的 JSON 库教程(一):启程 - 知乎
目录
一、JSON(JavaScript Object Notation)
一、JSON(JavaScript Object Notation)是一个用于数据交换的文本格式,现时的标准为ECMA-404。
例如,一个动态网页想从服务器获得数据时,服务器从数据库查找数据,然后把数据转换成 JSON 文本格式:
-
null: 表示为 null
-
boolean: 表示为 true 或 false
-
number: 一般的浮点数表示方式,在下一单元详细说明
-
string: 表示为 "..."
-
array: 表示为 [ ... ]
-
object: 表示为 { ... }
因此,对于JSON的语法子集,可使用RFC7159中的ABNF表示:
JSON-text = ws value ws
ws = *(%x20 / %x09 / %x0A / %x0D)//空格、制表、换行、回车
value = null / false / true
null = "null"
false = "false"
true = "true"
以下是网页中的JSON实例:
{
"title": "Design Patterns",
"subtitle": "Elements of Reusable Object-Oriented Software",
"author": [
"Erich Gamma",
"Richard Helm",
"Ralph Johnson",
"John Vlissides"
],
"year": 2009,
"weight": 1.8,
"hardcover": true,
"publisher": {
"Company": "Pearson Education",
"Country": "India"
},
"website": null
}
通过网页的脚本代码就可以获取以上 JSON 文本解析为内部的数据结构去使用。
二、项目需求
我们要实现的 JSON 库,主要是完成 3 个需求:
把 JSON 文本解析为一个树状数据结构(parse);
提供接口访问该数据结构(access);
把数据结构转换成 JSON 文本(stringify)。
我们会逐步实现这些需求。因此,我们需要设计两个API(应用程序编程接口)函数。第一个用于解析JSON:
/* 提示:这里应该是 JSON-text = ws value ws,*/
/* 以下实现没处理最后的 ws 和 LEPT_PARSE_ROOT_NOT_SINGULAR */
int lept_parse(lept_value* v, const char* json){
lept_context c;
int ret;// 用以保存解析结果
assert(v != NULL);
c.json = json;
v->type = LEPT_NULL;
lept_parse_whitespace(&c);
if((ret = lept_parse_value(&c, v)) == LEPT_PARSE_OK){
lept_parse_whitespace(&c);
if(*c.json!='\0')
ret = LEPT_PARSE_ROOT_NOT_SINGULAR;
}
return ret;
}
三、宏的编写技巧
当一个宏中含有多个语句时,就需要用do{…}while(0)语句包裹成单个语句,用反斜杠\表示当前语句未结束
//宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句
//反斜杠表示该行未结束
#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)
拓展:关于为何使用宏而非使用函数,给出如下解释:
- 《C Primer Plus》中P530:
- 《C与指针》中给出更详细的对比:
四、单元测试
当软件项目越来越复杂,用printf/cout打印结果肉眼测试的做法会越来越低效。一般我们会采用自动的测试方式,例如单元测试(unit testing)。单元测试也能确保其他人修改代码后,原来的功能维持正确(这称为回归测试/regression testing)。
①使用3个全局变量来进行测试结果的展示:
static int main_ret = 0;
static int test_count = 0;
static int test_pass = 0;
②定义EXPECT_EQ_BASE为宏,测试前对test_count先+1,若测试通过test_pass+1,不通过则通过stderr标准错误流直接打印至屏幕上。
关于宏可能疑惑的地方:
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
stderr译为标准错误,我们知道fprintf第一个参数FILE *stream用于流的定向,而标准错误默认都是将信息输出到终端上,所以意味着fprintf会将信息打印到屏幕上。
第二个参数char *format格式字符串,按照常理应该是“”双引号包围的字符串,但是在上述语句有多个双引号。我们先将宏展开参数带入(假设EXPECT_EQ_BASE中format参数为“%d”)
"%s:%d: expect: " format " actual: " format "\n"
在此有一个规则是在C语言中字符串中的二个相连的双引号会被自动忽略,于是上句等同于
"%s:%d: expect: %d actual: %d\n"
这样就写成熟悉的格式字符串的形式。
__FILE__和 __LINE__是内置的宏定义,用于打印在源文件中插入当前源文件名和在源代码中插入当前源代码行号。所以EXPECT_EQ_INT宏译为检测两个参数是否一致,如果不一致,打印消息(内容如fprintf所示)
具体的实现代码如下:
//宏里有多过一个语句(statement),就需要用 do { /*...*/ } while(0) 包裹成单个语句
//反斜杠表示该行未结束
//将单元测试定义为宏
#define EXPECT_EQ_BASE(equality, expect, actual, format) \
do {\
test_count++;\
if (equality)\
test_pass++;\
else {\
fprintf(stderr, "%s:%d: expect: " format " actual: " format "\n", __FILE__, __LINE__, expect, actual);\
main_ret = 1;\
}\
} while(0)
#define EXPECT_EQ_INT(expect, actual) EXPECT_EQ_BASE((expect) == (actual), expect, actual, "%d")
③具体的单元测试
在这个 JSON 语法子集下,我们定义 3 种错误码:
- 若一个 JSON 只含有空白,传回 LEPT_PARSE_EXPECT_VALUE。
- 若一个值之后,在空白之后还有其他字符,传回 LEPT_PARSE_ROOT_NOT_SINGULAR。
- 若值不是那三种字面值,传回 LEPT_PARSE_INVALID_VALUE。
//单元测试:用于测试对null的解析是否正确
static void test_parse_null() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_OK, lept_parse(&v, "null"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
//单元测试:测试——一个json中是否只含有空白
static void test_parse_expect_value() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, ""));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_EXPECT_VALUE, lept_parse(&v, " "));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
//单元测试:测试——对非合法值invalid_value的解析是否正确
static void test_parse_invalid_value() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "nul"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_INVALID_VALUE, lept_parse(&v, "?"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
//单元测试:测试——若 value 不是那三种字面值
static void test_parse_root_not_singular() {
lept_value v;
v.type = LEPT_FALSE;
EXPECT_EQ_INT(LEPT_PARSE_ROOT_NOT_SINGULAR, lept_parse(&v, "null x"));
EXPECT_EQ_INT(LEPT_NULL, lept_get_type(&v));
}
static void test_parse() {
test_parse_null();
/* ... */
test_parse_expect_value();
test_parse_invalid_value();
test_parse_root_not_singular();
}
int main() {
test_parse();
printf("%d/%d (%3.2f%%) passed\n", test_pass, test_count, test_pass * 100.0 / test_count);
system("pause");
return main_ret;
}
五、解析器原理
lept_parse() 若失败,会把 v 设为 null 类型,所以这里先把它设为 null,让 lept_parse_value() 写入解析出来的根值。leptjson 是一个手写的递归下降解析器(recursive descent parser)。由于 JSON 语法特别简单,我们不需要写分词器(tokenizer),只需检测下一个字符,便可以知道它是哪种类型的值,然后调用相关的分析函数。对于完整的 JSON 语法,跳过空白后,只需检测当前字符:
n ➔ null
t ➔ true
f ➔ false
" ➔ string
0-9/- ➔ number
[ ➔ array
{ ➔ object
所以,我们可以将以上语法简单地翻译成解析函数:
#define EXPECT(c, ch) do { assert(*c->json == (ch)); c->json++; } while(0)
/* ws = *(%x20 / %x09 / %x0A / %x0D) */
/* lept_parse_whitespace()是不会出现错误的 */
static void lept_parse_whitespace(lept_context* c) {
const char *p = c->json;
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')
p++;
c->json = p;
}
/* true = "true" */
static int lept_parse_true(lept_context *c, lept_value *v){
EXPECT(c, 't');
if (c->json[0] != 'r' || c->json[1] != 'u' || c->json[2] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_TRUE;
return LEPT_PARSE_OK;
}
/* false = "false */
static int lept_parse_false(lept_context *c, lept_value* v){
EXPECT(c, 'f');
if(c->json[0] != 'a' || c->json[1] != 'l' || c->json[2] != 's' || c->json[3] != 'e')
return LEPT_PARSE_INVALID_VALUE;
c->json += 4;
v->type = LEPT_FALSE;
return LEPT_PARSE_OK;
}
/* null = "null" */
static int lept_parse_null(lept_context *c, lept_value *v)
{
EXPECT(c, 'n');
if (c->json[0] != 'u' || c->json[1] != 'l' || c->json[2] != 'l')
return LEPT_PARSE_INVALID_VALUE;
c->json += 3;
v->type = LEPT_NULL;
return LEPT_PARSE_OK;
}
/* value = null / false / true */
static int lept_parse_value(lept_context* c, lept_value* v) {
switch (*c->json) {
case 't': return lept_parse_true(c, v);
case 'f': return lept_parse_false(c, v);
case 'n': return lept_parse_null(c, v);
case '\0': return LEPT_PARSE_EXPECT_VALUE;
default: return LEPT_PARSE_INVALID_VALUE;
}
}