可能是最詳細的字元編碼詳解

安穩.發表於2019-04-03

Created By JishuBao on 2019-04-02 12:38:22
Recently revised in 2019-04-03 12:38:22

 

  歡迎大家來到技術寶的掘金世界,您的star是我寫文章最大的動力!GitHub地址     

文章簡介:

1、遇上emoji

2、字元編碼的故事

3、字元編碼詳解

4、字元編碼區別

5、談談emoji

一、遇上emoji

最近閒來無事,在看原始碼,發現一個很有意思的事情。

可能是最詳細的字元編碼詳解
當時就震驚了,這個emoj表情好牛逼啊...直接在編輯器裡面就有表情了,頓時就感覺不懂原理,各種字元編碼都理解的雲裡霧裡的,只知道一個utf-8,所以準備搜尋大量資料準備把這個字元編碼瞭解通透!

二、字元編碼的故事

很久很久以前,有一群人,他們決定用8個可以開合的電晶體來組合成不同的狀態,以表示世界上的萬物。他們看到8個開關狀態是好的,於是他們把這稱為”位元組“。再後來,他們又做了一些可以處理這些位元組的機器,機器開動了,可以用位元組來組合出很多狀態,狀態開始變來變去。他們看到這樣是好的,於是它們就這機器稱為”計算機“。

開始計算機只在美國用。八位的位元組一共可以組合出256(2的8次方)種不同的狀態。 他們把其中的編號從0開始的32種狀態分別規定了特殊的用途,一但終端、印表機遇上約定好的這些位元組被傳過來時,就要做一些約定的動作:

遇上0×10, 終端就換行;

遇上0×07, 終端就向人們嘟嘟叫;

遇上0x1b, 印表機就列印反白的字,或者終端就用彩色顯示字母。

他們看到這樣很好,於是就把這些0×20以下的位元組狀態稱為”控制碼”。他們又把所有的空格、標點符號、數字、大小寫字母分別用連續的位元組狀態表示,一直編到了第127號,這樣計算機就可以用不同位元組來儲存英語的文字了。大家看到這樣,都感覺 很好,於是大家都把這個方案叫做 ANSI 的”Ascii”編碼(American Standard Code for Information Interchange,美國資訊互換標準程式碼)。當時世界上所有的計算機都用同樣的ASCII方案來儲存英文文字。

後來,就像建造巴比倫塔一樣,世界各地都開始使用計算機,但是很多國家用的不是英文,他們的字母裡有許多是ASCII裡沒有的,為了可以在計算機儲存他們的文字,他們決定採用 127號之後的空位來表示這些新的字母、符號,還加入了很多畫表格時需要用下到的橫線、豎線、交叉等形狀,一直把序號編到了最後一個狀態255。從128 到255這一頁的字符集被稱”擴充套件字符集“。從此之後,貪婪的人類再沒有新的狀態可以用了,美帝國主義可能沒有想到還有第三世界國家的人們也希望可以用到計算機吧!

等中國人們得到計算機時,已經沒有可以利用的位元組狀態來表示漢字,況且有6000多個常用漢字需要儲存呢。但是這難不倒智慧的中國人民,我們不客氣地把那些127號之後的奇異符號們直接取消掉, 規定:一個小於127的字元的意義與原來相同,但兩個大於127的字元連在一起時,就表示一個漢字,前面的一個位元組(他稱之為高位元組)從0xA1用到0xF7,後面一個位元組(低位元組)從0xA1到0xFE,這樣我們就可以組合出大約7000多個簡體漢字了。在這些編碼裡,我們還把數學符號、羅馬希臘的字母、日文的假名們都編進去了,連在 ASCII 裡本來就有的數字、標點、字母都統統重新編了兩個位元組長的編碼,這就是常說的”全形”字元,而原來在127號以下的那些就叫”半形”字元了。中國人民看到這樣很不錯,於是就把這種漢字方案叫做 “GB2312“。GB2312 是對 ASCII 的中文擴充套件

但是中國的漢字太多了,我們很快就就發現有許多人的人名沒有辦法在這裡打出來,特別是某些很會麻煩別人的國家領導人。於是我們不得不繼續把GB2312 沒有用到的碼位找出來老實不客氣地用上。後來還是不夠用,於是乾脆不再要求低位元組一定是127號之後的內碼,只要第一個位元組是大於127就固定表示這是一個漢字的開始,不管後面跟的是不是擴充套件字符集裡的內容。結果擴充套件之後的編碼方案被稱為 GBK 標準,GBK包括了GB2312 的所有內容,同時又增加了近20000個新的漢字(包括繁體字)和符號。 後來少數民族也要用電腦了,於是我們再擴充套件,又加了幾千個新的少數民族的字,GBK擴成了 GB18030。從此之後,中華民族的文化就可以在計算機時代中傳承了。 中國的程式設計師們看到這一系列漢字編碼的標準是好的,於是通稱他們叫做 “DBCS“(Double Byte Charecter Set 雙位元組字符集)。在DBCS系列標準裡,最大的特點是兩位元組長的漢字字元和一位元組長的英文字元並存於同一套編碼方案裡,因此他們寫的程式為了支援中文處理,必須要注意字串裡的每一個位元組的值,如果這個值是大於127的,那麼就認為一個雙位元組字符集裡的字元出現了。那時候凡是受過加持,會程式設計的計算機僧侶們都要每天念下面這個咒語數百遍: “一個漢字算兩個英文字元!一個漢字算兩個英文字元……

因為當時各個國家都像中國這樣搞出一套自己的編碼標準,結果互相之間誰也不懂誰的編碼,誰也不支援別人的編碼,連大陸和臺灣這樣只相隔了150海里,使用著同一種語言的兄弟地區,也分別採用了不同的 DBCS 編碼方案——當時的中國人想讓電腦顯示漢字,就必須裝上一個”漢字系統”,專門用來處理漢字的顯示、輸入的問題,像是那個臺灣的愚昧封建人士寫的算命程式就必須加裝另一套支援 BIG5 編碼的什麼”倚天漢字系統”才可以用,裝錯了字元系統,顯示就會亂了套!這怎麼辦?而且世界民族之林中還有那些一時用不上電腦的窮苦人民,他們的文字又怎麼辦? 真是計算機的巴比倫塔命題啊!

正在這時,大天使加百列及時出現了——一個叫 ISO(國際標誰化組織)的國際組織決定著手解決這個問題。他們採用的方法很簡單:廢了所有的地區性編碼方案,重新搞一個包括了地球上所有文化、所有字母和符號 的編碼!他們打算叫它”Universal Multiple-Octet Coded Character Set”,簡稱 UCS, 俗稱 “unicode“。

unicode開始制訂時,計算機的儲存器容量極大地發展了,空間再也不成為問題了。於是 ISO 就直接規定必須用兩個位元組,也就是16位來統一表示所有的字元,對於ASCII裡的那些“半形”字元,unicode包持其原編碼不變,只是將其長度由原來的8位擴充套件為16位,而其他文化和語言的字元則全部重新統一編碼。由於”半形”英文符號只需要用到低8位,所以其高8位永遠是0,因此這種大氣的方案在儲存英文文字時會多浪費一倍的空間。

這時候,從舊社會裡走過來的程式設計師開始發現一個奇怪的現象:他們的 strlen 函式靠不住了,一個漢字不再是相當於兩個字元了,而是一個!是的,從unicode開始,無論是半形的英文字母,還是全形的漢字,它們都是統一的”一個字元“!同時,也都是統一的”兩個位元組“,請注意”字元”和”位元組”兩個術語的不同,“位元組”是一個8位的物理存貯單元,而“字元”則是一個文化相關的符號。在unicode中,一個字元就是兩個位元組。一個漢字算兩個英文字元的時代已經快過去了。

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

unicode在很長一段時間內無法推廣,直到網際網路的出現,為解決unicode如何在網路上傳輸的問題,於是面向傳輸的眾多 UTF(UCS Transfer Format)標準出現了,顧名思義,UTF-8就是每次8個位傳輸資料,而UTF-16就是每次16個位。UTF-8就是在網際網路上使用最廣的一種unicode的實現方式,這是為傳輸而設計的編碼,並使編碼無國界,這樣就可以顯示全世界上所有文化的字元了。UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度,當字元在ASCII碼的範圍時,就用一個位元組表示,保留了ASCII字元一個位元組的編碼做為它的一部分,注意的是unicode一箇中文字元佔2個位元組,而UTF-8一箇中文字元佔3個位元組)。從unicode到utf-8並不是直接的對應,而是要過一些演算法和規則來轉換。

Unicode符號範圍 UTF-8編碼方式
0000 0000-0000 007F 0xxxxxxx
0000 0080-0000 07FF 110xxxxx 10xxxxxx
0000 0800-0000 FFFF 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

