字元編碼詳解(ASCII、Unicode、UTF-32、UTF-8) - 古詩詞庫

文章推薦指數: 80 %
投票人數:10人

下圖展示了ASCII 字符集對照表,其中包括了控制字元(回車鍵、退格、換行鍵等)和可顯示字元(英文大小寫字元、阿拉伯數字和西文符號)。

這種編碼方式就 ... MdEditor 字元編碼詳解(ASCII、Unicode、UTF-32、UTF-8) 語言:CN/TW/HK 時間 2020-05-1221:50:55 Joker'sBlog 主題: ASCII碼 Unicode 接近一年沒有更新部落格了,這是2020的第一篇,源於對基礎知識的鞏固,主要會從多個維度解釋字元編碼的由來以及內部原理。

大家都知道,程式中的所有資訊都是以二進位制的形式儲存在計算機的底層,也就是說我們在程式碼中定義的一個char字元或者一個int整數都會被轉換成二進位制碼儲存起來,這個過程可以被稱為編碼,而將計算機底層的二進位制碼轉換成螢幕上有意義的字元(如“helloworld”),這個過程就稱為解碼。

在計算機中字元的編解碼就涉及到字符集(CharacterSet)這個概念,他就相當於能夠將一個字元與一個整數一一對應的一個對映表,常見的字符集有ASCII、Unicode等。

很多時候我們會將字符集的編碼與字符集混為一談,從這裡就可以看出它們並非同一個概念,字符集僅僅是一個字元的集合,而編碼卻是一個更復雜的過程。

至於為什麼會經常將這兩個概念放在一起,他們之間的聯絡是什麼,我們經常使用的UTF-8又是什麼,這就是這篇文章我要討論的話題。

ASCII編碼 歷史中的很長一段時間裡,計算機僅僅應用在歐洲的一些發達國家,因此在他們的程式中只存在他們所理解的拉丁字母(如a、b、c、d等)和阿拉伯數字,他們在編碼解碼時也只需要考慮這一種情況,就是如何將這些字元轉換成計算機所能理解的二進位制數,此時ASCII字符集應運而生,他們在編碼時只需要對照著ASCII字符集,每當在程式中遇到字元a時,就會相應的找到其中a對應的ASCII值97然後以二進位制形式存起來即可。

下圖展示了ASCII字符集對照表,其中包括了控制字元(回車鍵、退格、換行鍵等)和可顯示字元(英文大小寫字元、阿拉伯數字和西文符號)。

這種編碼方式就被稱為ASCII編碼,從字符集對照表中可以看出,ASCII字符集支援128種字元,僅使用7個bit位,也就是一個位元組的後7位就可以將它們全部表示出來,而最前面的一位統一規定為0即可(如01100001表示a)。

後來,為了能夠表示更多的歐洲國家的常用字元如法語中帶符號的字元é,又制定了ASCII額外擴充套件的版本EASCII,這裡就可以使用一個完整子節的8個bit位表示共256個字元,其中就又包括了一些衍生的拉丁字母。

非ASCII編碼 ASCII字符集沿用至今,但它最大的缺點在於只能表示基本的拉丁字母、阿拉伯數字和英式標點符號,因此只能表示現代美國英語(而且在處理英語當中的外來詞如naïve、café、élite等等單詞時,所有重音符號都不得不去掉)。

而EASCII雖然解決了部份西歐語言的顯示問題,但是當計算機傳入亞洲之後,各國的語言依然不能完整地表示出來。

在這個年代,每個國家就各自來對ASCII字符集做了拓展,最具代表性的就是國內的GB類的漢字編碼模式,這種模式規定:ASCII值小於127的字元的意義與原來ASCII集中的字元相同,但當兩個ASCII值大於127的字元連在一起時,就表示一個簡體中文的漢字,前面的一個位元組(高位元組)從0xA1拓展到0xF7,後面一個位元組(低位元組)從0xA1到0xFE,這樣就可以組合出了大約7000多個簡體漢字了。

為了在解碼時操作的統一,GB類編碼表中還也加入了數學符號、羅馬希臘的字母、日文的假名等,連在ASCII裡本來就有的數字、標點、字母都統一重新表示為了兩個位元組長的編碼,這就是我們常說的“全形”字元,而原來在127號以下的那些就叫“半形”字元了,這種編碼規則就是後來的GB2312。

