刨根究底字符編碼之十四——UTF-16究竟是怎么編碼的(“代理區(Surrogate Zone)”,范圍為0xD800~0xDFFF(十進制55296~57343),共2048個碼點未定義。UTF8和UTF32沒有這個問題)


1.

首先要注意的是,代理Surrogate是專屬於UTF-16編碼方式的一種機制,UTF-8和UTF-32是不用代理的。

如前文所述,為了讓UTF-16能繼續編碼基本平面后面的增補平面中的碼點值,於是擴展了UTF-16編碼方式。

具體的擴展方法就是為其增加了代理機制,用兩個對應於基本平面碼點(即BMP代理區中的碼點)的16位碼元來表示一個增補平面碼點,這兩個用來表示一個增補平面碼點的特殊16位碼元就被稱為“代理對”。

如果要用簡單的一句話來概括,就是——所有大於0xFFFF的碼點值(即增補平面碼點編號,范圍為0x10000~0x10FFFF,十進制為65536~1114111;注意,0xFFFF是十六位二進制數的最大值的十六進制表示)要編碼成UTF-16編碼方式的話,就必須使用代理機制(也就是用代理對來表示)。

2.

在UTF-16編碼方式中,被合起來稱為“代理對”的這兩個16位碼元就其中的任一單個碼元而言,其實就直接對應於基本平面BMP中的某一個碼點(即BMP中每一個碼點的值必然對應於一個16位碼元的值,因為基本平面中的碼點總數為2^16=65536個,而16位碼元能表示的值也等於2^16=65536個)。

這樣一來,就產生了沖突:某個UTF-16碼元到底是用於表示基本平面字符的碼元,還是用於表示增補平面字符的代理對中的代理碼元?

因此,為避免沖突,這些被用作“代理”的任一碼元所對應的碼點在基本平面中均未定義字符,即均沒有指定字符。且形成“代理對”的兩個碼元所對應的碼點其編號必定是連續的。

“代理”的真實含義或許就在於此:用兩個基本平面中未定義字符的連續碼點合起來“代為署理”增補平面中的碼點。

因此,基本平面中這些用作“代理”的碼點區域就被稱之為“代理區(Surrogate Zone)”,其碼點編號范圍為0xD800~0xDFFF(十進制55296~57343),共2048個碼點。

3.

增補平面一共有16個平面(即第2平面~第17平面),碼點編號范圍為0x10000~0x10FFFF(十進制為65536~1114111,碼點總數為1048576個)。用兩個代理碼元表示,第一個碼元的取值范圍為0xD800~0xDBFF(二進制為1101 1000 0000 0000 ~ 1101 1011 1111 1111,十進制為55296 ~ 56319),第二個碼元的取值范圍為0xDC00~0xDFFF(二進制為1101 1100 0000 0000 ~ 1101 1111 1111 1111,十進制為56320 ~ 57343)。

因此,增補平面的第一個碼點的編號0x10000其UTF-16編碼就是0xD800 0xDC00(即0x10000經UTF-16編碼后的碼元序列為0xD800 0xDC00),其余類推。展現為二進制形式后如下:

    =====代理碼元1=====    =====代理碼元2=====

    1101 10pp ppxx xxxx     1101 11xx xxxx xxxx

其中代理碼元1中的110110、代理碼元2中的110111是定數,p、x是變數。去掉定數后組合起來就是pppp xxxx xxxx xxxx xxxx,共20位(2^20=1048576),剛好能夠表示目前16個增補平面中的全部碼點(0x10000~0x10FFFF,共1048576個)。其中pppp共4位,表示16個增補平面之一的編號(2^4=16);緊接着的16位x表示某個增補平面內的某個碼點(2^16=65536個碼點,而65536個碼點/平面*16個平面=1048576個碼點)。

4.

按照上面的編碼方式,代理對里面的兩個代理碼元分別稱之為高16位代理碼元(或稱為lead surrogates引導代理、前導代理),和低16位代理碼元(或稱為trail surrogates尾隨代理、后尾代理)。

由於引導代理和尾隨代理的值分別在0xD800~0xDBFF(十進制為55296 ~ 56319)之間和0xDC00~0xDFFF(十進制為56320 ~ 57343)之間,所以首尾兩個代理總共可以組合出(56319-55296+1)*(57343-56320+1)=1048576個代理對,也就是總共可以表示1048576個增補碼點,而目前Unicode標准所確定的16個增補平面的碼點總和也就是65536*16=1048576個。

