1. 程式人生 > >Unicode與UTF-8互轉(C語言實現)

Unicode與UTF-8互轉(C語言實現)



1.1 ASCII碼

我們知道, 在計算機內部, 所有的資訊最終都表示為一個二進位制的字串. 每一個二進位制
(bit)有0和1兩種狀態, 因此八個二進位制位就可以組合出 256種狀態, 這被稱為一個字
(byte). 也就是說, 一個位元組一共可以用來表示256種不同的狀態, 每一個狀態對應一
個符號, 就是256個符號, 從 0000000到11111111.

上個世紀60年代, 美國製定了一套字元編碼, 對英語字元與二進位制位之間的關係, 做了統
一規定. 這被稱為ASCII碼, 一直沿用至今.

ASCII碼一共規定了128個字元的編碼, 比如空格"SPACE"是32(二進位制00100000), 大寫的
字母A是65(二進位制01000001)

. 這128個符號(包括32個不能打印出來的控制符號), 只佔用
了一個位元組的後面7位, 最前面的1位統一規定為0.

1.2 非ASCII編碼

英語用128個符號編碼就夠了, 但是用來表示其他語言, 128個符號是不夠的. 比如, 在法
語中, 字母上方有注音符號, 它就無法用ASCII碼錶示. 於是, 一些歐洲國家就決定, 利
用位元組中閒置的最高位編入新的符號. 比如, 法語中的é的編碼為130(二進位制10000010).
這樣一來, 這些歐洲國家使用的編碼體系, 可以表示最多256個符號.

但是, 這裡又出現了新的問題. 不同的國家有不同的字母, 因此, 哪怕它們都使用256個
符號的編碼方式, 代表的字母卻不一樣. 比如, 130在法語編碼中代表了é, 在希伯來語
編碼中卻代表了字母Gimel (?)
, 在俄語編碼中又會代表另一個符號.

NOTE:
但是不管怎樣, 所有這些編碼方式中, 0-127表示的符號是一樣的, 不一樣的只是128-255
的這一段. // MMMMM

至於亞洲國家的文字, 使用的符號就更多了, 漢字就多達10萬左右. 一個位元組只能表示
256種符號, 肯定是不夠的, 就必須使用多個位元組表達一個符號. 比如, 簡體中文常見的
編碼方式是GB2312, 使用兩個位元組表示一個漢字, 所以理論上最多可以表示
256x256=65536個符號.

2. Unicode

2.1 Unicode的定義

正如上一節所說, 世界上存在著多種編碼方式, 同一個二進位制數字可以被解釋成不同的符
號. 因此, 要想開啟一個文字檔案, 就必須知道它的編碼方式, 否則用錯誤的編碼方式解
讀, 就會出現亂碼. 為什麼電子郵件常常出現亂碼?就是因為發信人和收信人使用的編碼
方式不一樣.

可以想象, 如果有一種編碼, 將世界上所有的符號都納入其中. 每一個符號都給予一個獨
一無二的編碼, 那麼亂碼問題就會消失. 這就是Unicode, 就像它的名字都表示的, 這是
一種所有符號的編碼.

Unicode也是一種字元編碼方法, 不過它是由國際組織設計, 可以容納全世界所有語言文
字的編碼方案. Unicode的學名是"Universal Multiple-Octet Coded Character Set
",
簡稱為UCS. UCS可以看作是"Unicode Character Set"的縮寫.

Unicode當然是一個很大的集合, 現在的規模可以容納100多萬個符號. 每個符號的編碼都
不一樣, 比如, U+0639表示阿拉伯字母Ain, U+0041表示英語的大寫字母A, U+4E25表示漢
字"". 具體的符號對應表, 可以查詢unicode.org, 或者專門的漢字對應表.

2.2 Unicode的問題

需要注意的是, "Unicode只是一個符號集, 它只規定了符號的二進位制程式碼, 卻沒有規定這
個二進位制程式碼應該如何儲存".

比如, 漢字""的unicode是十六進位制數4E25, 轉換成二進位制數足足有15位
(100111000100101), 也就是說這個符號的表示至少需要2個位元組. 表示其他更大的符號,
可能需要3個位元組或者4個位元組, 甚至更多.

