4. 字符串解析
4. 字符串解析
章节目录
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的方式, 主要为以下几个步骤:
- 一次读取四位十六进制数.(不足或超出的为错)
- 将读取的Unicode码转为
UTF-8
的编码方式.(具体由utf-8编码函数实现)- 对于在原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的编码规则:
- 对于单字节的字符, 字节的第一位设置为0, 后面7位为这个字符的Unicode码. 在这一部分, UTF-8编码和ASCII编码是相同的.
- 对于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)