4. 字符串解析

4. 字符串解析

章节目录

1. 解析总体流程

2. 类型分辨

3. 数值解析

4. 字符串解析

5. 数组解析

6. 对象解析

4.1 获取字符串的长度

json_value_t​的string是一个未初始化的char**, ​我们需要先知道这个字符串的长度, 初始化这个char才能继续解析.

  • 首先是一个while循环, 去遍历字符串.

  • 由于码数小于空格(32)的是控制字符, 所以如果检测到这种字符的话, 就是非法字符.

    if (*(const unsigned char*)cursor < ' ')
                return -2;
    
  • 然后cursor++, 判断下一个字符.

  • 发现当前字符的前一个字符是转义字符\​, 需要判断当前字符是否合法, 然后cursor++

    cursor++;
    if (cursor[-1] == '\\')
    {
        if (!*cursor)
        return -2;
    
        cursor++;
    }
    

    这里需要注意一下, 转义字符看起来是两个字符\b​实际上在内存中是一个字符, 这样只是为了在代码或字符串中表现出来, 所以在处理转义字符的时候, cursor++了两次, 实际的字符串的长度只++了一次.

static int __json_string_length(const char* cursor)
{
    int len = 0;

    while (1)
    {
        if (*cursor == '\"')
            break;

        if (*(const unsigned char*)cursor < ' ')
            return -2;

        cursor++;
        if (cursor[-1] == '\\')
        {
            if (!*cursor)
                return -2;

            cursor++;
        }

        len++;
    }

    return len;
}

需要特别说一下的是, 在转义字符的判断里面, 由于'\b'​这种形式的转义字符看起来是两个字符, 实际上是一个字符, 因为是为了人看的清楚, 这种形式只是一个表现方式, 所以在实际计算长度的时候需要++两次, 如果转义字符后字符串结束了, 就立刻返回错误(第17行), 不然会进行下次循环, 拿结束符进行判断.

有了以上的分析, 现在把函数__parse_json_value​中的switch中解析string的内容进行一些对应修改:

case '\"':
    cursor++;
    ret = __json_string_length(cursor);
    if (ret < 0)
        return ret;
  
    val->value.string = (char*)malloc(ret + 1);
    if (!val->value.string)
        return -1;
  
    ret = __parse_json_string(cursor, end, val->value.string);
    if (ret < 0)
    {
        free(val->value.string);
        return ret;
    }
  
    val->type = JSON_VALUE_STRING;
    break;

4.2 字符串解析

获取到了长度, 初始化了字符串char*, 现在就正式开始解析字符串.

  • 依旧是while循环, 直到循环到下一个双引号

    while (*cursor != '\"')
    {
        // some code
    }
    
  • 在获取字符串长度的时候, 我们已经将非法的空字符剔除了(直接报错), 所以这里在循环中可以直接将非转义字符直接复制到目标字符串中.

    *str = *cursor;
    

    之后继续++进行下一次循环.

  • 当前字符为转义字符时, 需要特殊处理, 即将显式传入的两个字符的转义字符, 转为内部存储的一个字符的转义字符

    • \\​反斜线本身;
    • \/​正斜线;
    • \"​双引号
    • \b​退格;
    • \f​换页;
    • \n​换行;
    • \r​回车;
    • \t​水平制表符;
    • \u​四位的十六进制数字(Unicode)
            if (*cursor == '\\')
            {
                cursor++;
                switch (*cursor)
                {
                    case '\"':
                        *str = '\"';
                        break;
                    case '\\':
                        *str = '\\';
                        break;
                    case '/':
                        *str = '/';
                        break;
                    case 'b':
                        *str = '\b';
                        break;
                    case 'f':
                        *str = '\f';
                        break;
                    case 'n':
                        *str = '\n';
                        break;
                    case 'r':
                        *str = '\r';
                        break;
                    case 't':
                        *str = '\t';
                        break;
                    case 'u':
                        cursor++;
                        ret = __parse_json_unicode(cursor, &cursor, str);
                        if (ret < 0)
                            return ret;
    
                        str += ret;
                        continue;
    
                    default:
                        return -2;
                }
            }
    

这里解析转义字符大部分都比较简单, 比较复杂的就是对Unicode的解析, 接下来说一下Unicode的解析.

4.3 Unicode解析(\u字符串解析)

Unicode就像一个大表格, 将世界上绝大多数的字符都整理在一个表里, 每一个字符都有其对应的Unicode码, Unicode码的表示范围为从 0 至 0x10FFFF.

Unicode将世界上绝大数的字符收入其中, 最大为0x10FFFF, 为了存储, 需要有三个字节的空间, 但是对于头部的字符, 原本只需要一个字节的字符, 现在为了统一使用三个字符存储, 而且存储的前两个字节都为0, 造成了大量的空间损耗, 所以为了节约计算机资源, 有必要设计一种Unicode的编码方式, 便于高效地存储Unicode编码.