最後簡單總結一下:

中國人民通過對 ASCII 編碼的中文擴充改造,產生了 GB2312 編碼,可以表示6000多個常用漢字。

漢字實在是太多了,包括繁體和各種字元,於是產生了 GBK 編碼,它包括了 GB2312 中的編碼,同時擴充了很多。

中國是個多民族國家,各個民族幾乎都有自己獨立的語言系統,為了表示那些字元,繼續把 GBK 編碼擴充為 GB18030 編碼。

每個國家都像中國一樣,把自己的語言編碼,於是出現了各種各樣的編碼,如果你不安裝相應的編碼,就無法解釋相應編碼想表達的內容。

終於,有個叫 ISO 的組織看不下去了。他們一起創造了一種編碼 UNICODE ,這種編碼非常大,大到可以容納世界上任何一個文字和標誌。所以只要電腦上有 UNICODE 這種編碼系統,無論是全球哪種文字,只需要儲存檔案的時候,儲存成 UNICODE 編碼就可以被其他電腦正常解釋。

UNICODE 在網路傳輸中,出現了兩個標準 UTF-8 和 UTF-16,分別每次傳輸 8個位和 16個位。於是就會有人產生疑問,UTF-8 既然能儲存那麼多文字、符號,為什麼國內還有這麼多使用 GBK 等編碼的人?因為 UTF-8 等編碼體積比較大,佔電腦空間比較多,如果面向的使用人群絕大部分都是中國人,用 GBK 等編碼也可以。

三、字元編碼詳解

1.為何需要編碼

我們知道,所有的資訊最終都表示為一個二進位制的字串每一個二進位制位(bit)有0和1兩種狀態。當我們需要把字元'A'存入計算機時,應該對應哪種狀態呢,儲存時,我們可以將字元'A'用01000010(這個隨便編的)二進位制字串表示,存入計算機;讀取時,再將01000010還原成字元'A'。那麼問題來了,儲存時,字元'A'應該對應哪一串二進位制數呢,是01000010?或者是10000000 11110101?說白了,就是需要一個規則。這個規則可以將字元對映到唯一一種狀態(二進位制字串),這就是編碼。而最早出現的編碼規則就是ASCII編碼,在ASCII編碼規則中,字元'A'既不對應01000010,也不對應1000 0000 11110101,而是對應**01000001*(不要問為什麼,這是規則)。

2.ASCII

這套編碼規則是由美國定製,一共規定了128個字元的編碼,比如空格"SPACE"是32(十進位制)(二進位制00100000),大寫的字母A是65(二進位制01000001)。這128個符號(包括 32個不能列印出來的控制符號),只佔用了一個位元組(8 bit)的後面7位,最前面的1位統一規定為0總共才有128個字元編碼,一個位元組都沒有用完,這好像似乎有點太少了。於是乎,就開始壓榨最高位,對其為1時也進行編碼,利用最高位進行編碼的方式就稱為非ASCII編碼,如ISO-8859-1編碼。

3.ISO-8859-1

這套編碼規則由ISO組織制定。是在 ASCII 碼基礎上又制定了一些標準用來擴充套件ASCII編碼,即 00000000(0) ~ 01111111(127) 與ASCII的編碼一樣,對 10000000(128) ~ 11111111(255)這一段進行了編碼,如將字元**§編碼成 10100111(167)。ISO-8859-1編碼也是單位元組編碼,最多能夠表示256**個字元。Latin1是ISO-8859-1的別名,有些環境下寫作Latin-1。但是,即使能夠表示256個字元,對中文而言,還是太少了,一個位元組肯定不夠,必須用多個位元組表示。但是,由於是單位元組編碼,和計算機最基礎的表示單位一致,所以很多時候,仍舊使用 ISO8859-1編碼來表示。而且在很多協議上,預設使用該編碼。比如,雖然"中文"兩個字不存在ISO8859-1編碼,以GB2312編碼為例,應該是D6D0 CEC4兩個字元,使用ISO8859-1編碼的時候則將它拆開為4個位元組來表示:D6D0 CEC4(事實上,在進行儲存的時候,也是以位元組為單位進行處理)。而如果是UTF編碼,則是6個位元組e4 b8 ad e6 96 87。很明顯,這種表示方法還需要以另一種編碼為基礎才能正確顯示。而常見的中文編碼方式有GB2312、BIG5、GBK。

4.GB2312