“一個漢字算兩個英文字元!一個漢字算兩個英文字元……” 下圖展示了GB2312字符集中的一小部分,具體可檢視GB2312簡體中文編碼表。

這樣,我們中國就有了屬於自己的字元集了,但中國的漢字又是在是太多了,人們很快就發現GB2312字符集只能夠那點漢字明顯不夠(如中國前總理朱鎔基的“鎔”字並不在GB2312字符集中),於是專家們又繼續把GB2312沒有用到的碼位使用到其他沒有被收錄的漢字中。

後來還是不夠用,於是乾脆不再要求低位元組一定是127號之後的內碼,只要第一個位元組是大於127就固定表示這是一個漢字的開始,不管後面跟的是不是擴充套件字符集裡的內容。

結果擴充套件之後的編碼方案被稱為GBK標準,GBK包括了GB2312的所有內容,同時又增加了近20000個新的漢字(包括繁體字)和符號。

當時的各個國家都像中國這樣制定出了一套自己的編碼標準,之後當我們需要使用計算機與國際接軌時,問題出現了!國家與國家之間誰也不懂誰的編碼,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母Gimel(ג),在俄語編碼中又會代表另一個符號。

但是所有這些編碼方式中,0–127表示的符號依然都是一樣的,因為他們都相容ASCII碼,這一點,如今也是一樣。

Unicode 正如上一節中所說的,世界上各國都有不同的編碼方式,同一個二進位制數字可以被解碼成不同的符號。

因此,要想開啟一個文字檔案,就必須知道它的編碼方式,否則用錯誤的編碼方式解讀,就會出現亂碼。

為了解決這個問題,最終的集大成者Unicode字符集出現了,它將世界上所有的符號都納入其中,成功實現了每個數字代表唯一的至少在某種語言中使用的符號,目前,Unicode字符集中已經收錄超過13萬個字元(第十萬個字元在2005年獲採納)。

值得關注的是,Unicode依然相容ASCII,即0~127意義依然不變。

碼點 Unicode表示的是一個字符集,與我們通常所說的UTF-8、UTF-6等編碼方式並不相同,本節介紹的編號就相當於ASCII碼中的ASCII值,它就是Unicode字符集中唯一表示某個字元的標識,在Unicode也稱作碼點(CodePoint),如碼點U+0061,這裡的61就是97的十六進位制表示,它就表示Unicode字符集中的字元‘a‘。

碼點的表示的形式為U+[XX]XXXX,X代表一個十六制數字,一般可以有4-6位,不足4位前補0補足4位,超過則按是幾位就是幾位,具體範圍是U+0000~U+10FFFF,大概是111萬。

按Unicode官方的說法,碼點範圍就這樣了,以後也不擴充了,一百多萬足夠用了,目前也只定義了11萬多個字元左右。

整個編碼過程中碼點就作為了一箇中間的過渡層,可用下面這張圖來表示: 從這張圖可以看出,整個解碼可分為兩個過程。

首先,將程式中的字元根據字符集中的編號數字化為某個特定的數值,然後根據編號以特定的方式儲存到計算機中。

顯然,這時候我們就可以發現編號並不是最終儲存在計算機中的結果。

按照之前的理解,編碼即把一個字元編碼為一個二進位制數字儲存起來,然而這種表述並不準確,真正的編碼不止這麼簡單,這其中還涉及了每個數字用幾個位元組表示,是用定長還是變長表示等具體細節。

舉個例子,字元a的碼點為U+0061(十進位制為97),那麼這個U+0061該如何儲存,單純的表示U+0061可以直接使用7位的二進位制數1100001表示,但在GB類的編碼模式中就需要以兩個位元組儲存即0000000001100001(空位用0填充)。

Unicode編碼 Unicode字符集衍生出來的編碼方案有三種,分別是UTF-32、UTF-16和UTF-8,這使他與之前的編碼模式不同,因為ASCII、GBK等類編碼模式的字符集和編碼方式都是一一對應的,而Unicode的編碼實現卻有三種,這就是我們需要區分字符集與編碼的原因之一,因為此時Unicode並不特指UTF-8或者UTF-32。

下面,我們來看下面這張示意圖,探究各種編碼模式下,碼點是如何具體轉換成各種編碼的: 上面表中包含了四個字元的碼點,其中也展示了四個不同的碼點在UTF-32、UTF-16和UTF-8三種編碼模式下的編碼結果。

