【字符編碼系列】GBK,UTF-8,UTF-16之間的轉換

寫在前面的話

本文屬於 字符編碼系列文章之一,更多請前往 字符編碼系列javascript

大綱

  • 不一樣編碼轉換的理論基礎html

    • UTF-16轉UTF-8
    • UTF-16轉GBK
  • UTF-16和UTF-8之間的轉換
  • UTF-16和GBK之間的轉換

不一樣編碼轉換的理論基礎

不一樣的編碼直接如何轉換的,這裏先簡單的描述下UTF-1六、UTF-八、GBK直接的轉換過程。java

因爲本文是基於JavaScript的,而JS如今的編碼能夠認爲是UTF-16,因此都會通過UTF-16中轉。git

UTF-16轉UTF-8

這二者都是Unicode,因此有一個大前提就是碼點一致,僅僅是對於碼點的編碼方式不一致而已,由於UTF-16能夠認爲是固定2字節的實現(4字節的比較少見),因此參考以下Unicode和UTF-8轉換關係表便可:github

Unicode編碼 UTF-8字節流
U+00000000 - U+0000007F 0xxxxxxx
U+00000080 - U+000007FF 110xxxxx 10xxxxxx
U+00000800 - U+0000FFFF 1110xxxx 10xxxxxx 10xxxxxx
U+00010000 - U+001FFFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U+00200000 - U+03FFFFFF 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U+04000000 - U+7FFFFFFF 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx

因此UTF16和UTF8之間的相互轉換能夠經過上表的轉換表來實現,判斷Unicode碼所在的區間就能夠獲得這個字符是由幾個字節所組成,以後經過移位來實現,分爲新的多個字節來存儲。編程

UTF-16轉GBK

UTF-16和GBK直接的轉換就稍微複雜點,由於Unicode和GBK的碼點不一致,所以須要GBK個Unicode的碼點映射關係表才能進行相應轉換。segmentfault

這裏GBK和Unicode的碼點映射表因爲太長了,就不單獨列出來,能夠參考:Unicode編碼和GBK的轉換映射表 (若是連接不可用能夠自動搜索或者參考博客中的源碼連接)數組

而後通用,拿到Unicode碼點後能夠根據映射錶轉換爲GBK碼點,而後用GBK的編碼方式編碼便可完成轉換編程語言

UTF-16和UTF-8之間的轉換

UTF-16轉UTF-8

步驟描述

  • Step1:獲取該字符對應的Unicode碼
  • Step2:判斷該Unicode碼所在的範圍,根據不一樣的範圍,來決定存儲它的字節長度。編碼

    • 若是介於U+00000000 – U+0000007F之間,表明該字符采起一個字節存儲,那麼直接經過這個新字節的unicode碼,便可轉換爲UTF-8碼(這是這裏的一種簡稱,不一樣的編程語言有不一樣實現,例如能夠用兩個字節來存儲一個字符的信息,解碼時進行判斷,若是發現是UTF-8的多字節實現,那麼將多字節合併後再轉爲一個字符輸出).轉換完畢
    • 若是介於U+00000080 – U+000007FF之間,表明該字符采起兩個字節存儲,那麼將該Unicode碼轉爲二進制,取出高5位(這裏不分大端序和小端序,只以實際的碼爲準,具體實現能夠採起移位實現),並加上頭部110,組成第一個字節;再取出低6位(按順序取),加上頭部10,組成第二個字節。而後分別經過兩個新的字節的unicode碼,能夠轉換爲相應的UTF-8碼.轉換完畢
    • 若是介於U+00000800 – U+0000FFFF之間,表明該字符采起三個字節存儲,那麼將該Unicode碼轉爲二進制,取出高4位,並加上頭部1110,組成第一個字節;再取出低6位(按順序取),加上頭部10,組成第二個字節;再取出低6位(按順序取),加上頭部10,組成第三個字節。而後分別經過三個新的字節的unicode碼,能夠轉換爲相應的UTF-8碼.轉換完畢
    • 若是介於U+00010000 – U+001FFFFF之間,表明該字符采起四個字節存儲(實際上,四個字節或以上存儲的字符是不多的),那麼將該Unicode碼轉爲二進制,取出高3位,並加上頭部11110,組成第一個字節;再取出低6位(按順序取),加上頭部10,組成第二個字節;再取出低6位(按順序取),加上頭部10,組成第三個字節;再取出低6位(按順序取),加上頭部10,組成第四個字節。而後分別經過四個新的字節的unicode碼,能夠轉換爲相應的UTF-8碼.轉換完畢
    • 若是介於U+00200000 – U+03FFFFFF,表明該字符采起五個字節存儲,那麼將該Unicode碼轉爲二進制,取出高2位,並加上頭部111110,組成第一個字節;再取出低6位(按順序取),加上頭部10,組成第二個字節;再取出低6位(按順序取),加上頭部10,組成第三個字節;再取出低6位(按順序取),加上頭部10,組成第四個字節;再取出低6位(按順序取),加上頭部10,組成第五個字節。而後分別經過五個新的字節的unicode碼,能夠轉換爲相應的UTF-8碼.轉換完畢
    • 若是介於U+04000000 – U+7FFFFFFF,表明該字符采起六個字節存儲,那麼將該Unicode碼轉爲二進制,取出高1位,並加上頭部1111110,組成第一個字節;再取出低6位(按順序取),加上頭部10,組成第二個字節;再取出低6位(按順序取),加上頭部10,組成第三個字節;再取出低6位(按順序取),加上頭部10,組成第四個字節;再取出低6位(按順序取),加上頭部10,組成第五個字節;再取出低6位(按順序取),加上頭部10,組成第六個字節。而後分別經過六個新的字節的unicode碼,能夠轉換爲相應的UTF-8碼.轉換完畢

代碼實現

/**
     * @description 將utf-16編碼字符串轉爲utf-8編碼字符串
     * @param {String} str 傳入的 utf16編碼字符串(javascript內置的就是utf16編碼)
     * @return {String} utf8編碼的字符串,js打印會有亂碼
     */
exports.utf16StrToUtf8Str = function(str) {
    if (!str) {
        // ''字符屬於ascii碼,因此沒必要擔憂不一樣編碼的轉換問題
        return '';
    }
    // res是用來存放結果的字符數組,最終會轉爲字符串返回
    var res = [],
    len = str.length;
    for (var i = 0; i < len; i++) {
        var code = str.charCodeAt(i);
        if (code > 0x0000 && code <= 0x007F) {
            // 單字節,這裏並不考慮0x0000,由於它是空字節
            // U+00000000 – U+0000007F  0xxxxxxx
            res.push(str.charAt(i));
        } else if (code >= 0x0080 && code <= 0x07FF) {
            // 雙字節
            // U+00000080 – U+000007FF  110xxxxx 10xxxxxx
            // 110xxxxx
            // 0xC0 爲12*16 = 192 二進制爲 11000000
            // 0x1F爲 31 二進制   00011111,由於第一個字節只取5位
            // code 右移六位是由於從高位開始取得,因此須要將低位的六位留到第二個字節
            var byte1 = 0xC0 | ((code >> 6) & 0x1F);
            // 10xxxxxx
            // 0x80爲128 二進制爲 10000000
            // 0x3F爲63 二進制位 00111111,由於只須要取到低位的6位
            var byte2 = 0x80 | (code & 0x3F);
            res.push(String.fromCharCode(byte1), String.fromCharCode(byte2));
        } else if (code >= 0x0800 && code <= 0xFFFF) {
            // 三字節
            // U+00000800 – U+0000FFFF  1110xxxx 10xxxxxx 10xxxxxx
            // 1110xxxx
            // 0xE0 爲224 二進制爲 11100000
            // 同理,須要留下 12位給低位
            // 0x0F爲15 00001111
            var byte1 = 0xE0 | ((code >> 12) & 0x0F);
            // 10xxxxxx
            // 再留6位給低位
            var byte2 = 0x80 | ((code >> 6) & 0x3F);
            // 10xxxxxx
            var byte3 = 0x80 | (code & 0x3F);
            res.push(String.fromCharCode(byte1), String.fromCharCode(byte2), String.fromCharCode(byte3));
        } else if (code >= 0x00010000 && code <= 0x001FFFFF) {
            // 四字節
            // U+00010000 – U+001FFFFF  11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            // 同理,須要留下 18位給低位
            // 0x07 00000111
            // 0xF0  240 11110000
            var byte1 = 0xF0 | ((code >> 18) & 0x07);
            // 10xxxxxx
            // 再留12位給低位
            var byte2 = 0x80 | ((code >> 12) & 0x3F);
            // 再留6位給低位
            var byte3 = 0x80 | ((code >> 6) & 0x3F);
            // 10xxxxxx
            var byte4 = 0x80 | (code & 0x3F);
            res.push(String.fromCharCode(byte1), String.fromCharCode(byte2), String.fromCharCode(byte3), String.fromCharCode(byte4));
        } else if (code >= 0x00200000 && code <= 0x03FFFFFF) {
            // 五字節
            // U+00200000 – U+03FFFFFF  111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
            // 同理,須要留下 24位給低位
            // 0x03 00000011
            // 0xF8  248 11111000
            var byte1 = 0xF8 | ((code >> 24) & 0x03);
            // 10xxxxxx
            // 再留18位給低位
            var byte2 = 0x80 | ((code >> 18) & 0x3F);
            // 再留12位給低位
            var byte3 = 0x80 | ((code >> 12) & 0x3F);
            // 再留6位給低位
            var byte4 = 0x80 | ((code >> 6) & 0x3F);
            // 10xxxxxx
            var byte5 = 0x80 | (code & 0x3F);
            res.push(String.fromCharCode(byte1), String.fromCharCode(byte2), String.fromCharCode(byte3), String.fromCharCode(byte4), String.fromCharCode(byte5));
        } else
        /** if (code >= 0x04000000 && code <= 0x7FFFFFFF)*/
        {
            // 六字節
            // U+04000000 – U+7FFFFFFF  1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
            // 同理,須要留下 24位給低位
            // 0x01 00000001
            // 0xFC  252 11111100
            var byte1 = 0xFC | ((code >> 30) & 0x01);
            // 10xxxxxx
            // 再留24位給低位
            var byte2 = 0x80 | ((code >> 24) & 0x3F);
            // 再留18位給低位
            var byte3 = 0x80 | ((code >> 18) & 0x3F);
            // 再留12位給低位
            var byte4 = 0x80 | ((code >> 12) & 0x3F);
            // 再留6位給低位
            var byte5 = 0x80 | ((code >> 6) & 0x3F);
            // 10xxxxxx
            var byte6 = 0x80 | (code & 0x3F);
            res.push(String.fromCharCode(byte1), String.fromCharCode(byte2), String.fromCharCode(byte3), String.fromCharCode(byte4), String.fromCharCode(byte5), String.fromCharCode(byte6));
        }
    }
    return res.join('');
};

UTF-8轉UTF-16