GB2312其對所收錄字元進行了"分割槽"處理,共94個區,區從1(十進位制)開始,一直到94(十進位制),每區含有94個位,位從1(十進位制)開始,一直到94(十進位制),共8836(94 * 94)個碼位,這種表示方式也稱為區位碼,GB2312是雙位元組編碼,其中高位元組表示區,低位元組表示位。各區具體說明如下:

01-09區收錄除漢字外的682個字元,有164個空位(9 * 94 - 682)。
10-15區為空白區,沒有使用。
16-55區收錄3755個一級漢字(簡體),按拼音排序。
56-87區收錄3008個二級漢字(簡體),按部首/筆畫排序。
88-94區為空白區,沒有使用。
複製程式碼

那麼根據區位碼如何算出GBK2312編碼呢?區位碼的表示範圍為0101 - 9494(包含了空的區位碼)。點選這裡,檢視中GB2312編碼區位碼。之後只需要按照如下規則進行轉化即可。

  1. 將區(十進位制)轉化為十六進位制。
  2. 將轉化的十六進位制加上A0,得到GB2312編碼的高位元組。
  3. 將位(十進位制)轉化為十六進位制。
  4. 將轉化的十六進位制加上A0,得到GB2312編碼的低位元組。
  5. 組合區和位,區在高位元組,位在低位元組。
  6. 得到GB2312編碼。

可能是最詳細的字元編碼詳解
例如:'李'字的區位碼為3278(表示在32區,78位)。

  1. 將32(區)轉化為十六進位制為20。
  2. 加上A0為C0。
  3. 將78(位)轉化為十六進位制為4E。
  4. 加上A0為EE。
  5. 組合區和位,為C0EE。
  6. 得到GB2312編碼,即'李'字的GB2312編碼為C0EE。

GB2312用兩個位元組編碼,採用分割槽編碼,總共編碼的中文個數為6763(3755 + 3008)。這些漢字只是最常用的漢字,已經覆蓋中國大陸99.75%的使用頻率。但是,還有一些漢字在GB2312中沒有被編碼,如'鎔'字,在GB2312中就沒有被編碼,這樣就導致了問題,隨之就出現了主流的GBK編碼。在講解GBK編碼之前,我們另外講解一下BIG5編碼。

5.BIG5

BIG5採用雙位元組編碼,使用兩個位元組來表示一個字元。高位位元組使用了0x81-0xFE,低位位元組使用了0x40-0x7E,及0xA1-0xFE。該編碼是繁體中文字符集編碼標準,共收錄13060箇中文字,其中有二字為重複編碼,即“兀、兀”(A461及C94A)和“嗀、嗀”(DCD1及DDFC)。具體的分割槽如下:  

8140-A0FE 保留給使用者自定義字元(造字區)
A140-A3BF 標點符號、希臘字母及特殊符號。其中在A259-A261,收錄了度量衡單位用字:兙兛兞兝兡兣嗧瓩糎。
A3C0-A3FE 保留。此區沒有開放作造字區用。
A440-C67E 常用漢字,先按筆劃再按部首排序。
C6A1-F9DC 其它漢字。
F9DD-F9FE 製表符。
複製程式碼

點選這裡,檢視BIG5編碼。注意,BIG5編碼與GBK編碼沒有什麼關係。

6.GBK

GBK編碼擴充套件了GB2312,完全相容GB2312編碼(如'李'字的GBK、GB2312編碼均為C0EE),但其不相容BIG5編碼('長'字的BIG5編碼為AAF8,GBK編碼為E94C,'李'字的BIG5編碼為A7F5 不等於C0EE),即如果使用GB2312編碼,使用GBK解碼是完全正常的,但是如果使用BIG5編碼,使用GBK解碼,會出現亂碼。相比於GB2312編碼,GBK編碼了更多漢字,如'鎔'字。GBK編碼依然採用雙位元組編碼方案,其編碼範圍:8140-FEFE,剔除xx7F碼位,共23940個碼位。能表示 21003 個漢字。點選這裡,檢視GBK編碼。點選這裡,可以查詢中文的其他編碼。在GBK之後又出現了GB18030編碼,但是沒有形成主流,故不做介紹,至此,中文編碼的問題已經講解完成。那麼問題又來了,大陸網民與在海峽兩岸網民交流時,若都使用GBK編碼,則沒有問題,若一方使用GBK編碼,一方使用BIG5編碼,那麼就會出現亂碼問題,這是在海峽兩岸網民交流,如果漂洋過海進行交流呢?那就更容易出現亂碼問題,這時候我們可能想,要是有一套全世界都通用的編碼就好了,不要擔心,這樣的編碼確實是存在的,那就是Unicode。

