从零开始的 JSON 库教程(三):解析字符串

119 阅读4分钟

代码地址:github.com/WangHao-nlp…

第三节是解析字符串模块:
解析字符串的难点主要是转义字符和双引号的处理,在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("/", "\"\/\"");

解析结果

教程3.png