參考文章:知乎
4. Unicode 的解析
文章目錄
-
-
- 4. Unicode 的解析
-
- 4.1 ASCII、Unicode、UTF-8 介紹
- 7.2 需求分析
- 4.3 頭檔案設計
- 4.3 測試代碼
- 4.5 實作解析器
-
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); //把解析的字元串壓棧
}
}
}