天天看點

字元編碼掠影:現代編碼模型

字元編碼,在計算機導論中經常作為開門的前幾個話題來講,然而很多cs教材對這個話題基本都是走馬觀花地幾頁帶過。導緻了許多人對如此重要且基本的概念認識模糊不清。直到在實際程式設計中,尤其是遇到多語言、國際化的問題,被虐的死去活來之後才痛下決心去重新鑽研。諸如此類極其基礎卻又容易被人忽視的的知識點還有:大小端表示,浮點數細節,正規表達式,日期時間處理等。本文是系列的第一篇,旨在闡明字元編碼這個大坑中許多糾纏不清的概念。

現代編碼模型自底向上分為五個層次:

抽象字元表 acr (abstract character repertoire)

編碼字元集 ccs (coded character set)

字元編碼表 cef (character encoding form)

字元編碼方案 ces (character encoding schema)

傳輸編碼文法 tes (transfer encoding syntax)

<a href="https://zh.wikipedia.org/wiki/%e5%ad%97%e7%ac%a6%e7%bc%96%e7%a0%81#.e7.8e.b0.e4.bb.a3.e7.bc.96.e7.a0.81.e6.a8.a1.e5.9e.8b">現代編碼模型-wiki</a>

<a href="http://unicode.org/glossary/">unicode術語表</a>

抽象字元集是現代編碼模型的最底層,它是一個集合,通過枚舉指明了所屬的所有抽象字元。但是要了解抽象字元集是什麼,我們首先需要了解什麼是字元與抽象字元

字元是指字母、數字、标點、表意文字(如漢字)、符号、或者其他文本形式的書寫“原子”。

例: <code>a</code>,<code>啊</code>,<code>あ</code>,<code>α</code>,<code>Д</code>等,都是抽象的字元。

抽象字元就是抽象的字元。像<code>a</code>這樣的字元是有形的,但在計算機中,有許多的字元是空白的,甚至是不可列印的。比如ascii字元集中的null,就是一個抽象字元。

注意<code>\x00</code>,<code>\000</code>,<code>null</code>,<code>0</code> 這些寫法都隻是這個抽象字元的某種表現形式,而不是這個抽象字元本身。

抽象字元集顧名思義,指的是抽象字元的集合。

已經有了很多标準的字元集定義: character sets

比如us-ascii, ucs(unicode), gbk這些我們耳熟能詳的名字,都是(或者至少是)抽象字元集。

us-ascii定義了128個抽象字元的集合。gbk挑選了兩萬多個中日韓漢字和其他一些字元組成字元集,而ucs則嘗試去容納一切的抽象字元。它們都是抽象字元集。

抽象字元 英文字母<code>a</code>同時屬于us-ascii, ucs, gbk這三個字元集。

抽象字元 中文文字<code>蛤</code>不屬于us-ascii,屬于gbk字元集,也屬于ucs字元集。

抽象文字 emoji <code></code>不屬于us-ascii與gbk字元集,但屬于ucs字元集。

集合的一個重要特性,就是無序性。

集合中的元素都是無序的,是以抽象字元集中的字元都是無序的。

抽象字元集與python中的set的概念類似:

例如:我可以自己定義一個字元的集合,叫這個集合為haha字元集。

<code>haha_acr = { 'a', '吼', 'あ', ' α', 'Д' }</code>

大家覺得抽象字元集這個名字太啰嗦,是以有時候直接叫它字元集。

最後需要注意一點的是,抽象字元集也是有開放與封閉之分的。

ascii抽象字元集定義了128個抽象字元,再也不會增加。這是一個封閉字元集。

unicode嘗試收納所有的字元,一直在不斷地擴張之中。最近(2016.06)unicode 9.0.0已經收納了128,237個字元,并且未來仍然會繼續增長,這是一個開放的字元集。

編碼字元集是現代編碼體系的第二層。

編碼字元集是一個每個所屬字元都配置設定了碼位的抽象字元集。

編碼字元集(ccs)也經常簡單叫做字元集(character set)。這樣的叫法經常會将抽象字元集acr與編碼字元集ccs搞混。不過大多時候人們也不在乎這種事情。

抽象字元集是抽象字元的集合,而集合是無序的。

無序的抽象字元集并沒有什麼卵用,因為我們隻能判斷某個字元是否屬于某個字元集,卻無法友善地引用,指稱這個集合中的某個特定元素。

以下兩個表述指稱了同一個字元,但哪一種更友善呢?

ascii(抽象)字元集中的那個代表什麼都沒有的通常表示為null的抽象字元

ascii(編碼)字元集中的0号字元

為了更好的描述,操作字元,我們可以為抽象字元集中的每個字元關聯一個數字編号,這個數字編号稱之為碼位(code point)。

通常根據習慣,我們為字元配置設定的碼位通常都是非負整數,習慣上用十六進制表示。且一個編碼字元集中字元與碼位的映射是一一映射。

舉個例子,為haha抽象字元集進行編碼,就可以得到haha編碼字元集。

<code>haha_ccs = { 'a' : 0x0, '吼':0x1 , 'あ':0x2 , ' α':0x3 , 'Д':0x4 }</code>

字元<code>吼</code>與碼位<code>0x1</code>關聯,這時候,在haha編碼字元集中,<code>吼</code>就不再是一個單純的抽象字元了,而是一個編碼字元(coded chacter),且擁有碼位 0x1。

如果說抽象字元集是一個set,那麼編碼字元集就可以類比為一個dict。

<code>ccs = { k:i for i, k in enumerate(acr)}</code>

它的key是字元,而value則是碼位。至于碼位具體是怎樣配置設定的,這個規律就不好說了。比如為什麼我想給haha_ccs的<code>吼</code>字元配置設定碼位<code>0x1</code>而不是<code>0x23333</code>呢?因為這樣能續一秒,反映了ccs設計者的主觀趣味。

編碼字元集有許許多多,但最出名的應該就是us-ascii和ucs了。ascii因為太有名了,是以就不說了。

最常見的編碼字元集就是統一字元集 ucs

ucs. acronym for universal character set, which is specified by international standard iso/iec 10646, which is equivalent in repertoire to the unicode standard.

ucs就是統一字元集,就是由 iso/iec 10646所定義的編碼字元集。通常說的“unicode字元集”指的就是它。不過需要辨明的一點是,“unicode”這個詞本身指的是一系列用于計算機表示所有語言字元的标準。

基本上所有能在其他字元集中遇到的符号,都可以在ucs中找到,而一些新的不屬于任何傳統字元集的字元,例如emoji,也會收錄于ucs中。這也是ucs地位超然的原因。

舉個例子,ucs中碼位為0x4e00~0x9fff的碼位,就用于表示“中日韓統一表意文字”

字元編碼掠影:現代編碼模型

大家喜聞樂見的emoji表情則位于更高的碼位,例如“哭笑”在ucs中的碼位就是0x1f602。

(如果這個站點不支援emoji,你就看不到這個字元了,上面那個是圖檔…)

關于ccs,這些介紹大抵足夠了。

不過還有一個細節需要注意。按照目前最新unicode 9.0.0的标準,ucs理論上收錄了128,237個字元,也就是0x1f4ed個。不過如果進行一些嘗試會發現,實際能用的最大的碼位點在0x1f6d0 ,也就是128,720,竟然超過了收錄的字元數,這又是為什麼呢?

字元編碼掠影:現代編碼模型

碼位是非負整數沒錯,但這不代表它一定是連續配置設定的。

出現這種情況隻有一個原因,那就是ucs的碼位配置設定不是連續的,中間有一段空洞,即存在一段碼位,沒有配置設定對應的字元。

實際上,ucs實際配置設定的碼位是 0x0000~0x0xd7ff 與 0xe000~0x10ffff 這兩段。中間0xd800~0xdfff這2048個碼位留作它用,并不對應實際的字元。如果直接嘗試去輸出這個碼位段的'字元',結果會告訴你這是個非法字元。例如在python2中嘗試列印碼位0xdddd的字元:

0x0000~0xd7ff | 0xe000~0x10ffff 稱為unicode标量值(unicode scala value)

0xd800~0xdbff 稱為high-surrogate

0xdc00~0xdfff 稱為low-surrogate

unicode标量值就是實際存在對應字元的碼位。

為什麼中間一端的碼位會留白,則是為了友善下一個層次的字元編碼表cef的utf-16而處理的。

除了ascii與ucs,世界上還有許許多多的字元集。