7.Unicode

有兩個獨立的, 創立單一字符集的嘗試. 一個是國際標準化組織(ISO)的 ISO 10646 專案, 另一個是由多語言軟體製造商組成的協會組織的 Unicode 專案. 在1991年前後, 兩個專案的參與者都認識到, 世界不需要兩個不同的單一字符集. 它們合併雙方的工作成果, 併為創立一個單一編碼表而協同工作. 兩個專案仍都存在並獨立地公佈各自的標準, 但 Unicode 協會和 ISO/IEC JTC1/SC2 都同意保持 Unicode 和 ISO 10646 標準的碼錶相容, 並緊密地共同調整任何未來的擴充套件。

Unicode是指一張表,裡面包含了可能出現的所有字元,每個字元對應一個數字,這個數字稱為碼點(Code Point),如字元'H'的碼點為72(十進位制),字元'李'的碼點為26446(十進位制)。Unicode表包含了1114112個碼點,即從000000(十六進位制) - 10FFFF(十六進位制)。地球上所有字元都可以在Unicode表中找到對應的唯一碼點。點選這裡,查詢字元對應的碼點。Unicode將碼空間劃分為17個平面,從00 - 10(十六進位制,最高兩位),即從0 - 16(十進位制),每個平面有65536個碼點(2^16),其中最重要的是第一個Unicode平面(碼位從0000 - FFFF),包含了最常用的字元,該平面被稱為基本多語言平面(Basic Multilingual Plane),縮寫為BMP,其他平面稱為輔助平面(Supplementary Planes),在基本多文種平面內, 從D800到DFFF之間的碼位區段是永久保留不對映到字元的, 因此UTF-16編碼巧妙的利用了這保留下來的碼位來對輔助平面內的字元進行編碼,這點後面進行講解。Unicode只是一個符號集,只規定的字元所對應的碼點,並沒有指定如何儲存,如何進行儲存出現了不同的編碼方案,關於Unicode編碼方案主要有兩條主線:UCS和UTF。UTF主線由Unicode Consortium進行維護管理,UCS主線由ISO/IEC進行維護管理。

8.UCS

UCS全稱為"Universal Character Set",在UCS中主要有UCS-2和UCS-4。

1.UCS-2

UCS-2是定長位元組的,固定使用2個位元組進行編碼,從0000(十六進位制)- FFFF(十六進位制)的碼位範圍,對應第一個Unicode平面。採用BOM(Byte Order Mark)機制,該機制作用如下:

  1. 確定位元組流採用的是大端序還是小端序。
  2. 確定位元組流的Unicode編碼方案。

2.UCS-4

UCS-4是定長位元組的,固定使用4個位元組進行編碼。也採用了BOM機制。

9.UTF

UTF全稱為"Unicode Transformation Format",在UTF中主要有UTF-8,UTF-16和UTF-32。

1.UTF-8

UTF-8是一種變長編碼方式,使用1-4個位元組進行編碼。UTF-8完全相容ASCII,對於ASCII中的字元,UTF-8採用的編碼值跟ASCII完全一致。UTF-8是Unicode一種具體的編碼實現。UTF-8是在網際網路上使用最廣的一種Unicode的編碼規則,因為這種編碼有利於節約網路流量(因為變長編碼,而非統一長度編碼)。關於Unicode碼點如何轉化為UTF-8編碼,可以參照如下規則:

  • 對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。
  • 對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。剩下的沒有提及的二進位制位,全部為這個符號的unicode碼。