步驟描述

  • Step1:獲取該字符對應的Unicode碼
  • Step1:用該碼的二進制和相應的關鍵字節相與,根據轉換關係表,判斷處於那一段區間,來判斷是使用幾個字節存儲字符的,而後分別合併對應的字節數,組成新的字符輸出。

    • 用該Unicode碼的二進制右移7位後與(11111111)相與,若是獲得了0,表明該字符只用了一個字節存儲,因此直接輸出該字符.轉換完畢
    • 用該Unicode碼的二進制右移5位後與(11111111)相與,若是獲得了110(6),表明該字符佔用了二個字節,因此分別獲取該字符和下一個字符,而後分別取出本字節的低5位後左移6位和取出下一個字節的低6位(保持不變),將2個字節相或,獲得一個新的字節.這個字節就是最終字符的unicode碼,而後轉爲對應的字符輸出. 轉換完畢
    • 用該Unicode碼的二進制右移4位後與(11111111)相與,若是獲得了1110(14),表明該字符佔用了三個字節,因此分別獲取該字符和下一個字符和下下個字符,而後分別取出本字節的低4位後左移12位和取出下一個字節的低6位後左移6位和取出下下一個字節的低6位(保持不變),將3個字節相或,獲得一個新的字節.這個字節就是最終字符的unicode碼,而後轉爲對應的字符輸出. 轉換完畢
    • 用該Unicode碼的二進制右移3位後與(11111111)相與,若是獲得了11110(30),表明該字符佔用了四個字節,因此分別獲取該字符和下一個字符和下下個字符和下下下個字符,而後分別取出本字節的低3位後左移18位取出下一個字節的低6位後左移12位和和取出下下一個字節的低6位後左移6位和取出下下下一個字節的低6位(保持不變),將4個字節相或,獲得一個新的字節.這個字節就是最終字符的unicode碼,而後轉爲對應的字符輸出. 轉換完畢
    • 用該Unicode碼的二進制右移2位後與(11111111)相與,若是獲得了111110(62),表明該字符佔用了五個字節,因此分別獲取該字符和下一個字符和下下個字符和下下下個字符和下下下下個字符,而後分別取出本字節的低2位後左移24位和取出下一個字節的低6位後左移18位和取出下下一個字節的低6位後左移12位和和取出下下下一個字節的低6位後左移6位和取出下下下下一個字節的低6位(保持不變),將5個字節相或,獲得一個新的字節.這個字節就是最終字符的unicode碼,而後轉爲對應的字符輸出. 轉換完畢
    • 用該Unicode碼的二進制右移1位後與(11111111)相與,若是獲得了1111110(126),表明該字符佔用了六個字節,因此分別獲取該字符和下一個字符和下下個字符和下下下個字符和下下下下個字符和下下下下下個字符,而後分別取出本字節的低1位後左移30位和取出下一個字節的低6位後左移24位和取出下下一個字節的低6位後左移18位和取出下下下一個字節的低6位後左移12位和和取出下下下下一個字節的低6位後左移6位和取出下下下下下一個字節的低6位(保持不變),將6個字節相或,獲得一個新的字節.這個字節就是最終字符的unicode碼,而後轉爲對應的字符輸出. 轉換完畢

代碼實現

/**
     * @description UTF8編碼字符串轉爲UTF16編碼字符串
     * @param {String} str utf8編碼的字符串
     * @return {String} utf16編碼的字符串,能夠直接被js用來打印
     */
exports.utf8StrToUtf16Str = function(str) {
    if (!str) {
        return '';
    }
    // res是用來存放結果的字符數組,最終會轉爲字符串返回
    var res = [],
    len = str.length;
    for (var i = 0; i < len; i++) {
        // 得到對應的unicode碼
        var code = str.charCodeAt(i);
        // 對第一個字節進行判斷
        if (((code >> 7) & 0xFF) == 0x0) {
            // 0xFF 255 11111111,表明只取前8位
            // 右移7位,若是是隻剩下0了,表明這個是單字節
            // 單字節
            // 0xxxxxxx
            res.push(str.charAt(i));
        } else if (((code >> 5) & 0xFF) == 0x6) {
            // 雙字節 110開頭  
            // 110xxxxx 10xxxxxx
            // 須要用到下一個字節
            var code2 = str.charCodeAt(++i);
            // 0x1F 31 00011111
            // 取到第一個字節的後5位,而後左移6位(這6位留給第二個字節的低6位),因爲js是number型,因此沒必要擔憂溢出
            var byte1 = (code & 0x1F) << 6;
            // 0x3F 63  00111111
            var byte2 = code2 & 0x3F;
            // 或運算,由於第一個字節第六位沒有,第二個字節只有低6位,因此算是結合了
            var utf16 = byte1 | byte2;
            res.push(String.fromCharCode(utf16));
        } else if (((code >> 4) & 0xFF) == 0xE) {
            // 三字節 1110開頭
            // 1110xxxx 10xxxxxx 10xxxxxx
            var code2 = str.charCodeAt(++i);
            var code3 = str.charCodeAt(++i);
            // 和00001111與後, 左移12位
            var byte1 = (code & 0x0F) << 12;
            // 和00111111與後,左移6位
            var byte2 = (code2 & 0x3F) << 6;
            // 和00111111與
            var byte3 = code3 & 0x3F
            var utf16 = byte1 | byte2 | byte3;
            res.push(String.fromCharCode(utf16));
        } else if (((code >> 3) & 0xFF) == 0x1E) {
            // 四字節 11110開頭
            // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
            var code2 = str.charCodeAt(++i);
            var code3 = str.charCodeAt(++i);
            var code4 = str.charCodeAt(++i);
            // 和00000111與後, 左移18位
            var byte1 = (code & 0x07) << 18;
            // 和00111111與後,左移12位
            var byte2 = (code2 & 0x3F) << 12;
            // 和00111111與後,左移6位
            var byte3 = (code3 & 0x3F) << 6;
            // 和00111111與
            var byte4 = code4 & 0x3F
            var utf16 = byte1 | byte2 | byte3 | byte4;
            res.push(String.fromCharCode(utf16));
        } else if (((code >> 2) & 0xFF) == 0x3E) {
            // 五字節 111110開頭
            // 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
            var code2 = str.charCodeAt(++i);
            var code3 = str.charCodeAt(++i);
            var code4 = str.charCodeAt(++i);
            var code5 = str.charCodeAt(++i);
            // 和00000011與後, 左移24位
            var byte1 = (code & 0x03) << 24;
            // 和00111111與後,左移18位
            var byte2 = (code2 & 0x3F) << 18;
            // 和00111111與後,左移12位
            var byte3 = (code3 & 0x3F) << 12;
            // 和00111111與後,左移6位
            var byte4 = (code4 & 0x3F) << 6;
            // 和00111111與
            var byte5 = code5 & 0x3F
            var utf16 = byte1 | byte2 | byte3 | byte4 | byte5;
            res.push(String.fromCharCode(utf16));
        } else
        /** if (((code >> 1) & 0xFF) == 0x7E)*/
        {
            // 六字節 1111110開頭
            // 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
            var code2 = str.charCodeAt(++i);
            var code3 = str.charCodeAt(++i);
            var code4 = str.charCodeAt(++i);
            var code5 = str.charCodeAt(++i);
            var code6 = str.charCodeAt(++i);
            // 和00000001與後, 左移30位
            var byte1 = (code & 0x01) << 30;
            // 和00111111與後,左移24位
            var byte2 = (code2 & 0x3F) << 24;
            // 和00111111與後,左移18位
            var byte3 = (code3 & 0x3F) << 18;
            // 和00111111與後,左移12位
            var byte4 = (code4 & 0x3F) << 12;
            // 和00111111與後,左移6位
            var byte5 = (code5 & 0x3F) << 6;
            // 和00111111與
            var byte6 = code6 & 0x3F
            var utf16 = byte1 | byte2 | byte3 | byte4 | byte5 | byte6;
            res.push(String.fromCharCode(utf16));
        }
    }
    return res.join('');
};

UTF-16和GBK之間的轉換

注意,這裏爲了篇幅,沒有將GBK和Unicode的碼錶映射放進來,更多詳細能夠參考博文中的源碼。

UTF-16轉GBK

步驟描述

  • Step1:獲取該字符對應的Unicode碼
  • Step2:判斷該Unicode的範圍

    • 若是是普通的ASCII碼,則不進行轉換
    • 若是是大於127小於等於255的(標準碼範圍的,GBK兼容ISO-8859標準碼的),根據映射表,轉爲對應的GBK碼
    • 若是是大於255的(大於255表明一個字節裝不下了,因此這時候再也不是兼容模式,而是GBK的存儲模式,須要兩個字節來存儲),就將改碼根據Unicode個GBK的映射表,轉換爲GBK獨特的雙字節存儲方式來存儲(高字節區存儲分區號,低字節去存儲碼號).轉換完畢

代碼實現

/**
     * @description 將utf16編碼的字符串(js內置編碼)轉爲GBK編碼的字符串
     * @param {String} str utf16編碼的字符串(js內置)
     * @return {String} 轉換後gbk編碼的字符串
     */