這裡就有兩個嚴重的問題, 第一個問題是, 如何才能區別unicode和ascii?計算機怎麼知
道三個位元組表示一個符號, 而不是分別表示三個符號呢?第二個問題是, 我們已經知道,
英文字母只用一個位元組表示就夠了, 如果unicode統一規定, 每個符號用三個或四個位元組
表示, 那麼每個英文字母前都必然有二到三個位元組是0, 這對於儲存來說是極大的浪費,
文字檔案的大小會因此大出二三倍, 這是無法接受的.

它們造成的結果是:

1)  出現了unicode的多種儲存方式, 也就是說有許多種不同的二進位制格式,
    可以用來表示unicode.
2)  unicode在很長一段時間內無法推廣, 直到網際網路的出現


3. UTF-8

網際網路的普及, 強烈要求出現一種統一的編碼方式. UTF-8就是在網際網路上使用最廣的一
種unicode的實現方式. 其他實現方式還包括UTF-16和UTF-32, 不過在網際網路上基本不用.
重複一遍, 這裡的關係是, UTF-8是Unicode的實現方式之一.

UTF-8最大的一個特點, 就是它是一種變長的編碼方式. 它可以使用1~6個位元組表示一個符
號, 根據不同的符號而變化位元組長度.

3.1 UTF-8的編碼規則

UTF-8的編碼規則很簡單, 只有兩條:

1) 對於單位元組的符號, 位元組的第一位設為0, 後面7位為這個符號的unicode碼. 因此對於
   英語字母, UTF-8編碼和ASCII碼是相同的.

2) 對於n位元組的符號(n>1), 第一個位元組的前n位都設為1, 第n+1位設為0, 後面位元組的前
   兩位一律設為10. 剩下的沒有提及的二進位制位, 全部為這個符號的unicode碼.

下表總結了編碼規則, 字母x表示可用編碼的位.

<SPAN xmlns="http://www.w3.org/1999/xhtml">// #txt---
   |  Unicode符號範圍      |  UTF-8編碼方式
 n |  (十六進位制)           | (二進位制)
---+-----------------------+------------------------------------------------------
 1 | 0000 0000 - 0000 007F |                                              0xxxxxxx
 2 | 0000 0080 - 0000 07FF |                                     110xxxxx 10xxxxxx
 3 | 0000 0800 - 0000 FFFF |                            1110xxxx 10xxxxxx 10xxxxxx
 4 | 0001 0000 - 0010 FFFF |                   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
 5 | 0020 0000 - 03FF FFFF |          111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
 6 | 0400 0000 - 7FFF FFFF | 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

                    表 1. UTF-8的編碼規則
// #txt---end
</SPAN>

下面, 還是以漢字""為例, 演示如何實現UTF-8編碼.

已知""的unicode是4E25(1001110 00100101), 根據上表, 可以發現4E25處在第三行的
範圍內(0000 0800 - 0000 FFFF), 因此""的UTF-8編碼需要三個位元組, 即格式是
"1110xxxx 10xxxxxx 10xxxxxx". 然後, 從""的最後一個二進位制位開始, 依次從後向前
填入格式中的x, 多出的位補0. 這樣就得到了, ""的UTF-8編碼是 "11100100 10111000
10100101", 轉換成十六進位制就是E4B8A5.


4. Little endian和Big endian

上一節已經提到, Unicode碼可以採用UCS-2格式直接儲存. 以漢字""為例, Unicode碼
是4E25, 需要用兩個位元組儲存, 一個位元組是4E, 另一個位元組是25. 儲存的時候, 4E在前,
25在後, 就是Big endian方式; 25在前, 4E在後, 就是Little endian方式.
// Big Endian(4E25)    Little Endian(254E)

因此, 第一個位元組在前, 就是"大頭方式"(Big endian), 第二個位元組在前就是"小頭方式
"(Little endian).

4.1 計算機怎麼知道某一個檔案到底採用哪一種方式編碼?(零寬度非換行空格(FEFF))

Unicode規範中定義, 每一個檔案的最前面分別加入一個表示編碼順序的字元, 這個字元
的名字叫做"零寬度非換行空格"(ZERO WIDTH NO-BREAK SPACE), 用FEFF表示. 這正好是
兩個位元組, 而且FF比FE大1.
// Big Endian(FEFF)    Little Endian(FFFE)

