从零开始的 JSON 库教程(四):解析unicode

276 阅读7分钟

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

第四节主要是第三节的补充,解决解析字符串中解析unicode字符\uXXXX的问题。

Unicode

先介绍下unicode相关背景知识。
ASCII是最熟悉的字符编码方式,将128个字符映射到0-127,需要7位二进制数表示,可以用一个字节(8位)进行存储。 但ASCII能表示的内容太少,于是人们提出各种各样的编码系统,不同地方不统一,unicode为解决这个问题被提出。
Unicode provides a unique number for every character, no matter what the platform, program, or language is.
unicode为每一个字符提供一个唯一的数字id,叫做码点(code point) www.unicode.org/cgi-bin/Get…
也就是说,unicode里面每个id对应着各不相同的字符,甚至包括笑脸表情。
目前码点取值范围从0000-10FFFF,总共是0000-FFFFF(16^5),10000-10FFFF(16^4)之和1114112个值(至少需要21位才能完全编码)。 如果直接对这些这些数字进行编码,需要32位,4个字节(即UTF-32编码方式)。(至于为什么不用3个字节24位,可以编码16777216个值,我就不清楚了)。
UTF-8是一种变长编码方案(不同字符占字节数不一样),完全兼容ASCII,同时避免UTF-16和UTF-32的字节序问题。
当给定四个字节,UTF-8知道如何解析它,是当作一个还是两个,还是三个,或者是四个。

image.png

解释:

  1. 如果以0开头,则其是单字节,需要7位二进制数表示,取值范围是\u0000-\u007F(0-127);
  2. 如果以110开头,则其是双字节,需要11位二进制数表示(双字节总共16位,第一个字节用110表示是双字节,第二个字节固定10开头), 取值范围是\u0080-\u07FF(128-2047);
  3. 如果以1110开头,则其是三字节,需要16位二进制数表示,取值范围是\u0800-\uFFFF(2048-65535)
  4. 如果以11110开头,则其是四字节,需要21位二进制数表示,取值范围是\u10000-\u10FFFF(65536-2097151)

image.png 以中文“汪”为例, “汪”unicode码点(或者说id,索引)是27754,二进制为(110 1100 0110 1010), 应该是三字节编码, 则把27754对应的二进制数0110 1100 0110 1010(补齐首0使其为完整16位)填到1110xxxx 10xxxxxx 10xxxxxx里面得到11100110 10110001 10101010(E6 B1 AA)。

小结:一开始是一个字节的ASCII,但能表示的太少; 于是提出unicode, 给出所有字符的id; utf-8是针对unicode的一种编码方式, 根据id范围, 选择编码字节数。
于是本节的工作是将4位16进制数解析为码点,再将码点编码成utf-8。
但这有一个问题,因为码点取值可能不止0000-ffff, 还包括10000-10ffff, 这种情况怎么处理?

U+0000 至 U+FFFF 这组 Unicode 字符称为基本多文种平面(basic multilingual plane, BMP),还有另外 16 个平面(我理解为0000-ffff属于00ffff平面,后面还有01-10共16个平面,总共17个平面)17*2^16=1114112=16^5+16^4。 那么 BMP 以外的字符,JSON 会使用代理对(surrogate pair)表示 \uXXXX\uYYYY。在 BMP 中,保留了 2048 个代理码点。如果第一个码点是 U+D800 至 U+DBFF,我们便知道它的代码对的高代理项(high surrogate),之后应该伴随一个 U+DC00 至 U+DFFF 的低代理项(low surrogate)。然后,我们用下列公式把代理对 (H, L) 变换成真实的码点:

codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)
// H取值U+D800(55296)至 U+DBFF(56319)  1023
// L取值U+DC00至U+DFFF 1023
// 0x400为1024
// 则codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)取值范围为
//最小0x10000,H为D800,L为DC00
//最大H为DBFF, L为DFFF, 0x10000+1023*1024+1023 = 0x10000+1024*1024 = 0x10000+0x100000
//0x110000

leptjson.h

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,
	LEPT_PARSE_INVALID_UNICODE_HEX,         // 不合法的十六进位数 (\u 后不是4位十六进位数字)
	LEPT_PARSE_INVALID_UNICODE_SURROGATE    // 不合法的代理 (如果只有高代理项而欠缺低代理项,或是低代理项不在合法码点范围)
};

