第三节是解析字符串模块:
解析字符串的难点主要是转义字符和双引号的处理,在json字符串中和C语言字符串中都用双引号把字符括起来,那如何在C语言中处理json字符串?就要引入转义字符\:
string = quotation-mark *char quotation-mark
char = unescaped /
escape (
%x22 / ; " quotation mark U+0022
%x5C / ; \ reverse solidus U+005C
%x2F / ; / solidus U+002F
%x62 / ; b backspace U+0008
%x66 / ; f form feed U+000C
%x6E / ; n line feed U+000A
%x72 / ; r carriage return U+000D
%x74 / ; t tab U+0009
%x75 4HEXDIG ) ; uXXXX U+XXXX
escape = %x5C ; \
quotation-mark = %x22 ; "
unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
由于json中的字符串需要在c语言的代码中处理,两者中都包含转义字符等特殊字符,就涉及到表示的问题, 并不是简单加个双引号即可:
json对象:[123,"abc"]
理论上json语句:"[123,"abc"]"
json语句在C语言中表示,这种格式才能成功解析:"[123,\"abc\"]"
看下test.cpp中非法的案例
// 缺少引号
TEST_ERROR(LEPT_PARSE_MISS_QUOTATION_MARK, "\""); // 缺少引号,应该是"\"\""->""
TEST_ERROR(LEPT_PARSE_MISS_QUOTATION_MARK, "\"abc"); // "\"abc\""->"abc"
// 无效的字符串转义
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\v\""); //"\v"不是合法的json字符串
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\'\""); // "\'"不是合法的json字符串
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\0\""); // "\0" 不合法,json不认,认"\u0000",json中字符串不需要\0作为结尾
TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\x12\""); // "\x12"不合法
// 无效的字符值( 0 至 31,",\)
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x01\"");
TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x1F\"");
leptjson.h
lept_value 事实上是一种变体类型(variant type),我们通过 type 来决定它现时是哪种类型,而这也决定了哪些成员是有效的。一个值不可能同时为数字和字符串,因此我们可使用 C 语言的 union 来节省内存:
typedef struct {
union {
struct { char* s; size_t len; }s; // string
double n; // number
}u;
lept_type type;
}lept_value;
新的解析结果返回值
enum {
LEPT_PARSE_OK = 0,
LEPT_PARSE_EXPECT_VALUE,
LEPT_PARSE_INVALID_VALUE,
LEPT_PARSE_ROOT_NOT_SINGULAR,
LEPT_PARSE_NUMBER_TOO_BIG,
LEPT_PARSE_MISS_QUOTATION_MARK, // 缺少引号
LEPT_PARSE_INVALID_STRING_ESCAPE, // 无效字符串转义
LEPT_PARSE_INVALID_STRING_CHAR // 无效的字符
};
leptjson.cpp
由于json字符串中可能包含转义字符等特殊字符,字符串解析结果不可预知,需要使用动态数字char* stack
来临时存储解析结果。
typedef struct {
const char* json;
char* stack;
size_t size, top;
}lept_context;
往json栈中加字符:
先对栈大小进行初始化,如果栈不够大,进行扩展;
然后计算新的栈顶;
返回之前栈顶位置;
PUTC函数中,将ch赋值给之前栈顶位置指针,从而实现加字符。
#define PUTC(c,ch) do{*(char*)lept_context_push(c,sizeof(char))=(ch);}while(0)
static void* lept_context_push(lept_context* c, size_t size) {
void* ret;
assert(size > 0);
if (c->top + size >= c->size) {
if (c->size == 0) {
c->size = LEPT_PARSE_STACK_INIT_SIZE;
}
while (c->top + size >= c->size) {
c->size += c->size >> 1; // c.size*1.5
}
c->stack = (char*)realloc(c->stack, c->size);
}
ret = c->stack + c->top; // 目前指向的字符串位置
c->top += size;
return ret;
}
从json栈中取字符
static void* lept_context_pop(lept_context* c, size_t size) {
assert(c->top >= size);
return c->stack + (c->top -= size);
}
解析字符串。
我们只需要先备份栈顶,然后把解析到的字符压栈,最后计算出长度并一次性把所有字符弹出,再设置至值里便可以。
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top, len;
const char* p;
EXPECT(c, '\"'); // 字符串类型,以\"开头
p = c->json;
for (;;) {
char ch = *p++; // 获取一个字符
switch (ch) {
case '\"': // 如果是\", 解析结束
len = c->top - head;
lept_set_string(v, (const char*)lept_context_pop(c, len), len); // 给json对象中的string部分赋值
c->json = p; // json字符串只保留剩余部分
return LEPT_PARSE_OK;
case '\\': // 遇到转义字符,c语言中字符串的\\,表示一个转义字符\
switch (*p++) { // 遇到转义字符就向栈中添加一个字符
case '\"': PUTC(c, '\"'); break;
case '\\': PUTC(c, '\\'); break;
case '/': PUTC(c, '/'); break;
case 'b': PUTC(c, '\b'); break;
case 'f': PUTC(c, '\f'); break;
case 'n': PUTC(c, '\n'); break;
case 'r': PUTC(c, '\r'); break;
case 't': PUTC(c, '\t'); break;
default: // 如果不是上述符号,则遇到了无效的转义字符
c->top = head; // 这句话似乎无用
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
case '\0': // 遇到了字符串结束负号,则意味着c语言中的字符串结束,json字符串缺少引号
c->top = head;
// "\"abc" , 解析失败,必须栈顶指针置为0
return LEPT_PARSE_MISS_QUOTATION_MARK;
default:
if ((unsigned char)ch < 0x20) { // 遇到了解析不出来的字符
c->top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;
}
PUTC(c, ch); // 可以正常解析的字符
}
}
}
test.cpp
解析过程示例
TEST_STRING("\" \\ / \b \f \n \r \t", "\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"");
//C语言中json语句 "\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\""
//先去掉两端双引号 "\\\" \\\\ \\/ \\b \\f \\n \\r \\t"
//再去掉转义字符 "\" \\ \/ \b \f \n \r \t"
// 即json中的字符串
值得注意的是,中间\\/
的解析
由于C语言中"/"与"/"都是/字符(c语言中/不需要转义,但json中需要)
所以C语言中的json语句""\/""、""/""解析结果也都是/
// 这四个都能通过检测
TEST_STRING("/", "\"\\/\"");
TEST_STRING("\/", "\"\\/\"");
TEST_STRING("\/", "\"\/\"");
TEST_STRING("/", "\"\/\"");