跟我一起夯實程式設計基礎 - 字元編碼

前行的烏龜發表於2020-02-02

跟我一起夯實程式設計基礎 - 字元編碼

為什麼要寫這篇呢?

很久之前入門學習 java 的時候第一次接觸到字元編碼這個東西,稍後在學習 web 基礎的時候接觸到了 UTF-8、字元亂碼。當時我以為我已經足夠了解字元編碼了

但直到我有一天我在掘金上看到一個問題:一箇中文字元佔幾個位元組?

回想當初老師告訴我們,一箇中文字元佔2個位元組,但是這種說法其實大錯特錯,Unicode 編碼中一箇中文字元可不是佔2個位元組的

所以才有了今天這篇文章,很多東西我們以為已經足夠了解了,但是依然被面試官打趴下。歸根結底我們為什麼不清楚、不知道呢,就是因為我們不是按照歷史的發展脈絡來學習的,所以我們必然有遺落,有不清楚

國外的學習資料,很多都喜歡把相關歷史發展講的明明白白的。以前不甚理解,但是現在我理解了,不瞭解歷史,你就抓不住全部


字符集、編碼集、儲存方式

時間從二戰來到50年代末60年代初,計算機發展很迅速,從軍用、科學向商業、辦公、教育等其他領域擴充套件,這時為了方便文字的顯示、儲存、輸入,字元編碼標準出現了

這直接崔生了世界首個字元編碼標準:ASCII 的誕生

ASCII 是什麼東西,就是一組儲存一種語言所有字母和字元的 Map 集合。value 是該字母,key 是該字母統一對外呼叫的標記號碼,就像門牌地址一樣,讓我們在一堆資料中準確快速的找到你。value 的集合叫:字符集,key 的集合叫:編碼集

字符集 會把你所在的語言體系裡面所有的字母、字元之類的全存進去,這些字元是計算機顯示的基礎,計算機根據我們輸入的字元代號來找出這些字元本身,然後顯示出來

比如 value:A 對應的 key:1,我們在輸入時,把1交給計算機,計算機就知道我們想要顯示A這個字元

計算機是 2 進位制儲存的,每一個 0 或 1 表示一位,8 個一位合起來是 1 個位元組,計算機儲存是按位元組為基本單位儲存的

跟我一起夯實程式設計基礎 - 字元編碼

英文因為字元少,所以 7 位的範圍:0-128 就能涵蓋所有字元了,此時 編碼集 使用自然循序序號表示即可,7 位的 2 進位制數,比如:0101011

但是在碰到中文、日本等文字後,這些文字不是字母拼接型別的語言,而是單個字元語言,中文裡有 3 萬個字元。字符集 倒是沒什麼,有什麼字元存什麼字元就行了。但是 編碼集 就有問題了,如果還是使用自然順序序號來表示字元編號,那麼有可能一個字元的 2 進位制編碼數會很長很長,非常不利於輸入和觀察,此時一箇中文詞語可能是這樣的:0100100001000101010011000100110001001111。這要是讓你輸入估計會是個災難,所以為了解決 編碼集 過長的問題,大家決定讓 編碼集 在輸入時使用 16 進位制,比如常見的:\u{1f44d},去掉格式化字元,1f44d 就是這個字元所在的編碼,這個 16 進位制的編碼在記憶體中還是以對應的 2 進位制數儲存

跟我一起夯實程式設計基礎 - 字元編碼

還有一個問題,字元在 字符集 中是如何儲存的。像英文字元少,所以 7 位 2 進位制 128 個位置 就能搞定,這樣英文的字元比編碼用一個位元組就可以了

但是中文呢,還有世界其他的那些語言呢,文字內字元很多,尤其是中文有幾萬個字元,那 編碼集 使用 7 位就不夠了,至少也得 16 位 65535 個位置才能放得下。這樣的話,一個字元就得用 2 個位元組甚至更多位元組表示了。但是中文中也會用到數字、應為字母之類的,這些字元若是也用 2 個位元組表示,就會浪費儲存空間,降低 CPU 計算效率

為了應對這種情況,有的 編碼集 採用可變字長,像英文字母之類的字元用 1 個位元組,有的字元用 2 個位元組或是更多。這種問題就叫做:儲存方式優化

有的朋友會問為什麼 2 個位元組會有浪費儲存空間的問題呢?螢幕上雖然我們看著是一個個文字,但是這些文字在計算機,也就是記憶體中全是按照字元對應的字元編碼的 2 進位制數儲存的, 也就是 編碼集 這個東西,所以表示一個字元使用的位元組越多,那麼越佔用,浪費資源

注意以下:

  • 字符集、編碼集、儲存方式 這3者共同組成了一個字元編碼標準,他們其中有任何一個產生變化都會演變成一個新的字元編碼標準
  • 有的字元編碼標準採用可變字長
  • 字元編碼標準之間要相容很難,很多文字亂碼就是字元標準之間不相容的問題

希望我這種特例獨行的解釋能讓大家接受,我覺得這樣最好理解,以上沒有抄襲任何諸如百度百科之類的解釋,完全是我自己的認知,有差錯請指出,在此萬分感謝!


字元編碼發展史

1. ASCII 碼時代