顯然,Unicode字符集作為開放式字符集,未來不斷增補字符進來,以至於增補平面超過16個,則按目前的UTF-16編碼算法是無法編碼的。也正是因為如此,UTF-16編碼方式的擴展性、適應性是不足的,未來全面被具備高擴展性、高適應性的UTF-8編碼方式代替是必然的。

笨笨阿林原創文章,轉載請注明出處)

5.

從增補平面的碼點值通過基本平面中的代理對編碼為增補平面字符的碼元序列的具體算法如下:

1) 增補平面中的碼點值(0x10000~0x10FFFF,二進制為0001 0000 0000 0000 0000~1 0000 1111 1111 1111 1111,對應的碼點名稱為U+10000~U+10FFFF)減去0x10000(二進制為0001 0000 0000 0000 0000),可得到20位長的比特組(值的范圍為0x00000~0xFFFFF,二進制為0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111);

2)將得到的20位長的比特組分拆為兩部分:高位10比特和低位10比特;

3)20位長的比特組中的高位10比特(值的范圍為0x000~0x3FF,二進制為00 0000 0000~11 1111 1111)加上0xD800(二進制為1101 1000 0000 0000),得到第一個代理碼元即引導代理(值的范圍是0xD800~0xDBFF,二進制為1101 1000 0000 0000 ~ 1101 1011 1111 1111);

4)20位長的比特組中的低位10比特(值范圍也是0x000~0x3FF,二進制為00 0000 0000~11 1111 1111)加上0xDC00(二進制為1101 1100 0000 0000),得到第二個代理碼元即尾隨代理(值的范圍是0xDC00~0xDFFF,二進制為1101 1100 0000 0000 ~ 1101 1111 1111 1111);

5)將引導代理與尾隨代理按前后順序組合在一起成為“代理對”,就得到了增補平面字符的碼元序列。

例如,增補平面中碼點值為10437(碼點名稱為U+10437)的字符(?):

1)0x10437減去0x10000,結果為0x00437,二進制為0000 0000 0100 0011 0111。

2)分拆成高10位值和低10位值兩部分:0000000001(即0x0001)及0000110111(即0x0037)。

3)添加0xD800到高位值,以形成高位的引導代理:0xD800 + 0x0001 = 0xD801(二進制為1101 1000 0000 0001)。

4)添加0xDC00到低位值,以形成低位的尾隨代理:0xDC00 + 0x0037 = 0xDC37(二進制為1101 1100 0011 0111)。

5)將高位的引導代理與低位的尾隨代理按前后順序組合在一起成為“代理對”,就得到了增補平面字符?(碼點名稱為U+10437)的碼元序列:1101 1000 0000 0001 1101 1100 0011 0111。

6.

下表總結了該轉換。不同的顏色表示碼點值是如何被分布到UTF-16碼元序列中的,而由UTF-16編碼過程中加入的代理附加位則以不同的紅色(亮紅色與暗紅色)顯示:

7.

顯然,增補平面中的碼點值從0x10000到0x10FFFF,共計0xFFFFF + 0x1個,即1,048,576個,剛好也就是需要20位來表示(2^20=1,048,576)。如果用兩個16位長的碼元組成的序列來表示,意味着引導代理要容納上述20位中的前10位,尾隨代理要容納上述20位中的后10位。

另外,還要能夠根據每個16位碼元來直接判斷該碼元到底是屬於引導代理(標志位為前6位11 0110,還剩下10位,因此總個數為2^10=1024個),還是屬於尾隨代理(標志位為前6位11 0111,也剩下10位,因此總個數也是2^10=1024個)。

為避免沖突,因此需要在基本多語言平面BMP中保留未定義Unicode字符的1024+1024=2048個碼點,就可以容納引導代理與尾隨代理所需要的編號空間(碼點空間、代碼空間),也就是16個增補平面所需要的編號空間,共計1024*1024=2^20=1048576個碼點。這BMP中的2048個碼點對於BMP總計65536個碼點來說,僅占3.125%(2048/65536=0.03125)。

8.

在UTF-16編碼方式中,引導代理的后面應該是一個尾隨代理,而尾隨代理的前面就應該是一個引導代理;不能出現一個引導代理的后面是一個非代理的普通UTF-16碼元的情況,也不能出現一個引導代理的后面還是一個引導代理的情況。

UTF-16文本(字符串)的最后一個碼元不能是引導代理,不允許出現一個尾隨代理的前面是一個尾隨代理的情況,也不允許出現一個尾隨代理的前面是一個非代理的普通UTF-16碼元的情況;UTF-16文本(字符串)的第一個碼元不能是尾隨代理。

