阿呆學Unicode之編碼

veldts發表於2012-02-26

原文:Unicode for dummies — Encoding。天天翻牆,身心健康!

這個故事說的是編碼(encoding)和解碼(decoding),中間穿插一小段Unicode介紹。

故事是這麼開始的:月黑風高夜,我們的主人公陷入沉思。他喃喃自語著:“編碼,編碼,什麼是編碼?”

什麼是編碼?

基本概念很簡單。首先,我們從一段資訊即訊息說起,訊息以人類可以理解、易懂的表示存在。我打算將這種表示稱為“明文”(plain text)。對於說英語的人,紙張上列印的或螢幕上顯示的英文單詞都算作明文。

其次,(原因稍候再述)我們需要能將明文表示的訊息轉成另外某種表示(不妨稱之為“編碼文字”,encoded text),我們還需要能將編碼文字轉回成明文。從明文到編碼文字的轉換稱為“編碼”,從編碼文字又轉回成明文則為“解碼”。

enter image description here

這個過程有三點很重要。

第一點是在編碼或解碼過程中不得丟失任何資訊。我們必須能做到來回傳遞訊息,從明文到編碼文字,然後又從編碼文字回到明文,取得的明文跟我們一開始擁有的完全一樣。舉例來說,這正是為什麼我們無法用一種自然語言(俄語、漢語、法語、納瓦霍語等)來編碼另一種自然語言(英語、印地語、斯瓦西里語等)。自然語言之間的對映關係太過鬆散,無法保證一條資訊能在來回的轉換中不失真。

要求來回轉換不失真,也就意味著明文和編碼文字之間的對映必須又緊密又精確。這就引出了第二點

為確保明文和編碼文字之間的對映非常緊密,也即,為了能非常精確地規定編碼和解碼過程是怎麼回事,我們必須非常精確地規定明文的表示是什麼樣的。

例如,假定我們的明文是這樣的:英美字母表的26個大寫字母,加上空格,及3個標點符號:句點(句號)、問號和短劃線(連字元)。這就形成了30個字元的明文字母表。需要數字時,我們可以直接拼寫出來,比如:six thousand seven hundred forty-three(六千七百四十三)。

另一方面,我們可能希望明文是這樣的:26個大寫字母,26個小寫字母,10個數字,空格符,及十幾種標點符號,包括句點、逗號、雙引號、左括號、右括號等等。於是我們又得到一張75個字元的明文字母表。

一旦精確地規定了訊息的明文表示是什麼樣的,比如上面的30個字元的字母表,也許是75個字元的字母表中有限的字元序列,我們就可以設計一套系統(編碼,code),能對用這張字母表寫成的明文訊息進行可靠的編碼和解碼。這類系統最簡單的一種是明文字母表裡的每個字元在編碼文字中有一個且只有一個對應的表示。舉個大家都熟悉的摩爾斯電碼的例子,其中明文“SOS”對應的編碼文字為... --- ...。

當然,在現實生活中,明文字母表中字元的選擇受到編碼文字上技術限制的影響。假定儲存編碼訊息有如下幾種技術可供選擇:一種技術支援256個字元的編碼字母表,另一種技術只支援128個編碼字元,第三種技術只支援64個編碼字元。如果我們清楚自己可以使用支援更大編碼文字字母表的技術,我們自然而然就能將明文字母表擴充套件得比以往大得多。

反之亦然。如果確定明文字母表一定要非常大,我們就會明白必須找到或設計一種能儲存大量編碼字元的技術。

這就引出了下面的話題,Unicode。

Unicode

Unicode的設計初衷是成為一套系統,可以儲存現存所有人類語言的所有明文字元的編碼表示。英語、法語、西班牙語、希臘語、阿拉伯語、印地語、漢語和亞述語(楔形文字)等。

這些字元數量龐大。

因此,Unicode計劃的首要任務就是把所有這些字元羅列出來,數個數。這就是Unicode的前半部分,通用字符集(Universal Character Set)。(真正談及Unicode時,不要把明文字元叫做“字元”,稱之為“碼點”(code points)。)