其中:碼點到UTF-32的轉換最簡單,就是在前面填充0滿4位元組即可;碼點到UTF-8的轉換,除了最小那個在數值上一樣外,其它三個完全看不出兩者的關係;碼點到UTF-16的轉換則是最不規則的,可以看出前三個字元UTF-16與碼點是完全一致的,但那個大碼點(準確地說是超過了U+FFFF的碼點)則有了很大的變化,長度變成了四位元組,值也變得很不一樣了。

這其中又涉及到編碼過程中定長與變長兩種實現方式,這裡的UTF-32就屬於定長編碼,即永遠用4位元組儲存碼點,而UTF-8、UTF-16就屬於變長儲存,UTF-8根據不同的情況使用1-4位元組,而UTF-16使用2或4位元組來儲存碼點。

定長於變長 為什麼要有定長於變長這兩種編碼形式?在中文的表達中都會有所謂的斷句問題,如果我們處理不好斷句很有可能會將意思傳遞錯誤。

如下面這句來自算命先生紙條中的內容: 大富大貴沒有災難要小心 此時,如果算命俠客這樣斷句: 大富大貴,沒有災難要小心 表示我福大命大,沒有災難,可以肆意妄為了,但是沒過多久這位俠客就去世了,算名先生絕望地說,你會錯意了,原來,其實是這樣斷句的: 大富大貴沒有,災難要小心 表示你沒有大富大貴,出門要小心,斷句就可能會出現這樣嚴重的後果。

這也是計算機在解碼時需要使用定長與變長的原因。

因為計算機底層的二進位制碼也和算命先生紙條中的內容一樣,毫無章法,我們如果想要正確理解其中的意思就要有一個約定俗成的規則。

UTF-32 在UTF-32這種定長的編碼方式下就表示每4個子節一個斷句,那麼字元A的碼點U+0041(二進位制為1000001)被UTF-32編碼後就會變成如下形式儲存在計算機中: 00000000000000000000000001000001 它會將4個位元組中空出的高位全部填充為0。

這種表示的最大缺點是佔用空間太大,因為不管都大的碼點都需要四個位元組來儲存,非常的佔空間,那麼如何突破這個瓶頸呢?變長方案應運而生。

UTF-8 UTF-8屬於變長的編碼方式,它可以由1,2,3,4四種位元組組合,使用的是高位保留的方式來區別不同變長,具體方式如下: 對於只有一個位元組的符號,位元組的第一位設為0,後面7位為這個符號的Unicode碼。

此時,對於英語字母UTF-8編碼和ASCII碼是相同的。

對於n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,後面位元組的前兩位一律設為10。

剩下的沒有提及的二進位制位,全部為這個符號的Unicode碼,如下表所示:|Unicode碼點範圍(十六進位制)|UTF-8編碼方式(二進位制)|位元組數||—————————|———————————–|——–||00000000~0000007F|0xxxxxxx|一個位元組||00000080~000007FF|110xxxxx10xxxxxx|二個子節||00000800~0000FFFF|1110xxxx10xxxxxx10xxxxxx|三個位元組||00010000~0010FFFF|11110xxx10xxxxxx10xxxxxx10xxxxxx|四個位元組|跟據上表,編碼字元時就非常簡單了,以漢字“醜”為例,它的碼點為0x4E11(0100111000010001)在上表的第三行範圍(00000800~0000FFFF)內,因此“醜”需要以三個位元組的形式編碼:這裡最高位的第一個位元組中的三個1表示該字元佔3個位元組,空出的16位x就會從“醜”的最後一個二進位制位開始,依次從後向前填入格式中,多出的位補0,這樣就得到了“醜”的UTF-8編碼是111001001011100010010001,轉換成十六進位制就是E4B891。

解碼UTF-8編碼也很簡單了,如果一個位元組的第一位是0,則這個位元組單獨就是一個字元;如果第一位是1,則連續有多少個1,就表示當前字元佔用多少個位元組,”醜”有三個1表示佔三個字元,然後取出有效位即可。

UTF-16 UTF-16使用的是是一種變長為2或4位元組編碼模式。

最初,Unicode1.0被設計為純16位編碼,擁有65,536個碼點(U+0000~U+FFFF),目的就是希望能夠表示所有現代字元,然而隨著時間推移,16位對於計算機而言顯然是不夠的,因此產生了如今的4位元組的UTF-16編碼,此時,Unicode就具有了1,114,112個程式碼點(U+10000~U+10FFFF),這就是我們之前介紹Unicode。