现在国际使用范围最广的便是``​的编码方式, 其实UTF-8就像一个协议, 通过这个协议, 可以实现高效地存储Unicode编码的数据.

而在JSON数据中, 对于使用\u转义的十六进制码, 只读取四位, 但Unicode码的表示范围刚刚说了, 是0 至 0x10FFFF, 很明显不够, 对于在Unicode码中的码位大于0xFFFF的字符, JSON借鉴UTF-16的方式, 使用代理对(surrogate pair)的方法表示, 使用连续的两个四位十六进制数来表示这个字符.

上面简单介绍了一下JSON解析Unicode的方式, 主要为以下几个步骤:

  1. 一次读取四位十六进制数.(不足或超出的为错)
  2. 将读取的Unicode码转为UTF-8​的编码方式.(具体由utf-8编码函数实现)
  3. 对于在原Unicode码中0xFFFF以外的字符, 使用代理对的方式读取, 即读取两个四位十六进制数来读取Unicode再转换为utf-8的编码.

4.3.1 读取四位十六进制数

惯例先声明函数: static int __parse_json_hex4(const char *cursor, const char **end, unsigned int *code)

  • 实现这个函数比较简单, 因为读四位, 循环四次就行了, 主要就是将字符串的数字字符转换成对应的数值.

        int hex;
        int i;
    
        // 初始化code
        *code = 0;
        for (i = 0; i < 4; i++)
        {
            hex = *cursor;
            if (hex >= '0' && hex <= '9')
            {
                hex = hex - '0';
            }
            else if (hex >= 'A' && hex <= 'F')
            {
                hex = hex - 'A' + 10;
            }
            else if (hex >= 'a' && hex <= 'f')
            {
                hex = hex - 'a' + 10;
            }
            else
            {
                return -2;
            }
    
    
        }
    
  • 一次循环完毕, 需要将hex​读入code​中, 这里需要注意, 这里转换出来的hex​是一位十六进制数, 而一位十六进制数是四位二进制数, 所以每次读入code​需要左移四位, 这样才是准确的.

    *code = (*code << 4) + hex;
    cursor++;
    
  • 这样就写完了:

    static int __parse_json_hex4(const char *cursor, const char **end, unsigned int *code)
    {
        int hex;
        int i;
    
        // 初始化code
        *code = 0;
        for (i = 0; i < 4; i++)
        {
            hex = *cursor;
            if (hex >= '0' && hex <= '9')
            {
                hex = hex - '0';
            }
            else if (hex >= 'A' && hex <= 'F')
            {
                hex = hex - 'A' + 10;
            }
            else if (hex >= 'a' && hex <= 'f')
            {
                hex = hex - 'a' + 10;
            }
            else
            {
                return -2;
            }
    
            *code = (*code << 4) + hex;
            cursor++;
        }
    
        *end = cursor;
        return 0;
    }
    

4.3.2 Unicode码转UTF-8

通过__parse_json_hex4​我们读取到了四位的十六进制数code​, 这个code​在大多数情况下就是所谓字符的Unicode码.

如汉字的Unicode码为: U+5F90​, 现在我们就需要将读入的code: 5F90​写入内存.

但上面说了, 这个code只是在大多数情况, 那就是还有例外的情况, 我们先讲讲例外的情况.

4.3.2.1 代理对

在介绍Unicode的时候, 提过JSON读取的时候, 只会读取四位十六进制数,Unicode的表示范围是不止四位十六进制数的, 对于四位十六进制能表示的字符, 上面读到的code就是这个字符的Unicode码, 而四位十六进制数无法表示的字符, code便无法表示这个字符, 因为code位数不够.

代理对就是为了解决这个问题,.

Unicode码在0X0000 ~ 0XFFFF(四位十六进制数), 被称为基本多文种平面(BMP), ​BMP和另外的16个平面(所谓平面其实就是一个码点范围)共同构成了整个Unicode码.

在BMP中, 保留了2048个码点, 作为代理码点, **U+D800**​ ~ ​**U+DBFF**区间, 被称为高半区. ​**U+DC00**​ ~ ​**U+DFFF**被称为低半区. ​

对于四位十六进制数无法表示的Unicode码点, 需要使用代理对的方式表示, 即使用连续的两个四位十六进制数来表示, 这两个连续的四位十六进制数必须为高半区码和低半区码的组合, 且必须以高半区开始.

即对于一个Unicode码超过四位十六进制数的字符, 需要使用\u0xHHHH\u0xLLLL​的形式来表示.(其中0XHHHH表示高半区, 0XLLLL表示低半区).