leptjson.cpp

解析4位十六进制整数为码点

//遇到\u, 解析4位十六进制整数为码点
static const char* lept_parse_hex4(const char* p, unsigned* u) {
    int i;
    *u = 0;
    for (i = 0; i < 4; i++) {
        char ch = *p++;
        *u <<= 4;// 乘16, 二进制左移四位
        if (ch >= '0' && ch <= '9')  *u |= ch - '0';
        else if (ch >= 'A' && ch <= 'F')  *u |= ch - ('A' - 10);
        else if (ch >= 'a' && ch <= 'f')  *u |= ch - ('a' - 10);
        else return NULL;
    }
    //printf("------%s\n", u);
    return p;
}

把码点编码成utf-8, 码点取值范围为0000-10FFFF

// 把码点编码成utf-8
static void lept_encode_utf8(lept_context* c, unsigned u) {
    if (u <= 0x7F)
        PUTC(c, u & 0xFF);
    else if (u <= 0x7FF) {
        // 110xxxxx 10xxxxxx
        // 0xC0 1100 0000
        // 300 100101100 12c
        // 100101100>>6 = 100
        // 100&0xFF = 0000 0100
        // 0xC0|(0000 0100) = 1100 0100
        // 100101100 & 0011 1111 = 010 1100
        // 
        PUTC(c, 0xC0 | ((u >> 6) & 0xFF)); // 取后六位之前的部分,然后补到1100 0000里面
        PUTC(c, 0x80 | (u & 0x3F)); // 取后六位,然后补到1000 0000里面
    }
    else if (u <= 0xFFFF) {
        PUTC(c, 0xE0 | ((u >> 12) & 0xFF)); // 取后十二位之前的部分,然后补到1110 0000里面
        PUTC(c, 0x80 | ((u >> 6) & 0x3F)); // 取后6位到12之间的部分,然后补到1000 0000里面
        PUTC(c, 0x80 | (u & 0x3F)); // 取后六位,然后补到1000 0000里面
    }
    else {
        assert(u <= 0x10FFFF);
        PUTC(c, 0xF0 | ((u >> 18) & 0xFF)); //取后18位之前的部分,然后补到1111 0000里面
        PUTC(c, 0x80 | ((u >> 12) & 0x3F));
        PUTC(c, 0x80 | ((u >> 6) & 0x3F));
        PUTC(c, 0x80 | (u & 0x3F));
    }
}

解析字符串,新增case 'u':部分

static int lept_parse_string(lept_context* c, lept_value* v) {
    size_t head = c->top, len;
    unsigned u, u2;  
    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 '\\':
            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, &u))) // 解析失败,无效四位十六进制码点
                        STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
                    // 代理对 "\"\\uD834\\uDD1E\""
                    if (u >= 0xD800 && u <= 0xDBFF) { /* surrogate pair */
                        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, &u2))) // 低代理项解析失败,无效四位十六进制码点
                                STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX);
                        if (u2 < 0xDC00 || u2 > 0xDFFF)  // 低代理项解析结果不符合规定
                                STRING_ERROR(LEPT_PARSE_INVALID_UNICODE_SURROGATE);
                        // 合并代理对 codepoint = 0x10000 + (H − 0xD800) × 0x400 + (L − 0xDC00)
                        u = (((u - 0xD800) << 10) | (u2 - 0xDC00)) + 0x10000;
                    }
                    lept_encode_utf8(c, u);
                    break;
                default:
                    STRING_ERROR(LEPT_PARSE_INVALID_STRING_ESCAPE);
            }
            break;
    case '\0':
            STRING_ERROR(LEPT_PARSE_MISS_QUOTATION_MARK);
    default:
            if ((unsigned char)ch < 0x20) {
                STRING_ERROR(LEPT_PARSE_INVALID_STRING_CHAR);
            }
            PUTC(c, ch);
        }
    }
}

test.cpp

可以正常解析例子

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 */

不合法四位16进制数字例子

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\"");
TEST_ERROR(LEPT_PARSE_INVALID_UNICODE_HEX, "\"\\u 123\"");

不合法的代理例子

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\"");

参考文献:

  1. geekdaxue.co/read/fcant@…

解析结果

教程4.png