Unicode符號範圍 UTF-8編碼方式
(十六進位制) (十進位制) (二進位制
0000 0000-0000 007F (0-127) 0xxxxxxx
0000 0080-0000 07FF (128-2047) 110xxxxx 10xxxxxx
0000 0800-0000 FFFF (2048-65535) 1110xxxx 10xxxxxx 10xxxxxx
001 0000-0010 FFFF (65536-1114111) 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

說明:字元'A'的Unicode碼點為65(十進位制),根據上表,在第一行範圍,則字元'A'的UTF-8編碼為01000001,中文字元'李'的Unicode碼點為26446(十進位制),二進位制為01100111 01001110,十六進位制為674E。根據上表,在第三行範圍,則將'李'二進位制程式碼從低位到高位依次填入x中,不足的填入0。得到UTF-8編碼為11100110 10011101 10001110,即E69D8E(十六進位制)。

由上述編碼規則可知,0000 0000 - 0000 FFFF(第一行到第三行)為Unicode第一個平面(基本多語言平面),而0001 0000 - 10 FFFF(第四行)為Unicode其他平面(輔助平面)。在基本多語言平面對應了絕大多數常用的字元。對於大於65535(十進位制)的碼點,即在輔助平面上的碼點,需要使用4個位元組來進行UTF-8編碼。

2.UTF-16

UTF-8是不定長的編碼,使用1、2、3、4個位元組編碼,而UTF-16則只使用2或4個位元組編碼。UTF-16也是Unicode一種具體的編碼實現。關於Unicode如何轉化為UTf-16編碼規則如下:

  1. 若Unicode碼點在第一平面(BPM)中,則使用2個位元組進行編碼。
  2. 若Unicode碼點在其他平面(輔助平面),則使用4個位元組進行編碼。

關於輔助平面的碼點編碼更詳細解析如下:輔助平面碼點被編碼為一對16位元(四個位元組)長的碼元, 稱之為代理對(surrogate pair), 第一部分稱為高位代理(high surrogate)或前導代理(lead surrogates),碼位範圍為:D800-DBFF. 第二部分稱為低位代理(low surrogate)或後尾代理(trail surrogates), 碼位範圍為:DC00-DFFF。注意,高位代理的碼位從D800到DBFF,而低位代理的碼位從DC00到DFFF,總共恰好為D800-DFFF,這部分碼點在第一平面內是保留的,不對映到任何字元,所以UTF-16編碼巧妙的利用了這點來進行碼點在輔助平面內的4位元組編碼。

說明:字元'A'的Unicode碼點為65(十進位制),十六進位制表示為41,在第一平面。根據規則,UTF-16採用2個位元組進行編碼。那麼問題又來了,知道了採用兩個位元組編碼,並且我們也知道計算機是以位元組為單位進行儲存,這兩個位元組應該表示為00 41(十六進位制)?或者是41 00(十六進位制)呢?這就引出了一個問題,需要用到之前提及的BOM機制來解決。

表示為00 41意味著採用了大端序(Big endian),而表示為41 00意味著採用了小端序。那麼計算機如何知道儲存的字元資訊採用了大端序還是小端虛呢?這就需要加入一些控制資訊,具體是採用大端序,則在檔案前加入FE FF,採用小端序,則在檔案前加入FF FE。這樣,當計算開始讀取時發現前兩個位元組為FE FF,就表示之後的資訊採用的是小端序,反之,則是大端序。

可能是最詳細的字元編碼詳解
字元 (無法顯示,只能截圖顯示),其Unicode碼點為65902(十進位制),十六進位制為1016E,很顯然,已經超出了第一平面(BMP)所能表示的範圍。其在輔助平面內,根據規則,UTF-16採用4個位元組進行編碼。然而其編碼不是簡單擴充套件為4個位元組(00 01 01 6E),而是採用如下規則進行計算。

  1. 使用Unicode碼位減去100000(十六進位制),得到的值擴充套件20位(因為Unicode最大為10 FF FF(十六進位制),減去1 00 00(十六進位制)後,得到的結果最大為0FFF FF(十六進位制),即為20位,不足20位的,在高位加一個0,擴充套件至20位即可)。
  2. 將步驟一得到的20位,按照高十位和低十位進行分割。
  3. 將步驟二的高十位擴充套件至2個位元組,再加上D800(十六進位制),得到高位代理或前導代理。取值範圍是D800 - 0xDBFF。
  4. 將步驟二的低十位擴充套件至2個位元組,再加上DC00(十六進位制),得到低位代理或後尾代理。取值範圍是DC00 - 0xDFFF。

Unicode轉UTF-16規則流程圖如下:

可能是最詳細的字元編碼詳解

按照這個規則,我們計算字元的UTF-16編碼,我們知道其碼點為1016E,減去10000得到016E,擴充套件至0016E,進行分割,得到高十位為00 0000 0000,十六進位制為0000,加上D800為D800;得到低十位為01 0110 1110,十六進位制為016E,加上DC00為DD6E;綜合得到D8 00 DD 6E。即UTF-16編碼為D8 00 DD 6E(也可為D8 0 DD 6E)。

而對於UTF-32是使用4個位元組表示,也採用BOM機制,可以類比UTF-16,這裡不再額外介紹。

四、字元編碼區別

1. UCS-2 與 UTF-16區別

從上面的分析知道,UCS-2採用的兩個位元組進行編碼。在0000到FFFF的碼位範圍內,它和UTF-16基本一致,為什麼說基本一致,因為在UTF-16中從U+D800到U+DFFF的碼位不對應於任何字元,而在使用UCS-2的時代,U+D800到U+DFFF內的值被佔用。

UCS-2只能表示BMP內的碼點(只採用2個位元組),而UTF-16可以表示輔助平面內的碼點(採用4個位元組)。

我們可以抽象的認為UTF-16可看成是UCS-2的父集。在沒有輔助平面字元(surrogate code points)前,UTF-16與UCS-2所指的意思基本一致。但當引入輔助平面字元後,想要表示輔助平面字元時,就只能用UTF-16編碼了。

2.UCS -4與 UTF-16的區別

在BMP上,UTF-16採用2個位元組表示,而在輔助平面上,UTF-16採用的是4個位元組表示。對於UCS-4,不管在哪個平面都採用的是四個位元組表示。

3 為什麼UTF-8編碼不需要BOM機制

因為在UTF-8編碼中,其自身已經帶了控制資訊,如1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx,其中1110就起到了控制作用,所以不需要額外的BOM機制。

五、談談emoji

終於可以說說emoji了。Emoji早先由日本企業發明,日文將其稱為“絵文字”。後來隨著智慧手機的推廣,全世界都在用,於是被Unicode收編了。截至Unicode 10.0,共有1144個emoji被收錄。

?與

這個小標題可能會有亂碼或者顯示一半的情況,這是有意為之。大家不妨來看一張圖:

可能是最詳細的字元編碼詳解
圖中是同一段資料在我司內部通訊工具和Mac版微信中的表現。

拋去格式不太友好之外,有一處特別的地方是emoji的兩片飄葉顯示不對。實際上除了微信,這個emoji在幾乎哪都不顯示。這就奇怪了,我特地把兩個軟體中的飄葉都複製出來,結果發現他們壓根連碼點都不同:

  • ?: U+1F343
  • : U+E447

emmmm...同一個emoji怎麼會變碼點呢?emoji的特殊規則?沒聽說啊。。

幾經搜尋,我終於找到了原因。原來iPhone中使用的emoji最早是軟銀的一套編碼,飄葉在這套編碼中的碼點正是U+E447。這個碼點位於使用者可自定義的私有碼位區域,因此現今以Unicode的角度來看這個字元,它是沒有確切的字形的。現在絕大部分軟體也都不再支援這套老emoji了。這個emoji來自於一位微信使用者的暱稱,恰好微信還支援這套老emoji,於是能夠顯示出來。

可能是最詳細的字元編碼詳解
從圖中可以看到,飄葉在這套編碼裡恰好是U+E447。

單色與彩色

如果emoji就這點玩法的話,也不至於寫這麼一章了。實際上emoji比我最初的認識還要複雜很多。Unicode的emoji除了可以使用大家平時見到的彩色來展示,還可以用單色來展示,以適應一些非常簡單的顯示裝置。

怎麼做呢?規則就是在普通的emoji碼點之後,緊跟一個用來表示顏色版本的“變幻符”,這個變幻符有兩個取值:VS15(U+FE0E)和VS16(U+FE0F)。其中VS15表示強制使用單色版,而VS16則表示強制使用彩色版。如果沒有變幻符呢,每個emoji可以使用自己預設的展示。

舉個例子來說,U+26A0這個emoji可以有兩種樣子:

  • ⚠︎(U+26A0 + U+FE0E)
  • ⚠️(U+26A0 + U+FE0F)

各位還記得Unicode當中的“組合字”麼,異曲同工。

膚色

想必大家已經見過了,現在每個人類emoji都有很多種膚色版本。Unicode從8.0開始,為所有展示人或人體部位的emoji都增加了膚色控制。

在普通人物的emoji碼點之後如果跟上一個膚色碼點,那麼這個emoji就會採用相應的膚色。舉個例子:

?? = ?(U+1F443) + ?(U+1F3FF)

實際上不管emoji中的人是否露出皮膚,都可以這樣組合,畫emoji的人可以去抉擇如何去表現。無論如何,現在用emoji已經可以這麼幹了:

?????

emoji裡的全家福

上面的emoji組合,都是同一個emoji內部的事情,然而實際上多個獨立的emoji也可以進行組合。

在Unicode中,存在一個特殊的碼點,被稱為零寬連線符(ZWJ),其碼點為U+200D。這個零寬連線符在平時是不會顯示出來的,不然也不會叫零寬連線符了。之前網上有人貼出“空字串”幾百個位元組,就是用它了。

零寬連線符在emoji中的作用,就是可以i將多個emoji組合成一個更大的emoji。大家平時在網上看到的全家福emoji,正是這樣做出來的:

?‍❤️‍?‍? = ?(U+1F468) + ZWJ(U+200D) + ❤(U+2764) + ZWJ(U+200D) + VS16(U+FE0F) + ZWJ(U+200D) +?(U+1F48B)+ ZWJ(U+200D) +?(U+1F468)

可以說是很解耦了。。

當然也不是所有的軟體現在都支援ZWJ這種玩法,對於不支援的軟體,全家福就會被打散。

Unicode && JavaScript

那麼作為一個前端,JavaScript對Unicode的支援是怎樣的呢?事實是,JavaScript的字串使用UTF-16來儲存字元。在我印象裡,有這麼幾個函式與Unicode關係最大:

String.prototype.charAt() //返回指定位置的字元 String.prototype.charCodeAt() //返回指定位置的UTF-16編碼 String.prototype.codePointAt() //返回指定位置的Unicode碼點 String.length //返回物件中字串所佔的UTF-16單元數量

幹說沒用,來考慮一下下面這段程式碼:

// U+1F4A9
const str = '?';
console.log(str.charCodeAt(0));
console.log(str.charAt(0));
console.log(str.codePointAt(0));
複製程式碼

這是一坨屎無誤,第一行log的情況是:

console.log(str.charCodeAt(0));
// 55357
// 0xD83D
複製程式碼

嗯?好像和碼點對不上?答案是這樣的

'?' === '\u{1F4A9}'
'?' === '\uD83D\uDCA9'
複製程式碼

哦,原來這坨屎已經超出了UTF-16最初支援的2位元組表示,因此需要使用代理對來表示,這樣一來就不是直接給出碼點了。

這樣的話,第二行log輸出也就很好理解了:

console.log(str.charAt(0));
// ‘?'
複製程式碼

charAt比較簡單,只是單純地將UTF-16字串按下標返回對應的字元,這裡會打出一個代理對的一半,所以顯示不出來。也就是說chartAt是不會完整吐出一個需要代理對的字來的。

第三個log給出了你可能最想要的:

console.log(str.codePointAt(0));
// 128169
// 0x1F4A9
複製程式碼

這是真正的Unicode碼點無誤了。那麼console.log(str.length);的話,會輸出什麼呢?由於這個屬性返回的是UTF-16單元的數量,而這坨屎需要2個UTF-16單元,因此其輸出會是2。這可不是位元組數哦。

那麼,既然str.codePointAt(0)能夠返回出整坨屎的Unicode碼點,那str.codePointAt(1)會返回什麼呢?ES標準說了,如果這個下標不是代理對的開頭,那麼只返回指向的UTF-16單元,也就是說:

console.log(str.codePointAt(1));
// 56489
// 0xDCA9
複製程式碼

最後說一下for迴圈的區別:

  • for(...i++;...)...str[i]: 按charCodeAt()進行迴圈
  • for…in: 按charCodeAt()進行迴圈
  • for…of: 按codePointAt()進行迴圈

所以只有for...of是真正理解Unicode的。大家用for來迴圈的時候,可要小心了,否則一不小心就會把代理對給拆開。

細節是魔鬼

計算機的歷史只有短短不到100年的時間,而網際網路則只有不到30年。因為歷史很短,很多時候我們會產生一種假象,那就是計算機的歷史好像是筆直的,一切設計都很合理、恰到好處,只需理解一下高抽象層次的概念和原理即可。而事實上則恰好相反,計算機世界的歷史崎嶇不平,充滿了錯誤和因為錯誤而顛簸的設計,這裡面隱藏了大量的細節。有時我們假裝自己已經對程式瞭如指掌,“啊,編碼嘛,不就是對映一下嘛;哦,HTTP協議嘛,很簡單啊,就是個抽象層而已啊”,假裝自己是高階程式設計師,因此好像可以忽略這些細節。實際呢,到處都是坑!

細節就是魔鬼,即使在看起來並不複雜的字元編碼上也是如此。如果你忽視了這些,就只有使用者來替你承擔了。

相關文章