exports.utf16StrToGbkStr = function(str) {
    if (!str) {
        return '';
    }
    // res是用來存放結果的字符數組,最終會轉爲字符串返回
    var res = [],
    len = str.length;
    for (var i = 0; i < len; i++) {
        // 得到對應的unicode碼
        var code = str.charCodeAt(i);
        if (code < 0) {
            code += 65536;
        }
        if (code > 127) {
            code = unicode2GBKCode(code);
        }
        if (code > 255) {
            // gbk中,若是是漢字的,須要兩位來表示
            // 對所收錄字符進行了「分區」處理,分爲若干區,每區若干碼位
            // 第一個字節爲「高字節」,對應不一樣分區
            // 第二個字節爲「低字節」,對應每一個區的不一樣碼位
            var varlow = code & 65280;
            // 取得低位                
            varlow = varlow >> 8;
            // 取得高位
            var varhigh = code & 255;
            res.push(String.fromCharCode(varlow));
            res.push(String.fromCharCode(varhigh));
        } else {
            res.push(String.fromCharCode(code));
        }
    }
    return res.join('');
};
/**
     * @description 將unicode經過查錶轉換,轉爲gbk的code
     * @param {Number} chrCode 字符unicode編碼
     */
function unicode2GBKCode(chrCode) {
    var chrHex = chrCode.toString(16);
    chrHex = "000" + chrHex.toUpperCase();
    chrHex = chrHex.substr(chrHex.length - 4);
    var i = unicodeCharTable.indexOf(chrHex);
    if (i != -1) {
        chrHex = gbkCharTable.substr(i, 4);
    }
    return parseInt(chrHex, 16)
};

GBK轉UTF-16

步驟描述

  • Step1:獲取該字符對應的Unicode碼
  • Step2:判斷該Unicode的範圍

    • 若是是普通的ASCII碼,則不進行轉換,直接輸出
    • 不然,須要根據GBK和Unicode的對應關係,轉換爲Unicode碼
    • 須要注意的是,這裏因爲GBK採起雙字節編碼的,因此須要用到兩個字節,轉碼時須要將編碼時的運算逆轉,轉爲Unicode碼,而後再輸出相應的字符.轉換完畢

代碼實現

/**
     * @description 將GBK編碼的字符串轉爲utf16編碼的字符串(js內置編碼)
     * @param {String} str GBK編碼的字符串
     * @return {String} 轉化後的utf16字符串
     */
exports.gbkStrToUtf16Str = function(str) {
    if (!str) {
        return '';
    }
    // res是用來存放結果的字符數組,最終會轉爲字符串返回
    var res = [],
    len = str.length;
    for (var i = 0; i < len; i++) {
        // 得到對應的unicode碼
        var code = str.charCodeAt(i);
        // 若是不是ASCII碼
        if (code > 127) {
            // 轉爲unicode    
            // 這裏左移8位是由於編碼時,被右移了8位
            code = gbkCode2Unicode((code << 8) + str.charCodeAt(++i));
        } else {
            // 普通的ASCII碼,什麼都不作                
        }
        res.push(String.fromCharCode(code));
    }
    return res.join('');
};
/**
     * @description將 gbk的對應的code經過查錶轉換,轉爲unicode
     * @param {Number} chrCode gbk字符對應的編碼
     */
function gbkCode2Unicode(chrCode) {
    //以16進制形式輸出字符串
    var chrHex = chrCode.toString(16);
    //
    chrHex = "000" + chrHex.toUpperCase();
    //
    chrHex = chrHex.substr(chrHex.length - 4);

    var i = gbkCharTable.indexOf(chrHex);

    if (i != -1) {
        chrHex = unicodeCharTable.substr(i, 4);
    }
    return parseInt(chrHex, 16)
};

源碼

爲了篇幅,如GBK何Unicode的碼錶映射沒有直接放在文中,詳細能夠參考源碼: https://github.com/dailc/charset-encoding-series

附錄

博客

初次發佈2017.06.03於我的博客

http://www.dailichun.com/2017/06/03/utf8ToUtf16ToGbk.html

參考資料

相關文章
相關標籤/搜索