博客 / 詳情

返回

對於 UTF-16 的高低代理項碼點的解析

起因:

​ 首先是我寫的這一段代碼, 這是一個手搓的 Json 解析器內部的一個, 把轉義字符還原成 UTF-16 的一個邏輯代碼:

case 'u': //Unicode 字符
    if (i + 4 < text.Length)
    {
        byte[] bytes;
        string u = text.Substring(i + 1, 4);
        if (Command.string2bytes(u, out bytes) == true)
        {
            i = i + 4; //這裏不因該是 i + 5, 因為下一次循環會 i++
            tmp_str = tmp_str + (string)char.ConvertFromUtf32((int)Command.sum4bytes(bytes));
        }
        else
        {
            tmp_str = tmp_str + "\\u";
        }
    }
    else
    {
        tmp_str = tmp_str + "\\u";
    }
break;

​ 正常來説它可以把一些類似於 \u5bd2\u5bd2\u5929\u4e0b\u7b2c\u4e00\u53ef\u7231\uff01 這樣的 Unicode 給轉換成 寒寒天下第一可愛!, 但是遇到 emoji 的文本時, 就拋出異常了:

System.ArgumentOutOfRangeException:“有效的 UTF32 值介於 0x000000 和 0x10ffff 之間(包括這兩者),而且不能包含代理項碼位值(0x00d800 ~ 0x00dfff)。”

​ 嗯, 沒見過的異常, 頭一次見到代理項碼位值, 然後在網路上查了一下這個玩意

什麼是代理項:

​ 在常規 UTF-16 編碼中, 文本是以 2 字節單位儲存的, 滿足一些常用字符的顯示. 但是對於 emoji 和其他生僻字符卻不夠用, 所以, 在原有的 0x0000 至 0xFFFF 區間, 劃分了一些區域給代理項使用, 然後通過代理項轉換最後映射為補充碼點.

  • 首先是高代理項碼點, 它是識別拓展字符的關鍵, 區間在 0xD800 至 0xDBFF.

  • 尾隨高代理項的是低代理項碼點, 區間在 0xDC00 至 0xDFFF.

  • 補充碼點, 用於拓展一些不常見的生僻的字符, 或者 emoji 及其他符號, 代理項最終要映射在此區間, 區間在 0x010000 至 0x10FFFF.

怎麼解析:

補充代碼點 => 代理項代碼點
TMP = INPUT - 0x10000
HSCP = (TMP / 0x400) + 0xD800
LSCP = TMP % 0x400 + 0xDC00
代理項代碼點 => 補充代碼點
OUTPUT = ((HSCP - 0xD800) * 0x0400) + (LSCP - 0xDC00) + 0x010000

​ 按照以上流程, 用 C# 寫的話, 如下所示

case 'u': //Unicode 字符
    if (i + 4 < text.Length)
    {
        byte[] bytes;
        string u = text.Substring(i + 1, 4);
        if (Command.TryString2Bytes(u, out bytes) == true)
        {
            int hex = (int)Command.Sum4Bytes(bytes);
            //對於代理位解析, 參看一下連接, 感謝!
            //https://zhuanlan.zhihu.com/p/147339588
            //https://www.cnblogs.com/sailJs/p/18397567
            if (hex > 55295 && hex < 56320) // D800 到 DBFF //高代理位 //emoji 那些
            {
                if(i + 10 < text.Length && text[i + 5] == '\\' && text[i + 6] == 'u')
                {
                    u = text.Substring(i + 7, 4);
                    if (Command.TryString2Bytes(u, out bytes) == true)
                    {
                        int HSCP = hex;
                        int LSCP = (int)Command.Sum4Bytes(bytes);
                        if (LSCP < 56320 || LSCP > 57343) //低代理位, 在 DC00 到 DFFF 
                        {
                            tmp_str = tmp_str + "\\u";
                        }
                        else
                        {
                            //HSCP = HSCP - (int)Command.Sum4Bytes(Command.String2Bytes("D800"));
                            HSCP = HSCP - 55296;
                            HSCP = HSCP * 1024;
                            //LSCP = LSCP - (int)Command.Sum4Bytes(Command.String2Bytes("DC00"));
                            LSCP = LSCP - 56320;
                            //hex = HSCP + LSCP + (int)Command.Sum4Bytes(Command.String2Bytes("010000"));
                            hex = HSCP + LSCP + 65536;
                            i = i + 10;
                            tmp_str = tmp_str + (string)char.ConvertFromUtf32(hex);
                        }
                    }
                    else
                    {
                        tmp_str = tmp_str + "\\u";
                    }
                }
                else
                {
                    tmp_str = tmp_str + "\\u";
                }
            }

            else 
            {
                i = i + 4; //這裏不因該是 i + 5, 因為下一次循環會 i++
                tmp_str = tmp_str + (string)char.ConvertFromUtf32(hex);
            }
        }
        else
        {
            tmp_str = tmp_str + "\\u";
        }
    }
    else
    {
        tmp_str = tmp_str + "\\u";
    }
    break;

​ 總之就是, 在俺不瞭解代理項之前, 俺一直以為一個 \uXXXX 代表一個 char, 瞭解代理項之後就明白, 只要遇到一個處於高代理項的字符時, 它的後面必定會有一個低代理項 (即兩個 char).

參考連接:

  • Unicode | 代理項(Surrogate)

  • Unicode編碼 - 代理區和4字節codePoint

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.