可以通过一个公式, 来将使用代理对方式表达的字符, 转换为其真实的Unicode码点:

codepoint = 0x10000 + (0xHHHH - 0xD800) * 0x400 + (0xLLLL - 0xDC00)

这里我们只做解析, 所以不用考虑代理对是如何生成的, 详情可以参考UTF-16的编码规则.

现在我们有一个使用代理对方式表示的字符: \uD83E\uDE72​, 来试着将其转为Unicode码吧:

  • 直接代入公式:

    code = 0x10000 + (0xD83E - 0xD800) * 0x400 + (0xDE72 - 0xDC00)

    = 0x10000 + 0x3E * 0x400 + 0x272

    = 0x10000 + 0xF800 + 0x272

    = 0x10000 + 0xFA72

    = 0x1FA72

  • U+0x1FA72​就是其Unicode码点, 字符是:

  • 其实在代码中, 可以使用另一个公式: codepoint = (((0xHHHH & 0x3ff) << 10) | (0xLLLL & 0x3ff)) + 0x10000​, 改为位运算, 效率更高, 计算的结果是一样的.

4.3.2.2 Unicode转UTF-8

现在我们得到了code​, 即字符的Unicode码点, 需要将字符读如char*中, JSON只支持UTF-8编码, 现在我们需要将Unicode码点code​转为UTF-8编码.

先来看看UTF-8的编码规则:

  1. 对于单字节的字符, 字节的第一位设置为0, 后面7位为这个字符的Unicode码. 在这一部分, UTF-8编码和ASCII编码是相同的.
  2. 对于n字节的字符(n > 1), 第一个字节的前n位都设为1, 第n + 1位设置为0, 后面使用到的字节的前两位都设置为10. 剩下的二进制位设置为这个字符的Unicode码.

通过一个表可以更直观的了解:

|Unicode符号范围
|码点位数
|UTF-8编码方式(二进制)||||

字节1 字节2 字节3 字节4
U+0000 ~ U+007F 7 0xxxxxxx
U+0080 ~ 0x07FF 11 110xxxxx 10xxxxxx
U+0800 ~ 0xFFFF 16 1110xxxx 10xxxxxx 10xxxxxx
U+10000 ~ 0x10FFFF 21 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

表中xx位依次填入Unicode码点, 多余的前面补0.

通过这个表, 可以试一下将上面的即U+0x1FA72​转换为UTF-8编码.

0x1FA72 转换为二进制: 0001 1111 1010 0111 0010

0x1FA72在第四个范围, 需要四个字节, 四个字节有21位码点位, 0x1FA72的二进制有20位, 需要在前面补一个0.

得到: 1111 0000 1001 1111 1010 1001 1011 0010 ​(加粗为码点)

将其转为十六进制即为: 0xF09F 0xA9B2

4.3.3 代码实现

讲过上述的基础分析, 现在我们可以用代码来实现了.

先上函数声明: static int __parse_json_unicode(const char *cursor, const char **end, char *utf8)

  • 先读取四位十六进制数

        int ret;
        unsigned int code;
        unsigned int next;
    
        ret = __parse_json_hex4(cursor, end, &code);
        if (ret < 0)
        {
            return ret;
        }
    
  • 如果当前code​处于低半区, 那么是错误的, 因为使用代理对表示的时候, 高半区必须在前

        if (code >= 0xdc00 && code <= 0xdfff)
        {
            return -2;
        }
    
  • 如果code​是高半区, 那么继续读一个四位十六进制数next​, code​和next​构成了一个代理对, 然后把这个代理对转换为Unicode码.

        if (code >= 0xd800 && code <= 0xdbff)
        {
            cursor = *end;
            if (*cursor != '\\')
            {
                return -2;
            }
    
            cursor++;
            if (*cursor != 'u')
            {
                return -2;
            }
    
            cursor++;
            ret = __parse_json_hex4(cursor, end, &next);
            if (ret < 0)
            {
                return ret;
            }
    
            if (next < 0xdc00 || next > 0xdfff)
            {
                return -2;
            }
    
            // 转换为Unicode码
            code = (((code & 0x3ff) << 10) | (next & 0x3ff)) + 0x10000;
        }
    
  • 得到了字符的Unicode码, 将其根据UTF-8标准, 转换为UTF-8的编码

        if (code <= 0x7f)
        {
            utf8[0] = code;
            return 1;
        }
        else if (code <= 0x7ff)
        {
            utf8[0] = 0xc0 | (code >> 6);
            utf8[1] = 0x80 | (code & 0x3f);
            return 2;
        }
        else if (code <= 0xffff)
        {
            utf8[0] = 0xe0 | (code >> 12);
            utf8[1] = 0x80 | ((code >> 6) & 0x3f);
            utf8[2] = 0x80 | (code & 0x3f);
            return 3;
        }
        else
        {
            utf8[0] = 0xf0 | (code >> 18);
            utf8[1] = 0x80 | ((code >> 12) & 0x3f);
            utf8[2] = 0x80 | ((code >> 6) & 0x3f);
            utf8[3] = 0x80 | (code & 0x3f);
            return 4;
        }
    

    这里将转换为UTF-8使用到的字节数返回, 外部指针可增加对应的步长.