NOTE:
如果一個文字檔案的頭兩個位元組是FE FF, 就表示該檔案採用大頭方式; 如果頭兩個位元組
是FF FE, 就表示該檔案採用小頭方式.

5. Unicode與UTF-8之間的轉換

從表1我們很明顯可以得知Unicode與UTF-8的關係, 下面以C語言實現兩者之間的轉換.

1) 將一個字元的Unicode(UCS-2和UCS-4)編碼轉換成UTF-8編碼.

// #c---
/*****************************************************************************
 * 將一個字元的Unicode(UCS-2和UCS-4)編碼轉換成UTF-8編碼.
 *
 * 引數:
 *    unic     字元的Unicode編碼值
 *    pOutput  指向輸出的用於儲存UTF8編碼值的緩衝區的指標
 *    outsize  pOutput緩衝的大小
 *
 * 返回值:
 *    返回轉換後的字元的UTF8編碼所佔的位元組數, 如果出錯則返回 0 .
 *
 * 注意:
 *     1. UTF8沒有位元組序問題, 但是Unicode有位元組序要求;
 *        位元組序分為大端(Big Endian)和小端(Little Endian)兩種;
 *        在Intel處理器中採用小端法表示, 在此採用小端法表示. (低地址存低位)
 *     2. 請保證 pOutput 緩衝區有最少有 6 位元組的空間大小!
 ****************************************************************************/
int enc_unicode_to_utf8_one(unsigned long unic, unsigned char *pOutput,
        int outSize)
{
    assert(pOutput != NULL);
    assert(outSize >= 6);

    if ( unic <= 0x0000007F )
    {
        // * U-00000000 - U-0000007F:  0xxxxxxx
        *pOutput     = (unic & 0x7F);
        return 1;
    }
    else if ( unic >= 0x00000080 && unic <= 0x000007FF )
    {
        // * U-00000080 - U-000007FF:  110xxxxx 10xxxxxx
        *(pOutput+1) = (unic & 0x3F) | 0x80;
        *pOutput     = ((unic >> 6) & 0x1F) | 0xC0;
        return 2;
    }
    else if ( unic >= 0x00000800 && unic <= 0x0000FFFF )
    {
        // * U-00000800 - U-0000FFFF:  1110xxxx 10xxxxxx 10xxxxxx
        *(pOutput+2) = (unic & 0x3F) | 0x80;
        *(pOutput+1) = ((unic >>  6) & 0x3F) | 0x80;
        *pOutput     = ((unic >> 12) & 0x0F) | 0xE0;
        return 3;
    }
    else if ( unic >= 0x00010000 && unic <= 0x001FFFFF )
    {
        // * U-00010000 - U-001FFFFF:  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput+3) = (unic & 0x3F) | 0x80;
        *(pOutput+2) = ((unic >>  6) & 0x3F) | 0x80;
        *(pOutput+1) = ((unic >> 12) & 0x3F) | 0x80;
        *pOutput     = ((unic >> 18) & 0x07) | 0xF0;
        return 4;
    }
    else if ( unic >= 0x00200000 && unic <= 0x03FFFFFF )
    {
        // * U-00200000 - U-03FFFFFF:  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput+4) = (unic & 0x3F) | 0x80;
        *(pOutput+3) = ((unic >>  6) & 0x3F) | 0x80;
        *(pOutput+2) = ((unic >> 12) & 0x3F) | 0x80;
        *(pOutput+1) = ((unic >> 18) & 0x3F) | 0x80;
        *pOutput     = ((unic >> 24) & 0x03) | 0xF8;
        return 5;
    }
    else if ( unic >= 0x04000000 && unic <= 0x7FFFFFFF )
    {
        // * U-04000000 - U-7FFFFFFF:  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
        *(pOutput+5) = (unic & 0x3F) | 0x80;
        *(pOutput+4) = ((unic >>  6) & 0x3F) | 0x80;
        *(pOutput+3) = ((unic >> 12) & 0x3F) | 0x80;
        *(pOutput+2) = ((unic >> 18) & 0x3F) | 0x80;
        *(pOutput+1) = ((unic >> 24) & 0x3F) | 0x80;
        *pOutput     = ((unic >> 30) & 0x01) | 0xFC;
        return 6;
    }

    return 0;
}
// #c---end