而單獨的一個代理碼元(不管是引導代理還是尾隨代理)是不合法的,代理必須以一個“引導代理+尾隨代理”編碼對(即代理對)的形式出現。

笨笨阿林原創文章,轉載請注明出處)

9.

UTF-16的這種“代理對”編碼規則保證了文本處理程序能夠正確地訪問和處理包括了基本平面和增補平面在內的全部UTF-16碼元序列,並消除了基本平面字符和增補平面字符之間發生沖突的可能性。

因為引導代理和尾隨代理碼元被各自規定在一個特定范圍內取值,所以很簡單的一個原則就是:凡是在代理編碼范圍內的碼元就是“代理”增補平面SP字符的“代理碼元”,否則就是“基本平面BMP字符的碼元”。由於BMP中的字符碼元和代理碼元分別在各自獨立的編碼范圍內進行編碼,所以對於一個符合格式規范的UTF-16碼元來講,它必須滿足以下條件:

  - 非代理碼元(BMP字符碼元)必須避開代理碼元所占用的范圍0xD800~0xDFFF(二進制為1101 1000 0000 0000 ~ 1101 1111 1111 1111,共2048個);

  - 引導代理必須是代理對中的第一個碼元;

  - 尾隨代理必須是代理對中的第二個碼元。

在處理UTF-16文本時,為了確保文本數據的完整性,絕對不能把任意一個代理從代理對中拆出來,也不能在代理對中間插入另一個字符的碼元或碼元序列。

10.

在UTF-16編碼方式里面,一個Unicode字符碼點值由一個或兩個16位碼元編碼。所以,如果想在一個UTF-16碼元序列里面判斷某個碼元是屬於哪個字符的話,就需要檢查那個碼元的值,然后根據碼元的類型(是否具有代理標志位)決定是否還需要向前或向后檢查一個相鄰的碼元的值(可以不必理會除了前后相鄰的兩個碼元之外的其他碼元)。

由於引導代理、尾隨代理、BMP字符碼元,三者互不重疊,搜索就很簡單,這意味着UTF-16具有“自同步”(self-synchronizing)性:通過僅檢查一個碼元就可以判斷當前字符的下一個字符的起始碼元,每個字符碼元的邊界很明確;同時,還具有“非傳遞”性:單獨的一個UTF-16碼元出錯涉及的只是一個字符,不會傳遞到文本的其他部分去,因此,即使文本中某些字符數據遭到破壞,其影響也只是局部性的。

UTF-8也有類似優點。但許多早期的編碼方式就不是自同步的,比如大多數的多字節編碼標准如GBK、Big5等,必須從頭開始分析文本才能確定不同字符的碼元的邊界;也不具有非傳遞性,局部字符數據被破壞,很可能傳遞到整個文件,導致整個文件無法正確顯示。

因此,UTF-8和UTF-16編碼方式所具有的“自同步性”、“非傳遞性”等特點除了增強抗干擾能力外,也提供了隨機訪問的能力。

11.

由於在大多數的文本數據中,代理對(即增補平面字符碼元序列)出現的概率是很小的,很多情況下處理的還是非代理對(即基本平面字符碼元序列),導致許多軟件處理代理對的部分往往得不到充分的測試。這導致了一些長期的bug與潛在安全漏洞,甚至有些廣為流行、得到良好評價的優秀軟件也是如此。

因此,雖然編程時同時考慮文本中可能出現的不同存儲長度的字符(基本平面字符是單16位編碼,即單碼元編碼;增補平面字符是雙16位編碼,即雙碼元編碼)並相應做出不同的處理,會比單純只考慮16位編碼在性能上要遜色一些。但實際上,現有的遵循定長16位編碼規范但不能處理代理對的程序只需做很小的一點修改就可以同時處理基本平面字符和增補平面字符的編碼了。

另外,需要特別注意的是,雖然Unicode標准規定BMP代理區(U+D800~U+DFFF)的碼點值不對應於任何字符,即未作定義,但在UCS-2中,U+D800~U+DFFF是被定義了的,也就是已經用於某些字符了。不過,只要前后兩個16位碼元不是恰好構成了代理對,許多程序還是能把這些不匹配Unicode標准的字符碼元正確地辨識、轉換成合規的碼元。這種由歷史原因造成的碼元序列按現在的Unicode標准來看,應算作是編碼錯誤。

笨笨阿林原創文章,轉載請注明出處)

 

(未完待續)

https://zhuanlan.zhihu.com/p/27827951


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM