小白在學習 Python 中的“字符串”這個基本概念時,問了大神這樣一個問題:既然 Python 內部的字符串都是使用 Unicode 來編碼的,那干嘛還要在存儲和傳輸的時候轉成 UTF-8 呢?這樣轉來轉去的多麻煩?
說實話,回答小白的問題,通常比回答大神的問題要難。因為他們總是問一些不著邊際的問題。好在偉大的互聯網時代,沒有什么事情不可以通過在網上搜集信息,整理分析之后給出一個相對滿意的回答的。
小白提出這個問題,主要是源于網上教程中一個錯誤的解釋:Unicode標準也在不斷發展,但最常用的是用兩個字節表示一個字符(如果要用到非常偏僻的字符,就需要4個字節)。于是,小白就認為,既然2個字節可以搞定,干嘛還搞成 UTF-8,因為那樣可能需要 3 個字節或者更多來表示一個字。這里的錯誤是什么呢?就是混淆了“標準”和“實現”。好吧,小白肯定又要暈了。
先來舉一個現實中的例子:現在我們使用的長度單位是“米”。這就是一個標準。這個標準怎么定義的呢?在國際計量學會有幾個標準“米”容器,那個東西的長度,就被定義成一個標準的“米”。但不會有誰真的跑去用那個標準容器。我們都是以那個標準容器為樣板,生產自己的尺,來測量東西。這個尺就是“米”這個標準的一種實現。我們可以做一根硬的木尺,也可以是軟的卷尺,還可以是使用激光測距的光尺。不管怎么說,只要這個尺量出來的“米”和標準容器量出來的“米”是一樣的,那就可以了。
那么 Unicode 是怎么回事呢?Unicode 做為一個國際標準,其實只是定義了每個字符對應的一個數字(實際上 Unicode 包含很多復雜的內容,這里只說最簡單的部分),比如 20031 這個數字就代表“中”這個字,25991 代表“文”這個字。但其實 Unicode 并沒有說你要怎么保存一個字。
現在大家都知道,電腦里存信息,通常使用的單位叫:字節。比如我們說一首歌下載需要 3MB 的流量,翻譯成“人話”,就是需要傳輸 3 百萬字節的信息。一個字節能表示的數字非常的少,只能表示 0 - 255 這 256 個數。那如果我們需要表示超過 255 的數字要怎么辦?那當然就是用多個字節。比如兩個字節就可以表示 0 - 65535 這 65536 個數了。那我要表示負數怎么辦?最笨的辦法就是用3個字節嘛,用一個字節來表示正負,剩下的字節來表示數字。當然小白肯定會說:正負就兩個狀態,需要一個字節,那多浪費!于是,有就了各種不使用3個字節也能表示正負數的方法。這部分內容,任何一本“計算機原理”的書里都會講到。什么符號位啊,補碼啊……
講這么一堆關于怎么表示一個數字的問題干什么呢?和這里說的 Unicode 和 UTF-8 有什么關系呢?當然有關系!這里大神要說的意思就是:同樣一個數,你可以選不同的方式來表示它(比如剛才說的用3個字節來表示正負數,或者使用符號位來表示正負數)。選用什么方案,就是對 Unicode 的一種編碼方法,也就是我們剛才說的“實現”。為什么前面提到的教程作者會說 Unicode 通常是兩個字節呢?因為在 Unicode 早期,設計 Unicode 的人學識有限。他們在考查了世界上主要的文字之后,感覺用2個字節也就是最多 65536 個字符應該夠用了。對應于一種 2 字節的編碼方案叫 UCS2。后來做著做著,發現不對了,2個字節不夠用了(現在 Unicode 包含了 12萬 8 千個字符),又擴展到4個字節(因為計算機使用二進制的原因,3個字節在讀寫的時候很沒效率,所以都是2的倍數來擴展的),對應于一種編碼方案叫 UCS4。而對于我們來說,常用的中文字都在 65535 以下,所以通常 UCS2 就能表示常用的 6000 多個漢字了。這里說的 UCS2 和 UCS4 都是 Unicode 的“實現”,或者叫編碼方案。雖然,因為 UCS2 不能覆蓋全部的 Unicode 字符集,已經被標為“廢棄”了,但不要被他們騙了,一個已經使用了很久的技術,想廢棄是非常難的。
UCS2 和 UCS4 都是固定編碼長度的方案。也就是說不管是什么字符,都使用 2 個字節或者 4 個字節來表示。對于我們來說,這并不是什么問題,但問題出在:這個世界上最流行的語言不是漢語,而是英語(中國人表示不服!漢語是世界上使用人數最多的語言)。雖然把每個英語字母都使用兩個字節來表示也不是什么大的問題,可是,在遙遠的計算機啟蒙時代,無論是硬盤的大小還是網絡的速度,都非常的有限,如果不對傳輸的內容進行壓縮和限制,那成本是非常高的。于是就有了 UTF-8。
和 UCS2 這樣的固定編碼長度的方案不同的的是,UTF-8 并不是固定長度的。最短的 UTF-8 字符只需要使用 1 個字節。最長是 5 個字節。最長那 5 個字節能表示的數字,相當于用 4 個連續字節用來表示正負數,里面正數的個數,大概有20多億,具體是多少,其實也不重要??雌饋?,多用了一個字節,還沒有原來表示的數字多,好像很浪費,但這里的目的是:最常用的英文字母,使用最少的字節,不常用的,反正也不常用,多兩個字節沒關系。(這里請不要批判帝國主義霸權。不是人家想把英文搞這么短,是我們的中文想搞短也做不到……)
說回 Python。在 Python 3.3 版之后,對內部的 str 表示做了一些改進。教程里說:Python 內部使用 Unicode 來表示字符串,而在保存到硬盤或者發送到網上,需要轉成 UTF-8。這個說法其實不夠準確。在 Python 內部(不同的版本可能不同,大神在這里引用的是 Python 3 的文檔),是使用 UCS 的各種版本來表示字符串的。比如說,對整個字符串做一次掃描,發現所有的字符都小于 255,那么就使用 UCS1,也就是一個字節的編碼方案;如果發現有大于 255 的字符,但又都小于 65535,那么就使用 UCS2。當然,如果發現有奇怪的字符,那就只能使用 UCS4 了。為什么在內部要使用 UCS 呢?怎么不繼續使用 UTF-8,那樣不就不用轉換了嗎?(小白總是想省事)這個就涉及到 UTF-8 的一個缺點:計算字符串長度和查找子字符串非常沒效率。在使用 UCS2 的時候,要想知道這個字符串有多長,只要看一下它占了幾個字節,然后除個 2 就可以了,而 UTF-8 的話,就需要一個字符一個字符的數出來。在做子字符串搜索的時候,因為不知道下一個字符占幾個字節,所以那些高效的搜索算法也都不靈了(小白:算法是啥?)。基于這兩個原因,只好做一個轉換:在保存到硬盤或網絡傳輸的時候,使用一種壓縮的方案,使得傳輸或保存需要的字節數最少;而在內部進行處理的時候,則使用效率更高的 UCS 方案。
PS:大神非常爽的講完之后,小白似懂非懂的走開了……
參考資料: