参考文章:知乎
4. Unicode 的解析
4.1 ASCII、Unicode、UTF-8 介绍
我们知道在计算机中,所有的数据在存储和运算时都要使用二进制数表示(因为计算机用高电平和低电平分别表示1和0),例如,像a、b、c、d这样的字母以及0、1等数字还有一些常用的符号等,在计算机中存储时也要使用二进制数来表示。
而具体用哪些二进制数字表示哪个符号,当然每个人都可以约定自己的一套(这就叫编码)。大家如果要想互相通信而不造成混乱,那么大家就必须使用相同的编码规则,于是美国有关的标准化组织就出台了ASCII编码。
拓展: ASCII
它是一种字符编码,ASCII 码使用指定的7 位或8 位二进制数
组合来表示128 或256 种可能的字符。标准ASCII 码使用7 位二进制数(剩下的1位二进制为0)来表示所有的大写和小写字母,数字0 到9、标点符号,以及在美式英语中使用的特殊控制字符
在美国,这128是够了,但是其他国家不够,ASCII是不够的。所以各地区制定了不同的编码系统,如中文主要用 GB 2312 和大五码、日文主要用 JIS 等。为了保持与ASCII码的兼容性,各地区制定了不同的编码系统,设定一般最高位为0时和原来的ASCII码相同,最高位为1的时候,各个国家自己给后面的位(1xxx xxxx)赋予他们国家的字符意义。但是这样一来又有问题出现了,不同国家对新增的128个数字赋予了不同的含义,比如说130在法语中代表了é,但是在希伯来语中却代表了字母Gimel,这样会造成很多不便。
于是,在这样的情景下:上世纪80年代末,Xerox、Apple 等公司开始研究,是否能制定一套多语言的统一编码系统。于是 Unicode 就诞生了。
拓展: Unicode
Unicode为世界上所有字符都分配了一个唯一的数字编号,这个编号范围(码点)从 0x000000 到 0x10FFFF(十六进制
),有110多万,每个字符都有一个唯一的Unicode编号,这个编号一般写成16进制,在前面加上U+。Unicode就相当于一张表,建立了字符与编号之间的联系。
注意:Unicode本身只规定了每个字符的数字编号是多少,并没有规定这个编号如何存储。
UTF,Unicode 转换格式,它的作用在于制定了各种储存码点的方式,现时流行的 UTF 为 UTF-8、UTF-16 和 UTF-32。每种 UTF 会把一个码点储存为一至多个编码单元(code unit)。例如 UTF-8 的编码单元是 8 位的字节、UTF-16 为 16 位、UTF-32 为 32 位。除 UTF-32 外,UTF-8 和 UTF-16 都是可变长度编码。
拓展:UTF-8
UTF-8就是使用变长字节表示,意思就是使用的字节数可变,这个变化是根据 Unicode 编号的大小有关,编号小的使用的字节就少,编号大的使用的字节就多。这个编码方法的好处之一是,码点范围 U+0000 ~ U+007F 编码为一个字节,与 ASCII 编码兼容。这范围的 Unicode 码点也是和 ASCII 字符相同的。因此,一个 ASCII 文本也是一个 UTF-8 文本。
Unicode编号范围与对应的UTF-8二进制格式 :
对于一个具体的Unicode编号,具体进行UTF-8的编码的方法:
- 首先找到该Unicode编号所在的编号范围,进而可以找到与之对应的二进制格式,然后将该Unicode编号转化为二进制数(去掉高位的0),最后将该二进制数
从右向左
依次填入二进制格式的X中,如果还有X未填,则设为0 。 - 比如:“马”的Unicode编号是:0x9A6C,整数编号是39532,对应第三个范围,其格式为:1110XXXX 10XXXXXX 10XXXXXX,39532 对应的二进制是 1001 1010 0110 1100,将二进制填入进入就为: 11101001 10101001 10101100 。
对于这例子的范围,对应的 C 代码是这样的:
if (u >= 0x0800 && u <= 0xFFFF) {
OutputByte(0xE0 | ((u >> 12) & 0xFF)); /* 0xE0 = 11100000 */
OutputByte(0x80 | ((u >> 6) & 0x3F)); /* 0x80 = 10000000 */
OutputByte(0x80 | ( u & 0x3F)); /* 0x3F = 00111111 */
}
7.2 需求分析
首先由于 UTF-8 的普及性,我们的 JSON 库也只支持 UTF-8。又因为 C 标准库没有关于 Unicode 的处理功能(C++11 有),所以我们要实现 JSON 库所需的字符编码处理功能。
对于非转义的字符,只要它们不小于 32(0 ~ 31 是不合法的编码单元),我们可以直接复制结果。
对于转义字符,JSON字符串中的 \uXXXX 是以 16 进制表示的,码点范围为 U+0000 至 U+FFFF,所以我们需要做的是:
- 解析 4 位十六进制整数为码点;
- 我们要把这个码点编码成 UTF-8。
注意:4 位的 16 进制数字只能表示 0 至 0xFFFF,但 Unicode 的码点是从 0 至 0x10FFFF,那怎么表示多出来的码点呢?
规则是这样的:
首先,U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(BMP)。
对于 BMP 以外的字符,JSON 会使用代理对的格式("\uXXXX\uYYYY")表示 。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(High surrogate),也就是\uXXXX部分;之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(Low surrogate),也就是\uYYYY部分。
如果我们知道了代理对 (H, L) ,可利用下面的公式将其变换成真实的码点:codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)
注意:
- 如果只有高代理项而欠缺低代理项,或是低代理项不在合法码点范围,我们都返回 LEPT_PARSE_INVALID_UNICODE_SURROGATE 错误。
- 如果 \u 后不是 4 位十六进位数字,则返回 LEPT_PARSE_INVALID_UNICODE_HEX 错误。
4.3 头文件设计
因为是还是字符串的解析,所以头文件的改动不大,只需添加两个新的错误码就行。
enum {
/* */
LEPT_PARSE_INVALID_UNICODE_HEX,
LEPT_PARSE_INVALID_UNICODE_SURROGATE
};
4.3 测试代码
因为新加两个错误码,而且我们在这部分打算测试Unicode,所以我们在test.c文件里面这么写:
static void test_parse_string() {
TEST_STRING("", "\"\"");
TEST_STRING("Hello", "\"Hello\"");
TEST_STRING("Hello\nWorld", "\"Hello\\nWorld\"");
TEST_STRING("\" \\ / \b \f \n \r \t", "\"\\\" \\\\ \\/ \\b \\f \\n \\r \\t\"");
#if 0
TEST_STRING("Hello\0World", "\"Hello\\u0000World\"");
TEST_STRING("\x24", "\"\\u0024\""); /* Dollar sign U+0024 */
TEST_STRING("\xC2\xA2", "\"\\u00A2\""); /* Cents sign U+00A2 */
TEST_STRING("\xE2\x82\xAC", "\"\\u20AC\""); /* Euro sign U+20AC */
TEST_STRING("\xF0\x9D\x84\x9E", "\"\\uD834\\uDD1E\""); /* G clef sign U+1D11E */
TEST_STRING("\xF0\x9D\x84\x9E", "\"\\ud834\\udd1e\""); /* G clef sign U+1D11E */
#endif
}
static void test_parse_invalid_unicode_hex() {
#if 0
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u01\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u012\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u/000\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\uG000\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0/00\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u0G00\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u00/0\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u00G0\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000/\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u000G\"");
#endif
}
static void test_parse_invalid_unicode_surrogate() {
#if 0
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uDBFF\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\\\\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uDBFF\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE, "\"\\uD800\\uE000\"");
#endif
}
4.5 实现解析器
还是那句话,因为我们是完善字符串的解析,所以改动的地方在于 lept_parse_string,主要是在转义字符串的处理处,得加上/uXXXX的处理方法。
#define STRING_ERROR(ret) do{c->top = head; return ret;} while (0);
/*
函数目的:解析字符串
参数:1. lept_context* c :要被解析的value
2. lept_value* v :解析后要被保存的位置
*/
static int lept_parse_string(lept_context* c, lept_value* v) {
/* */
for (;;) {
char ch = *p++;
switch (ch) {
case '\\': //转义字符的处理
switch (*p++){
case 'u':
if (!(p = lept_parse_hex4(p, &num)))
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);//如果 \u 后不是 4 位十六进位数字
/* 待完善 */
lept_encode_utf8(c, num);
break;
/* */
}
/* */
}
}
}
接下来实现两个函数:
- lept_parse_hex4(); 解析 4 位十六进数字,存储为码点 u。这个函数在成功时返回解析后的文本指针,失败返回 NULL。如果失败,就返回 LEPT_PARSE_INVALID_UNICODE_HEX 错误
- lept_encode_utf8(); 把码点编码成 UTF-8,写进缓冲区。
/*
函数目的:解析4位16进制数字,并存储在num里面
步骤:
1. 参数p指向的是/u后面跟着的数字
2. num的初始值为0,然后将此时num的值左移四位
3. 在循环内判断ch的类型,并将char类型的ch转化为int类型的。
4. 因为ch数字范围在0~F之间,所以ch最多占的4个bit就可以保存,将ch与num按位或后再赋值给num.(按位或:有一个1就为1),相当于将ch的值赋到num的后4位上。
5. 一套循环下来,num就保存了4位16进制数字了。
*/
static const char* lept_parse_hex4(const char* p, unsigned* num)
{
int i;
*num = 0;
for (i = 0; i < 4; i++)
{
char ch = *p++;
*num = *num << 4;//向左移动四位
if (ch >= '0' && ch <= '9') *num |= ch - '0'; //如果ch是数字,-'0'意味着将char 0 转换为 int 0
else if (ch >= 'A' && ch <= 'F') *num |= ch - ('A' - 10);
else if (ch >= 'a' && ch <= 'f') *num |= ch - ('a' - 10);
else return NULL;
}
return p;
}
/* 将码点编码成UTF-8,写进缓冲区 */
static void lept_encode_utf8(lept_context* c, unsigned u)
{
if (u <= 0x7F)
PUTC(c, u & 0xFF); //按位与(&):一个为0就为0,为什么要&,主要是为了防止编译器的警告或者误判
else if (u <= 0x7FF) {
PUTC(c, 0xC0 | ((u >> 6) & 0xFF)); //0xC0 = 11000000. FF = 11111111
PUTC(c, 0x80 | (u & 0x3F)); //0x80 = 10000000 3F = 111111
}
else if (u <= 0xFFFF) {
PUTC(c, 0xE0 | ((u >> 12) & 0xFF));
PUTC(c, 0x80 | ((u >> 6) & 0x3F));
PUTC(c, 0x80 | (u & 0x3F));
}
else {
assert(u <= 0x10FFFF);
PUTC(c, 0xF0 | ((u >> 18) & 0xFF));
PUTC(c, 0x80 | ((u >> 12) & 0x3F));
PUTC(c, 0x80 | ((u >> 6) & 0x3F));
PUTC(c, 0x80 | (u & 0x3F));
}
}
代码写到这里,剩下的事情就是完善lept_parse_string 函数,主要是对代理对的处理,遇到高代理项,就需要把低代理项 \uxxxx 也解析进来,然后用这两个项去计算出码点。
/*
函数目的:解析字符串
参数:1. lept_context* c :要被解析的value
2. lept_value* v :解析后要被保存的位置
*/
static int lept_parse_string(lept_context* c, lept_value* v) {
size_t head = c->top;//备份栈顶
size_t len;
unsigned num, num2;
EXPECT(c, '\"');//判断c是否为string类型
const char* p = c->json;
for (;;) {
char ch = *p++;
switch (ch) {
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;
case 'u':
if (!(p = lept_parse_hex4(p, &num)))
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);//如果 \u 后不是 4 位十六进位数字
if (num >= 0xD800 && num <= 0xDBFF)
{
if (*p++ != '\\')
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
if (*p++ != 'u')
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
if (!(p = lept_parse_hex4(p, &num2)))
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
if (num2 < 0xDC00 || num2 > 0xDFFF)
STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
num = 0x10000 + (((num - 0xD800) << 10) | (num2 - 0xDC00));//右移10位 = *2^10 = *1024 = * 0x400
}
lept_encode_utf8(c, num);
break;
default:
c->top = head;
return LEPT_PARSE_INVALID_STRING_ESCAPE;
}
break;
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:
if ((unsigned char)ch < 0x20){ //剩余的情况下有不合法字符串的情况:%x00 至 %x1F
c->top = head;
return LEPT_PARSE_INVALID_STRING_CHAR;//不合法的字符串
}
PUTC(c, ch); //把解析的字符串压栈
}
}
}