完成前面的工作後,你還得想出一種技術,以儲存全部對應的編碼文字字元。(用Unicode的話來說,編碼文字字元被稱作“碼值”(code values)。)

實際上,Unicode定義了不止一種方法,將碼點對映到碼值。這些方法都有自己的名字。其中部分名字以“UTF”開頭,其他以“UCS”開頭:UTF-8、UTF-16、UTF-32、UCS-2、UCS-4等等。命名規範是“UTF-<碼值的位元數>”和“UCS-<碼值的位元組數>”。有些方法(如UCS-4和UTF-32)功能相若。參看維基百科Unicode相關頁面

關於這些方法最重要的一點是,有些方法是定長編碼,有些則是變長編碼。基本思想是定長編碼都很長,如UCS-4和UTF-32都有4位元組(32位元)長,長到足以hold住未來可期的最大碼值。

相比而言,變長編碼則設計成簡短但可擴充套件。例如,UTF-8可以使用少至8位元(1位元組)來儲存Latin和ASCII碼點。不過,它還有一種“接下一個位元組”機制,以便在必要時(有可能用來表示中文字元)可以使用2個位元組甚或4個位元組。對於西方國家的程式設計師,這就意味著UTF-8既高效又靈活,UTF-8之所以成為交換Unicode文字事實上的標準編碼,原因正在於此。

總之,不存在所謂單一的Unicode編碼系統或方法,而是有好幾種編碼方法;打算跟別人交換文字時,你需要明確指定自己採用了哪種編碼方法。

比如,是不是這樣:

enter image description here

或者這樣:

enter image description here

或者其他編碼方法。

這又回到我前面提過的話題。

為什麼要用Unicode來編碼?

本文開頭提到:

首先,我們從一條資訊即訊息說起,訊息以人類可以理解、易懂的表示存在。

其次,(原因稍候再述)我們需要能將明文表示的訊息轉成另外某種表示(不妨稱之為“編碼文字”),我們還需要能將編碼文字轉回成明文。從明文到編碼文字的轉換稱為“編碼”,從編碼文字又轉回成明文則為“解碼”。

好的。現在是時候探討這些原因了。為什麼我們會想著將明文表示的訊息轉換成另一種表示呢?

一個原因當然是我們想要保密。我們想通過加密(encrypting)和解密(decrypting)來隱藏訊息的明文,基本上編碼和解碼的演算法要求保密和不擴散。

不過,這是個截然不同的主題。眼下,我們對保密不感興趣;身為Python程式設計師,我們感興趣的是Unicode。因此:

身為Python程式設計師,為什麼我會需要能將明文訊息轉換成某種編碼表示……比如UTF-8之類的Unicode表示?

假定你正舒舒服服地坐在電腦前,用你最喜歡的文字編輯器,用Python(特別是用Python 3+)編寫標準的Hello World程式。你的整個程式只有下面這一行程式碼。

           print("Hello, world!")

這裡的“Hello, world!”就是明文。你在螢幕上看得見它。你可以閱讀,明白它的含義。它就是一字串,你可以對它執行字串型別的標準操作,比如擷取一個子串(切片)。

不過現在假定你準備將這個字串“Hello, world!”放到一個檔案中,並將該檔案儲存到硬碟上。也許你打算將這個檔案傳送給朋友。

這意味著你必須將那可憐瘦小的字串逐出Python程式中溫暖、和睦、安全的家,那裡它就以明文字元的形式存在。你必須把它推入檔案系統那陰冷、沒有人情味的外部世界。那裡它不會以字元的形式存在,只有1和0,一堆雜亂的點點滴滴,充電和未充電的微粒。而這意味著原本幸福瘦小的明文字串必須以1和0的某種特定配置來表示,這樣一來,有人想要獲取那組1和0,將它轉換回可讀的明文,完全可以辦到。

將明文轉換成1和0的特定配置的過程就是編碼的過程。為了將字串寫入檔案中,你必須採用某種編碼系統(比如UTF-8)對字串進行編碼。要從檔案中取回字串,你又必須讀入檔案,對這組1和0進行解碼,轉回成明文。

