Json-Tutorial03 字符串解析

前言

学习了Json字面量解析和数值解析后,本节我们将完成Json的单个字符串解析。Json中的字符串是以""修饰的。

代码设计

1. 编写 lept_get_boolean() 等访问函数的单元测试,然后实现。

要解决这个问题,首先我们得弄明白lept_get_xxx(const lept_value* v)函数是用来干什么的。顾名思义,就是将Json Value v的值转化为C语言的内置变量。略微观察一下这一节的lept_value结构跟上一节有什么区别。

typedef struct {
    union {
        struct { char* s; size_t len; }s;  /* string: null-terminated string, string length */
        double n;                          /* number */
    }u;
    lept_type type;
}lept_value;

这里有一个union的结构体,用法跟struct差不多,但是struct的各个成员在结构中都具有单独的内存位置,union成员则共享同一个内存位置,也就是说联合中的所有成员都是从相同的内存地址开始。这里将字符串值s和数值n放在一个union结构中,可以节省内存,因为一个Json Value不可能同时属于字符串和数值类型。下图左边struct的内存结构,右边是union的内存结构。
img

所以,letp_get_xxxlept_set_xxx的实现分别如下:

int lept_get_boolean(const lept_value* v) {
    assert(v != NULL && (v->type == LEPT_FALSE || v->type == LEPT_TRUE));
    /*0 for false, 1 for true*/
    return v->type == LEPT_TRUE;
}

void lept_set_boolean(lept_value* v, int b) {
    lept_free(v);
    v->type = (b ? LEPT_TRUE : LEPT_FALSE);
}

double lept_get_number(const lept_value* v) {
    assert(v != NULL && v->type == LEPT_NUMBER);
    return v->u.n;
}

void lept_set_number(lept_value* v, double n) {
    lept_free(v);
    v->type = LEPT_NUMBER;
    v->u.n = n;
}

这里要注意的是,在set方法的开始,必须要调用一下lept_free函数,对v进行reset,该初始化的初始化,该释放的资源也得释放(堆上的char[]),否则会有资源泄漏的问题。

2. 实现除了 \u 以外的转义序列解析,令test_parse_string()中所有测试通过

test_parse_string()主要涉及的函数是lept_parse_string(lept_context* c, lept_value* v)。先来分析一下它的结构:

static int lept_parse_value(lept_context* c, lept_value* v) {
    switch (*c->json) {
        case 't':  return lept_parse_literal(c, v, "true", LEPT_TRUE);
        case 'f':  return lept_parse_literal(c, v, "false", LEPT_FALSE);
        case 'n':  return lept_parse_literal(c, v, "null", LEPT_NULL);
        default:   return lept_parse_number(c, v);
        case '"':  return lept_parse_string(c, v);
        case '\0': return LEPT_PARSE_EXPECT_VALUE;
    }
}

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);
                c->json = p;
                return LEPT_PARSE_OK;
            case '\0':
                c->top = head;
                return LEPT_PARSE_MISS_QUOTATION_MARK;
            default:
                PUTC(c, ch);
        }
    }
}

首先判断要解析的字符串的第一个字符,如果是"的话,我们就认为这是一个Json字符串的起始标志。注意,单引号内部使用双引号,不需要加上转义字符\。然后,在lept_parse_string内部对后续的字符串序列做一个循环,如果:

  • 是双引号。认为一个字符串结束,将栈内暂存的解析完的字符全部pop出来到v内。
  • 是结束符'\0'。此时,说明已经遍历到字符串最后了,并且从没遇到过",因为一旦遇到就会return,不会走到这个分支。
  • default。其他情况就将字符串入栈。

这里为什么要用一个栈结构来存字符串呢?
我们解析字符串(以及之后的数组、对象)时,因为有可能会解析失败,所以需要把解析的结果先储存在一个临时的缓冲区,最后再用 lept_set_string() 把缓冲区的结果设进值之中。在完成解析一个字符串之前,这个缓冲区的大小是不能预知的。因此,我们可以采用动态数组(dynamic array)这种数据结构,即数组空间不足时,能自动扩展。C++ 标准库的 std::vector 也是一种动态数组。
如果每次解析字符串时,都重新建一个动态数组,那么是比较耗时的。我们可以重用这个动态数组,每次解析 JSON 时就只需要创建一个。而且我们将会发现,无论是解析字符串、数组或对象,我们也只需要以先进后出的方式访问这个动态数组。换句话说,我们需要一个动态的堆栈(stack)数据结构。