此時,範圍在U+0000~U+FFFF的碼點被稱了為BMP(BasicMultilingualPlane,基本多語言平面),而後來拓展的範圍U+10000~U+10FFFF稱為非BMP字元。

UTF-16就是利用BMP使用代理的方式來對字元進行編碼。

何為代理? 代理和UTF-8中的高位保留的目的一樣,就是為了能夠實現變長的編碼方式。

什麼是代理區? 代理區由兩個特殊範圍(BMP中的空閒部分)的Unicode碼點組成,總共有2048個位置,均分為高代理區(D800–DBFF)和低代理區(DC00–DFFF)兩部分,各1024,這兩個區可以組成一個二維的表格,共有65536個單元格,所以它恰好可以表示代理(增補)的16位中的所有字元。

這種從一維儲存轉換到二維儲存的方式就可以實現空間增大的效果了,UTF-16也就有了能夠額外獲得碼點的方式了。

一個高代理區(即上圖中的Lead(頭),行)的加一個低代理區(即上圖中的Trail(尾),列)的編碼組成一對代理對(SurrogatePair)。

在圖中就可以看到一些轉換的例子,如 (D800DC00)—>U+10000,左上角,第一個增補字元 (DBFFDFFF)—>U+10FFFF,右下角,最後一個增補字元 從UTF-16轉換為字元程式碼的演算法是什麼? 分成兩部分: BMP中直接對應,無須做任何轉換; 增補平面SP中,則需要做相應的計算。

其實由上圖中的表也可看出,碼點就是從上到下,從左到右排列過去的,所以只需做個簡單的除法,拿到除數和餘數即可確定行與列。

拿到一個碼點,先減去10000,再除以400(=1024)就是所在行了,餘數就是所在列了,再加上行與列所在的起始值,就得到了代理對了。

$$ C_H=(碼點–10000_{16})\div400_{16}+D800_{16} $$ $$ C_L=(碼點–10000_{16})\%400_{16}+DC00_{16} $$ 下面以前面的U+1D11E具體示例了代理對的計算: $$ 高代理=(1D11E–10000_{16})÷400_{16}+DB00=D11E÷400_{16}+D800=34+D800=D834 $$ $$ 低代理=(1D11E–10000_{16})\%400_{16}+DC00=D11E\%400_{16}+DC00=11E+DC00=DD1E $$ 所以,碼點U+1D11E對應的代理對即是D834DD1E。

本篇到此結束,大家有任何問題可直接聯絡我,也可在評論區討論。

「其他文章」 Bolt的Flutter路由管理實踐(頁面解耦,流程控制、功能拓展等) 細數2020年官方對Android的重大更新 Flutter應用適配(自適應佈局元件實踐) Linux容器化技術詳解(虛擬化、容器化、Docker) 字元編碼詳解(ASCII、Unicode、UTF-32、UTF-8) 「ASCII碼」 C#.NETORMFreeSql讀取使用US7ASCII的Oracle資料庫中文顯示亂碼問題 iOS小技能:引數名ASCII碼從小到大排序、物件陣列排序 在Linux上用ASCII藝術列印萬聖節問候語 微軟Win11正式版發現新問題:不相容登錄檔中帶有非ASCII字元的應用程式 魔眾工具箱系統v1.1.0隨機串生成器工具、日期計算器工具、ASCII碼對照表工具 原力計劃Arduino基本人機介面:點陣LED、漢字型檔、鍵盤§01漢字型檔GT32L32基本資訊1、基本介紹這顆G... AndroidAndroidStudioGradle到處JavaDocJar提示編碼GBK的字元無法對映解決辦法最近因為要把P... 終端看片指日可待——ASCII轉義序列的妙用 QPSK訊號調製之ASCII碼 資料通訊--ASCII碼通訊&16進位制通訊的區別 「Unicode」 一次性弄懂Unicode和UTF-8 Unicode視覺欺騙攻擊深度解析 深入理解“字元編碼模型” AWK加入Unicode支援,由80歲的原作者貢獻 ABAP裡檔案操作涉及到中文字符集的問題和解決方案試讀版 一個熱搜兩幅面孔,娛樂圈的“高仿熱搜”是咋騙你的? 康熙來了 CVE-2015-0057提權漏洞學習筆記 一文讀懂位元組、字元與字元編碼 聊聊JS獲取GIF總幀數



請為這篇文章評分?