從檔案讀寫字串時需要對它進行編碼和解碼並不是新鮮事,這不是Python 3對Unicode的新支援引入的額外負擔。其實你一直在做這件事。不過就是不怎麼明顯而已。較早版本的Python採用ASCII編碼方案。因為在那些久遠的年代,也只有ASCII這種編碼方案,你不需要指定自己想要讀寫ASCII編碼的檔案。Python直接假定檔案預設就採用ASCII,自動進行編碼和介面。但是,無論你是否意識到這點,無論何時,只要你的程式從檔案讀寫字串,Python都會在幕後忙碌,為你完成編碼和解碼工作。

綜上,這就是為什麼身為Python程式設計師的你需要能將文字編碼成UTF-8(或其他某種編碼:UTF-16、ASCII等),或將UTF-8轉回到文字。你需要將字串編碼成一組1和0,這樣才能將這些1和0放進檔案,然後將檔案傳送給其他人。

什麼是明文?

前面提到編碼/解碼過程有三點需要注意,我探討了前面兩點。下面是第三點

明文和編碼文字之間的差別是相對的,且依賴語境。

作為程式設計師,我們會把書面文字當作明文。但也要從其他角度分析事物。例如,我們可以把口頭文字看作明文,而書面文字看作編碼文字。從這個角度來看,書面語是編碼過的口語。世界上有很多種不同的編碼,將口語編碼成書面語。舉例來說,有古埃及象形文字、瑪雅象形文字、拉丁字母、希臘字母、阿拉伯文、中國的表意文字,還有非常平滑的天城文(Devanagari),即使是簡寫體也呈鋒利尖銳的楔形。這些都是口語詞的書寫編碼。正如英國政治哲學家托馬斯·霍布斯(Thomas Hobbes)所言,它們都是“我們也許賴以記住我們思想的記號”。

這又提醒我們,在不同的語境中,即使言語本身——語言——也可能被看作是一種編碼形式。在大部分早期現代哲學中(想想霍布斯和洛克),言語(或語言)基本上被認為是思想和理念的編碼。當我將我的想法編碼成語言說出口並對你說的時候,溝通就發生了。你聽到我言語的聲音,將其解碼成想法。當我經由語言成功地將自己心裡的想法傳遞到你心裡的時候,我們的溝通也就成功了。由於我的言語,你心裡的想法跟我心裡的達成一致時,你便理解了我。(參看Ian Hacking的Why Does Language Matter to Philosophy?

最後,注意在其他語境中,“明文”甚至不是文字。明文是聲波的(比如音樂),可以編碼成mp3檔案。明文是影像的,可以編碼為gif、png或jpg檔案。明文是電影的,可以編碼成wmv檔案。不一而足。

編碼和解碼無所不在。


備註

首先,推薦Eli Bendersky的最新文章The bytes/str dichotomy in Python 3,正是這篇文章促使我把上面的想法寫下來。我特別喜歡下面這一段:

這個問題要這麼來看:字串是文字的抽象表示。字串由字元組成,字元則是與任何特定二進位制表示無關的抽象實體。在操作字串時,我們生活在幸福的無知之中。我們可以對字串進行分割和分片,可以拼接和搜尋字串。我們並不關心它們內部是怎麼表示的,字串裡的每個字元要用幾個位元組儲存。只有在將字串編碼成位元組包(例如,為了在通道上傳送它們)或從位元組包解碼字串(反向操作)時,我們才會開始關注這點。

其次,極力推薦Charles Petzold的神作《編碼:隱匿在計算機軟硬體背後的語言》(Code: The Hidden Language of Computer Hardware and Software)。

最後,個人認為Stephen Pincode的Codebreaker: The History of Secret Communications一書也不錯。這本書會告訴你其他許多軼聞,二戰期間著名的納瓦霍密語者(codetalker)能夠談論潛水艇和俯衝轟炸機……儘管事實上納瓦霍語裡沒有“潛水艇”或“俯衝轟炸機”對應的詞。


引申閱讀

  1. Python 3的bytes/str之別(原文:The bytes/str dichotomy in Python 3)

相關文章