完整代码:

static int __parse_json_unicode(const char *cursor, const char **end, char *utf8)
{
    int ret;
    unsigned int code;
    unsigned int next;

    ret = __parse_json_hex4(cursor, end, &code);
    if (ret < 0)
    {
        return ret;
    }

    if (code >= 0xdc00 && code <= 0xdfff)
    {
        return -2;
    }

    if (code >= 0xd800 && code <= 0xdbff)
    {
        cursor = *end;
        if (*cursor != '\\')
        {
            return -2;
        }

        cursor++;
        if (*cursor != 'u')
        {
            return -2;
        }

        cursor++;
        ret = __parse_json_hex4(cursor, end, &next);
        if (ret < 0)
        {
            return ret;
        }

        if (next < 0xdc00 || next > 0xdfff)
        {
            return -2;
        }

        code = (((code & 0x3ff) << 10) | (next & 0x3ff)) + 0x10000;
    }

    if (code <= 0x7f)
    {
        utf8[0] = code;
        return 1;
    }
    else if (code <= 0x7ff)
    {
        utf8[0] = 0xc0 | (code >> 6);
        utf8[1] = 0x80 | (code & 0x3f);
        return 2;
    }
    else if (code <= 0xffff)
    {
        utf8[0] = 0xe0 | (code >> 12);
        utf8[1] = 0x80 | ((code >> 6) & 0x3f);
        utf8[2] = 0x80 | (code & 0x3f);
        return 3;
    }
    else
    {
        utf8[0] = 0xf0 | (code >> 18);
        utf8[1] = 0x80 | ((code >> 12) & 0x3f);
        utf8[2] = 0x80 | ((code >> 6) & 0x3f);
        utf8[3] = 0x80 | (code & 0x3f);
        return 4;
    }
}

4.4 字符串解析代码实现

现在我们实现了解析特殊的带有转义字符的字符串, 可以完整实现全部的string解析了:

// 外部调用
    case '\"':
    {
        cursor++;
        ret = __parse_json_length(cursor);
        if (ret < 0)
        {
            return ret;
        }

        val->value.string = (char *)malloc(ret + 1);
        if (val->value.string == NULL)
        {
            return -1;
        }

        ret = __parse_json_string(cursor, end, val->value.string);
        if (ret < 0)
        {
            free(val->value.string);
            return ret;
        }

        val->type = JSON_VALUE_STRING;
        break;
    }

static int __parse_json_string(const char *cursor, const char **end, char *str)
{
    int ret;

    while (*cursor != '\"')
    {
        if (*cursor == '\\')
        {
            cursor++;
            switch (*cursor)
            {
            case '\"':
                *str = '\"';
                break;
            case '\\':
                *str = '\\';
                break;
            case '/':
                *str = '/';
                break;
            case 'b':
                *str = '\b';
                break;
            case 'f':
                *str = '\f';
                break;
            case 'n':
                *str = '\n';
                break;
            case 'r':
                *str = '\r';
                break;
            case 't':
                *str = '\t';
                break;
            case 'u':
                cursor++;
                ret = __parse_json_unicode(cursor, &cursor, str);
                if (ret < 0)
                    return ret;

                str += ret;
                continue;

            default:
                return -2;
            }
        }
        else
        {
            *str = *cursor;
        }

        cursor++;
        str++;
    }

    *str = '\0';
    *end = cursor + 1;
    return 0;
}

可以看出, 除了特殊的转义字符外, 其他的字符只需要直接拷贝就行了, 末尾需要写入一个'\0'​表示字符串结束.

参考

[1] 从零开始的 JSON 库教程(四):Unicode - 知乎 (zhihu.com)

[2] Workflow 源码解析 Json parser :part1 parse - 知乎 (zhihu.com)

[3] Unicode字符平面映射 - 维基百科,自由的百科全书 (wikipedia.org)

[4] 字符编码笔记:ASCII,Unicode 和 UTF-8 - 阮一峰的网络日志 (ruanyifeng.com)

[5] JSON数据类型 (biancheng.net)

posted @ 2023-08-08 11:41  明天咪西什么  阅读(13)  评论(0编辑  收藏  举报