在us-ascii誕生與unicode誕生之間,很多英語之外的字元無法在計算機中表示。

大家八仙過海各顯神通,定義了許許多多其他的字元集。

字元編碼掠影:現代編碼模型

例如gbk字元集,以及其近似實作 code page 936。

這些字元集中的字元,最後都彙入了unicode中。

字元編碼表是現代編碼模型的第三層。

現在我們擁有一個編碼字元集了,let's say: ucs。

這個字元集中的每個字元都有一個非負整數位位與之一一對應。

看上去很好,既然計算機可以存儲整數,而現在字元已經能表示為整數,我們是不是可以說,用計算機存儲字元的問題已經得到了解決呢?

慢!還有一個問題沒有解決。

在講抽象字元集acr的時候曾經提起,ucs是一個開放字元集,未來可能有更多的符号加入到這個字元集中來。也就是說ucs需要的碼位,理論上是無限的。

但計算機整形能表示的整數範圍是有限的。譬如,一個位元組的無符号單位元組整形(unsigned char, uint8)能夠表示的碼位隻有0~0xff,共256個;一個無符号短整形(unsigned short, uint16)的可用碼位隻有0~0xffff,共65536個;而一個标準整形(unsigned int, uint32)能表示的碼位隻有0~0xffffffff,共4294967296個。

雖然就目前來看,ucs收錄的符号總共也就十多萬個,用一個uint可以表示幾十億個字元呢。但誰知道哪天制定unicode标準的同志們不會玩心大發造幾十億個emoji加入ucs中。是以說到底,一對有限與無限的沖突,必須通過一種方式進行調和。這個解決方案,就是字元編碼表(character encoding form)。

字元編碼表将碼位(code point)映射為碼元序列(code unit sequences)。

對于unicode而言,字元編碼表将unicode标量值(unicode scalar value)一一映射為碼元序列(code unit sequences)。

碼元是能用于處理或交換編碼文本的最小比特組合。通常計算機處理字元的碼元為一位元組,即8bit。同時因為計算機中char其實是一種整形,而整形的計算往往以計算機的字長作為一個基礎單元,通常來講,也就是4位元組。

unicode定義了三種不同的cef,分别采用了1位元組,2位元組,4位元組的碼元,正好對應了計算機中最常見的三種整形長度:

在unicode中,指定了三種标準的字元編碼表,utf-8, utf-16, utf-32。分别将unicode标量值映射為比特數為8、16、32的碼元的序列。

utf-8的碼元為uint8, utf-16的碼元為uint16, utf-32的碼元為uint32。

當然也有一些非标準的cef,如ucs-2,ucs-4,在此不多介紹。

需要注意一點的是,cef将碼位映射為碼元序列。這個映射必須是一一映射(雙射)。

因為當使用cef進行編碼(encode)時,是将碼位映射為碼元序列。

而當使用cef進行解碼(decode)時,是将碼元序列還原為碼位。

為了保證兩個過程都不出現歧義,必須保證cef是一個雙射。

知道了字元編碼表cef是什麼還不夠,我們還需要知道它是怎麼做的。

即:如何将一個無限大的整數,一一映射為指定字寬的碼元序列。

這個問題可以通過變長編碼來解決:無論是utf-8還是utf-16,本質思想都是通過預留标記位來訓示碼元序列的長度,進而實作變長編碼。

各個cef的細節我建議參看維基百科

<a href="https://zh.wikipedia.org/wiki/utf-8">utf-8</a>

<a href="https://zh.wikipedia.org/wiki/utf-16">utf-16</a>

<a href="https://zh.wikipedia.org/wiki/utf-32">utf-32</a>

寫的相當清楚,我就沒必要在此再寫一遍了。

舉個例子:

字元編碼掠影:現代編碼模型
字元編碼掠影:現代編碼模型

## 字元編碼方案 ces (character encoding schema)

字元編碼方案是現代編碼模型的第四層。

簡單說,字元編碼方案 ces 等于 字元編碼表cef 加上位元組序列化的方案。

通過字元編碼表cef,我們已經可以将字元轉為碼元序列。無論是哪種utf-x的碼元,都可以找到計算機中與之對應的整形存放。那麼現在我們能說存儲處理交換字元這個問題解決了嗎?

還不行。