2) 將一個字元的UTF8編碼轉換成Unicode(UCS-2和UCS-4)編碼.

<SPAN xmlns="http://www.w3.org/1999/xhtml">// #c---
/*****************************************************************************
 * 將一個字元的UTF8編碼轉換成Unicode(UCS-2和UCS-4)編碼.
 *
 * 引數:
 *    pInput      指向輸入緩衝區, 以UTF-8編碼
 *    Unic        指向輸出緩衝區, 其儲存的資料即是Unicode編碼值,
 *                型別為unsigned long .
 *
 * 返回值:
 *    成功則返回該字元的UTF8編碼所佔用的位元組數; 失敗則返回0.
 *
 * 注意:
 *     1. UTF8沒有位元組序問題, 但是Unicode有位元組序要求;
 *        位元組序分為大端(Big Endian)和小端(Little Endian)兩種;
 *        在Intel處理器中採用小端法表示, 在此採用小端法表示. (低地址存低位)
 ****************************************************************************/
int enc_utf8_to_unicode_one(const unsigned char* pInput, unsigned long *Unic)
{
    assert(pInput != NULL && Unic != NULL);

    // b1 表示UTF-8編碼的pInput中的高位元組, b2 表示次高位元組, ...
    char b1, b2, b3, b4, b5, b6;

    *Unic = 0x0; // 把 *Unic 初始化為全零
    int utfbytes = enc_get_utf8_size(*pInput);
    unsigned char *pOutput = (unsigned char *) Unic;

    switch ( utfbytes )
    {
        case 0:
            *pOutput     = *pInput;
            utfbytes    += 1;
            break;
        case 2:
            b1 = *pInput;
            b2 = *(pInput + 1);
            if ( (b2 & 0xE0) != 0x80 )
                return 0;
            *pOutput     = (b1 << 6) + (b2 & 0x3F);
            *(pOutput+1) = (b1 >> 2) & 0x07;
            break;
        case 3:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            if ( ((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80) )
                return 0;
            *pOutput     = (b2 << 6) + (b3 & 0x3F);
            *(pOutput+1) = (b1 << 4) + ((b2 >> 2) & 0x0F);
            break;
        case 4:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            if ( ((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                    || ((b4 & 0xC0) != 0x80) )
                return 0;
            *pOutput     = (b3 << 6) + (b4 & 0x3F);
            *(pOutput+1) = (b2 << 4) + ((b3 >> 2) & 0x0F);
            *(pOutput+2) = ((b1 << 2) & 0x1C)  + ((b2 >> 4) & 0x03);
            break;
        case 5:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            b5 = *(pInput + 4);
            if ( ((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                    || ((b4 & 0xC0) != 0x80) || ((b5 & 0xC0) != 0x80) )
                return 0;
            *pOutput     = (b4 << 6) + (b5 & 0x3F);
            *(pOutput+1) = (b3 << 4) + ((b4 >> 2) & 0x0F);
            *(pOutput+2) = (b2 << 2) + ((b3 >> 4) & 0x03);
            *(pOutput+3) = (b1 << 6);
            break;
        case 6:
            b1 = *pInput;
            b2 = *(pInput + 1);
            b3 = *(pInput + 2);
            b4 = *(pInput + 3);
            b5 = *(pInput + 4);
            b6 = *(pInput + 5);
            if ( ((b2 & 0xC0) != 0x80) || ((b3 & 0xC0) != 0x80)
                    || ((b4 & 0xC0) != 0x80) || ((b5 & 0xC0) != 0x80)
                    || ((b6 & 0xC0) != 0x80) )
                return 0;
            *pOutput     = (b5 << 6) + (b6 & 0x3F);
            *(pOutput+1) = (b5 << 4) + ((b6 >> 2) & 0x0F);
            *(pOutput+2) = (b3 << 2) + ((b4 >> 4) & 0x03);
            *(pOutput+3) = ((b1 << 6) & 0x40) + (b2 & 0x3F);
            break;
        default:
            return 0;
            break;
    }

    return utfbytes;
}
// #c---end
</SPAN>