回看lept_parse_string函数,没有考虑到转义字符的情况,我们所要做的就是补上两种情况:

  • 转义正确
  • 转义错误

转义正确的情况一共有tutorial所说的以下几种(escape):

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

如果不包括\u开头的Unicode编码,我们只需要考虑前面8种转义字符。首先判断\\字符,如果首次出现则表示下一个字符是转义字符。再判断下一个字符如果在合法的转义字符范围内,则入栈,否则,返回ESCAPE错误码。

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);
                c->json = p;
                return LEPT_PARSE_OK;
            case '\0':
                c->top = head;
                return LEPT_PARSE_MISS_QUOTATION_MARK;
            case '\\':
                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:
                        /* Parse failed, move top to the original position and return. */
                        c->top = head;
                        return LEPT_PARSE_INVALID_STRING_ESCAPE;
                }
                break;
            default:
                PUTC(c, ch);
        }
    }
}

3. 解决 test_parse_invalid_string_escape()test_parse_invalid_string_char() 中的失败测试。

我们来看这俩测试用例:

static void test_parse_invalid_string_escape() {
#if 1
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\v\"");
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\'\"");
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\0\"");
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE, "\"\\x12\"");
#endif
}

static void test_parse_invalid_string_char() {
#if 1
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x01\"");
    TEST_ERROR(LEPT_PARSE_INVALID_STRING_CHAR, "\"\x1F\"");
#endif
}

对于"\"\\v\""这样的字符串,Json在解析时,会将\"\\都分别看作一个字符,而不是两个。举个例子,解析字符串"\"\\v\""时,只会解析3个字符。所以,在解析完\\字符后,C语言程序读取的实际上只是一个\符号,此时已经进入了转义语义的解析,由于我们只有8个转义字符是合法的(不算\u),这里的v将被认为是非法,所以直接返回ESCAPE错误码。这个测试用例在上一小节已经被修复了:
img

对于"\"\x01\""这样的字符串,我们在解析时如果在双引号内部遇到了\x _ _这样的形式,则说明这是一个16进制的ASCII字符,\x01转换成16进制其实就是1,与ASCII码表对应可以发现,这并不是一个可读字符,所以认为是非法INVALID_STRING_CHAR
img
从ASCII码表中来看,小于32的值都是非法的,正好对应了tutorial中unescaped = %x20-21 / %x23-5B / %x5D-10FFFF的范围。我们在本节只需要判断小于\x20就可以。完整的字符串解析函数如下:

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);
                c->json = p;
                return LEPT_PARSE_OK;
            case '\0':
                c->top = head;
                return LEPT_PARSE_MISS_QUOTATION_MARK;
            case '\\':
                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:
                        /* Parse failed, move top to the original position and return. */
                        c->top = head;
                        return LEPT_PARSE_INVALID_STRING_ESCAPE;
                }
                break;
            default:
                /* 0x20 is the smallest ASCII symbol(space) -- 32
                 * This chapter is only refer to ASCII symbols,
                 * so a char (8bit) is enough to describe a symbol.*/
                if ((unsigned char)ch < 0x20) {
                    c->top = head;
                    return LEPT_PARSE_INVALID_STRING_CHAR;
                }
                PUTC(c, ch);
        }
    }
}

4. 栈设计

顺便来看一下这里栈的设计,有两个关键函数:lept_context_poplept_context_push。入栈时可以直接用宏PUTC

#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)
            /*如果字符串大小超过了栈容量,扩容至原容量的1.5倍,不够就再扩*/
            c->size += c->size >> 1;  /* c->size * 1.5 */
        /*realloc重新分配内存*/
        c->stack = (char*)realloc(c->stack, c->size);
    }
    ret = c->stack + c->top;
    c->top += size;
    /*本次入栈的字符串:[ret, c->top]*/
    return ret;
}

typedef struct {
    const char* json;
    /*字符栈*/
    char* stack;
    /*栈大小、栈顶位置*/
    size_t size, top;
}lept_context;

static void* lept_context_pop(lept_context* c, size_t size) {
    assert(c->top >= size);
    /*本次出栈的字符串:[c->stack + (c->top -= size), ]*/
    return c->stack + (c->top -= size);
}

方法的含义基本上都在注释中写明白了,需要强调的一点是,这里的栈所说的先进后出指的是整个字符串的进出,而不是单个字符的进出。

posted @ 2023-01-04 11:26  南小小小小乔  阅读(54)  评论(0编辑  收藏  举报