1960年 ASCII 碼 字元編碼出臺,使用7位編碼,有效位置是 128 個,用來統一英文的輸入、儲存、顯示,因為計算機是按位元組儲存的,所以補了一位,以 0 開頭

跟我一起夯實程式設計基礎 - 字元編碼

2. 擴充套件 ASCII 碼時代

ASCII 碼 出來後,效果很好,但是歐洲其他國家有自己的語言,自己的字元,所以紛紛盯上了 ASCII 碼 沒有使用的補 0 的這一位,擴充成了有效空間為 256 個的字元編碼。但是呢,這些歐洲國家自己搞自己的,搞出來的字元編碼相互不能通用,非常混亂,亂碼成了一個棘手的問題

跟我一起夯實程式設計基礎 - 字元編碼

3. GB2312/GBK 時代

1981年,我過出臺了自己的面向中文的字元編碼:GB2312,包含 7445 個字元,包括 6763 個漢字,682 個字元

雖然又推出了:GBK,支援更多的中文字元,支援共 21003 個漢字,並且完整支援中日韓文字

GB2312/GBK 系中文字元標準,window 中文版預設就是使用 GB2312 這個字元編碼,特點是每個字元使用2個位元組

4. Unicode 萬國碼

前面說過,大家自己搞自己的字元編碼,整個相互不通用,竟是亂碼,隨著網際網路的發展,這樣可是不行的,隨後 ISO 組織出面集合大夥搞了統一的,大家一起使用的,相容各自字元編碼的國際統一碼:Unicode

Unicode 使用4個位元組(可以擴容支援更多位元組)的字元範圍,預設100多萬個字元位置,以容納世界上所有的語言,特殊字元,emoji 表情這些

Unicode 把目前分成 17個扇區,每個扇區有 65535 個位置,規定不同型別的字元儲存在不同的扇區

跟我一起夯實程式設計基礎 - 字元編碼

跟我一起夯實程式設計基礎 - 字元編碼

有一點十分重要,Unicode 只是一種 編碼集 規範,規定了一個字元對應的字元的位置,但是針對每個字元都佔用4個位元組的問題,又產生了 UTF 這種經過優化的 字元編碼規範


UTF 編碼

其他的都不用詳說了,UTF 編碼 是我們平時最常用的,需要詳細的展開一下,目前 UTF 編碼 有3種規範:

  • UTF-8: 可變字元編碼,佔用1到4個位元組
  • UTF-16: 可變字元編碼,佔用2到4個位元組
  • UTF-32: 不可變字元編碼,統一使用4個位元組表示一個字元

大家要知道這3其實是一回事,搞清楚一個其他也就明白了,都是優化位元組佔用量。很多時候 Unicode 4個位元組的儲存方式裡,這4個位元組的數字裡面很多都是沒有用的,純粹為了補位的,像英文1個位元組就夠了,這就是優化的原動力

UTF-8 使用一至四個位元組為每個字元編碼

  • 使用一個位元組編碼:128 個 ASCII 字元(Unicode 範圍由 U+0000 至 U+007F)
  • 使用二個位元組編碼:帶有變音符號的拉丁文、希臘文、西裡爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及馬爾地夫語(Unicode 範圍由 U+0080 至 U+07FF)
  • 使用三個位元組編碼:其他基本多文種平面(BMP)中的字元(CJK屬於此類-Qieqie注),中文就在這個範圍內
  • 使用四個位元組編碼:其他 Unicode 輔助平面的字元,比如 emoji 表情

UTF-16UTF-8 不同的地方在於英文等字元不再是一個字元編碼了,而是2個

UTF-32 統一使用4個位元組編碼,我們處理 emoji 表情符號基本上都是轉成 UTF-32 來顯示

大家看懂了嗎~ 這就是 UTF-8 被廣泛採用的原因,對於英文的優化真是好...

有一道經典的面試題:中文佔幾個字元,這下大家知道怎麼回答了吧,GBK 是2個,UTF-8 是3個,UTF-8 是4個

為啥是3個呢?UTF 裡面每8位開頭都有表示分類和位置的佔位,3個位元組裡面正好有1個位元組被這種佔位佔走了,剩下的2位才能承載中文那幾萬個字元,所以 UTF 編碼中中文統一都是用3個字元編碼

大家看圖:

跟我一起夯實程式設計基礎 - 字元編碼


字元佔位對照圖

編碼 英文位元組數 中文位元組數
GB2312 1 2
GBK 1 2
GB18030 1 2
ISO-8859-1 1 1
UTF-8 1 3
UTF-16 2 4
UTF-32 4 4
UTF-16BE 2 2
UTF-16LE 2 2

Dart、Flutter 中的 emoji

讓我對字元編碼產生疑問的是從 emoji 顯示這個問題開始的,這裡記錄下我找到的資料:

  • Dart 文字顯示預設是 UTF-16 的
  • 我們相容 emoji 的話最好用 UTF-32
  • Flutter 提供了 Runes 這個類,來儲存、轉換 UTF-32 編碼的字元

不知道別的平臺怎麼讓 emoji 顯示出來的,反正 Flutter 想顯示 emoji 必須使用 UTF-32 這一種方式

Runes emojiString = new Runes('\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d} 哇哈哈哈哈!!!');
var index = String.fromCharCodes(emojiString)
複製程式碼

跟我一起夯實程式設計基礎 - 字元編碼


相關文章