假設一個字元按照utf16拆成了若幹個碼元組成的碼元序列,因為每個碼元都是一個unsigned short,實際上是兩個位元組。是以将碼元序列化為位元組序列的時候,就會遇到一些問題。

大小端序問題:每個碼元究竟是高位位元組在前還是低位位元組在前呢?

位元組序标記問題:另一個程式如何知道當文本是什麼端序的呢?

這些都是cef需要操心的問題。

對于網絡交換和本地處理,大小端序各有優劣。這個問題不屬于本文範疇。

位元組序标記bom (byte order mark),則是放置于編碼位元組序列開始處的一段特殊位元組序列,用于表示文本序列的大小端序。

對于這兩個問題的不同答案,在3種cef:utf-8,utf-16,utf-32上。

unicode實際上定義了 7種 字元編碼方案ces:

utf-8

utf-16le

utf-16be

utf-16

utf-32le

utf-32be

utf-32

其中utf-8因為已經采用位元組作為碼元了,是以實際上不存在位元組序的問題。其他兩種ces嘛,都有一個大端版本一個小端版本,還有一個随機應變大小端帶bom的版本。

下面給一個python編碼的小例子,将emoji:'哭笑' 轉換為各種ces。

字元編碼掠影:現代編碼模型

這裡也出現一個問題,曆史上字元編碼方案(character encoding schema)曾經就是指utf(unicode transformation formats)。是以utf-x到底是屬于字元編碼方案ces還是屬于字元編碼表cef是一個模棱兩可的問題。utf-x可以同時指代字元編碼表cef或者字元編碼方案ces。

utf-8問題還好,因為utf-8的位元組序列化方案太樸素了,以至于ces和cef都沒什麼差別。但其他兩種:utf-16,utf-32,就比較棘手了。當我們說utf-16時,既可以指代utf-16字元編碼表,又可以指代utf-16字元編碼方案。是以當有人說“這個字元串是utf-16編碼的”時,鬼知道他到底說的到底是一個(utf-16 encoding form的)碼元序列還是(utf-16 encoding schema 的)位元組流。

簡單的說,字元編碼表cef和字元編碼方案ces差別如下:

c ∈ ccs ---cef--&gt; code unit sequence

c ∈ ccs ---ces--&gt; byte sequence

字元編碼表cef将碼位映射為碼元序列,而字元編碼方案ces将碼位序列化為位元組流。

我們通常所說的動詞編碼(encode)就是指使用ces,将ccs中字元組成的字元串轉變為位元組序列。

而解碼(decode)就是反過來,将 編碼位元組序列 通過ces的一一映射還原為ccs中字元的序列。

除了unicode标準定義的七中ces,還有兩種ces: ucs-2,ucs-4 。嚴格來說,ucs-2和ucs-4屬于字元編碼表cef的層次,不過鑒于其樸素的序列化方案,也可以了解為ces。這兩種ces的特點是采用定長編碼,比如ucs-2直接把碼位序列化為unsigned short。之前一直很流行,但當ucs中字元越來越多,超過65536個之後,ucs-2就gg了。至于ucs-4,基本和utf-32差不多。雖說有生之年基本不可能看到ucs大小超出四位元組的表示範圍,但每個字元統一用4位元組來存儲這件事本身就很蠢了……。

當然除了ucs,其他字元集,例如us-ascii,gbk,也會有自己的字元編碼方案,隻不過我們很少聽說,一個很重要的原因是,這些字元集的編碼方案太簡單了,以至于ccs,cef,ces三層直接合一了。

例如us-ascii的ces,因為ascii就128個字元,隻要直接把其碼位轉換成(char),就完成了編碼。如此簡單的編碼,直接讓ccs,cef,ces三層合一。很多其他的字元集也與之類似。

傳輸編碼文法是現代編碼模型的最頂層

通過ces,我們已經可以将一個字元表示為一個位元組序列。

但是有時候,位元組序清單示還不夠。比如在http協定中,在url裡,一些字元是不允許出現的。這時候就需要再次對位元組流進行編碼。

著名的base64編碼,就是把位元組流映射成了一個由64個安全字元組成字元集所表示的字元流。進而使位元組流能夠安全地在web中傳輸。

不過這一塊的内容已經離我們讨論的主題太遠了。