為了徹底理解亂碼問題,一怒之下我把字符集歷史扒了個底朝天
- 前言
- 一個故事來理解為什么要編碼
- 為什么會亂碼
- 字符集的歷史
- ASCII 編碼的誕生
- IOS-8859 編碼家族誕生
- GB2312 和 GBK 等雙位元組編碼誕生
- Unicode 字符誕生
- UTF 編碼家族誕生
- UTF-32 編碼
- UTF-16 編碼
- UTF-8 編碼
- 為什么有時候亂碼都是 ? 號
- 拓展知識
- 代碼點和代碼單元
- 大端模式和小端模式
- BOM
- 總結
前言
在日常開發中,亂碼問題可以說曾經都困擾過我們,那么為什么會有亂碼發生呢?為什么全世界不統一使用一套編碼呢?本文將會從字符集的發展歷史來解答這兩個問題,看完本篇,相信大家對亂碼現象會有本質上的認識,
一個故事來理解為什么要編碼
現在有兩個人,張三和李四,張三只會中文,李四只會英文,那么這時候他們怎么溝通?解決辦法是他們可以找個翻譯,這個翻譯的程序就可以理解為編碼,也就是說從中文到英文或者從英文到中文這就是一個編碼的程序,編碼的本質就是為了讓對方能讀懂自己的語言,
人類的各種官方語言和方言數不勝數,所以在應用到在計算機時總不能兩兩互相編碼吧?而且最重要的是人類的語言并不適合計算機使用,所以就需要發明一種適合計算機的語言,這就是二進制,二進制就是當今世界計算機的語言,當然,曾經前蘇聯也發明過三進制計算機,但是沒有普及,這個感興趣的可以自己去了解下,
有了二進制這種計算機能讀懂的語言就好辦了,當我們想和計算機溝通的時候,先轉成二進制(編碼),計算機處理完成之后,再轉換回人類語言(解碼),這就是需要編碼的原因,
為什么會亂碼
但是為什么會亂碼呢?還是用上面的故事中張三李四來舉例,假如有一次張三說了一個生僻詞,然后翻譯從來沒見過這個詞,這時候翻譯就不知道怎么翻譯了,沒有辦法,就直接翻譯成了 ??,也就是亂碼了,
在計算機的世界也是同理,比如我們想從一個程式 A 發送 雙子孤狼 四個字到另一個程式 B,這時候計算機資料傳輸的時候會轉成二進制,傳輸過去之后,因為二進制不適合人類閱讀,所以 B 就需要進行解碼,可是現在 B 并不知道 A 用的是什么語言進行的編碼,所以就胡亂用英文進行解碼,解碼出來的字符英文肯定是不存在的,也就是在英文字符集里面找不到 雙子孤狼 這個單詞,這時候就會發生亂碼,
所以亂碼的本質其實就是當前編碼無法決議接收到的二進制資料,
字符集的歷史
知道了為什么要編碼以及亂碼的原因之后,不禁又有另一個疑問了,如果說全世界都統一用一種編碼,那在正常情況下也就沒有亂碼問題了,可是現實情況卻是各種編碼猶如八仙過海各顯神通,整的我們程式員頭暈腦脹,一不留神亂碼就出來了,不過要回答這個問題那么就需要了解一下字符集的發展歷史了,
ASCII 編碼的誕生
計算機最開始誕生于美國,而且計算機只能識別二進制,所以我們就需要把常用語言和二進制關聯起來,美國人把英文里面常用的字符以及一些控制字符轉換成了二進制資料,比如我們耳熟能詳的小寫字母 a,對應的十進制是 97,二進制就是 01100001,而一個位元組有 8 位,即最大能表示 255 個字符,但是英語的常用字符比較少,常用的字母以及一些常用符號列出來就是 128 個,所以美國人就占用了這 0-127 的位置,形成了一個編碼對應關系表,這就是 ASCII(American Standard Code for Information Interchange,美國標準資訊交換碼) 編碼,ASCII 編碼表的對應關系如果大家想知道的可以自己去查一下,這里就不列舉了,
IOS-8859 編碼家族誕生
隨著計算機的普及,計算機傳到了歐洲,這時候發現歐洲的常用字符也需要進行編碼,于是國際標準化組織(ISO)及國際電工委員會(IEC)決定聯合制定另一套字符集標準,于是 ISO-8859-1 字符集就誕生了,
因為 ASCII 只用到了 0-127 個位置,另外 128-255 的位置并沒有被占用(也就是一個位元組的最高位并沒有被使用),于是歐洲人就把第 8 位利用了起來,從此 這128-255 就被西歐常用字符占用了,ISO-8859-1 字符也叫做 Latin1 編碼,
慢慢的,隨著時間的推移,歐洲越來越多國家的字符需要編碼,所以就衍生了一系列的字符集,從 ISO-8859-1 到 ISO-8859-16 經過了一系列的微調,但是這些都屬于 ISO-8859 標準,
需要注意的是,ISO-8859 標準是向下兼容 ASCII 字符集的,所以平常我們見到的許多場景下默認都是用的 ISO-8859-1 編碼比較多,而不會直接使用 ASCII 編碼,
GB2312 和 GBK 等雙位元組編碼誕生
慢慢的,隨著時間的推移,計算機傳到了亞洲,傳到了中國以及其他國家,這時候許多國家都針對自己國家的常用文字制定了自己國家的編碼,中國也不例外,
但是這個時候卻發現,一個位元組的 8 位已經全部被占用了,于是只能再擴展一個位元組,也就是用 2 個位元組來存盤,但是兩個位元組來存盤又有一個問題,那就是比如我讀取了兩個位元組出來,這兩個位元組到底是表示兩個單位元組字符還是表示的是雙位元組的中文呢?
于是我們偉大的中國人民就決定制定一套中文編碼,用來兼容 ASCII,因為 ASCII 編碼中的單位元組字符一定是小于 128 的,所以最后我們就決定,中文的雙位元組字符都從 128 之后開始,也就是當發現字符連續兩位都大于 128 時,就說明這是一個中文,指定了之后我們就把這種編碼方式稱之為 GB2312 編碼,
需要注意的是 GB2312 并不兼容 ISO-8859-n 編碼集,但是兼容 ASCII 編碼,
GB2312 編碼收錄了常用的漢字 6763 個和非漢字圖形字符 682 (包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母在內的全角字符)個,
隨著計算機的更進一步普及,GB2312 也暴露出了問題,那就是 GB2312 中收錄的中文漢字都是簡體字和常用字,對于一些生僻字以及繁體字沒有收錄,于是乎 GBK 出現了,
GB2312 編碼因為兩個位元組采用的都是高位,就算全部對應上,最大也只能存盤 16384 個漢字,而我國漢字如果加上繁體字和生僻字是遠遠不夠的,于是 GBK 的做法就是只要求第一位是大于 128,第二位可以小于 128,這就是說只要發現一個位元組大于 128,那么緊隨其后的一個位元組就是和其作為一個整體作為中文字符,這樣最多就能存盤 32640 個漢字了,當然,GBK 并沒有全部用完,GBK 共收入 21886 個漢字和圖形符號,其中漢字(包括部首和構件)21003 個,圖形符號 883 個,
后面隨著計算機的再進一步普及,我們也慢慢擴展了其他的中文字符集,比如 GB18030 等,但是這些都屬于雙位元組字符,
到這里希望大家明白,為什么英文是一個字符,中文是兩個甚至更多字符了,一個原因就是低位被用了,另一個就是常用中文字符太多了,一個位元組是遠遠存不完的,
Unicode 字符誕生
其實計算機在發展程序中,不單單是美國,歐洲和中國,其他許多國家都有自己的字符,比如日本,韓國等都有自己的字符集,可以說很混亂,于是有關部門看不下去了,決定結束這種世界大戰的混亂局面,重新制定另一套字符標準,這就是 Unicode,
從一出生開始,Unicode 就覺得除了自己,其他各位都是渣渣,所以它壓根就沒準備兼容其他編碼,直接另起爐灶來了一套標準,Unicode 字符最開始采用的是 UCS-2 標準,UCS-2 標準規定一個字符至少使用 2 個位元組來表示,當然,2 個位元組即使全被利用也只能存盤 65536 個字符,這肯定容納不了世界上所有的語言和符號以及控制字符,所以后面又有了 UCS-4 標準,可以用 4 個位元組來存盤一個字符,四個位元組來存盤全世界所有語言文字和控制字符是基本沒有問題了,
需要注意的是:Unicode 編碼只是定義了字符集,對于字符集具體應該如何存盤并沒有做要求,站在我們開發的角度,相當于 Unicode 只定義了介面,但是沒有具體的實作,
UTF 編碼家族誕生
UTF 系列編碼就是對 Unicode 字符集的實作,只不過實作的方式有所區別,其中主要有:UTF-8,UTF-16,UTF-32 等型別,
UTF-32 編碼
UTF-32 編碼基本按照 Unicode字符集標準來實作,任何一個符號都占用 4 個位元組,可以想象,這會浪費多大空間,對英文而言,空間擴大了四倍,中文也擴大了兩倍,所以這種編碼方式也導致了 Unicode 在最初并沒有被大家廣泛的接受,
UTF-16 編碼
UTF-16 編碼相比較 UTF-32 做了一點改進,其采用 2 個位元組或者 4 個位元組來存盤,大部分情況下 UTF-16 編碼都是采用 2 個位元組來存盤,而當 2 個位元組存盤時,UTF-16 編碼會將 Unicode 字符直接轉成二進制進行存盤,對于另外一些生僻字或者使用較少的符號,UTF-16 編碼會采用 4 個位元組來存盤,但是采用四個位元組存盤時需要做一次編碼轉換,
下表就是 UTF-16 編碼的存盤格式:
| Unicode 編碼范圍(16 進制) | UTF-16 編碼的二進制存盤格式 |
|---|---|
| 0x0000 0000 - 0x0000 FFFF | xxxxxxxx xxxxxxxx |
| 0x0001 0000 - 0x0010 FFFF | 110110xx xxxxxxxx 110111xx xxxxxxxx |
這個表先不解釋,后面解釋 UTF-8 編碼時會一起說明,
UTF-8 編碼
UTF-8 是一種變長的編碼,兼容了 ASCII 編碼,為了實作變長這個特性,那么就必須要有一個規范來規定存盤格式,這樣當程式讀了 2 個或者多個位元組時能決議出這到底是表示多個單位元組字符還是一個多位元組字符,
UTF-8 編碼的存盤規范如下表所示:
| Unicode 編碼范圍(16 進制) | UTF-8 編碼的二進制存盤格式 |
|---|---|
| 0x0000 0000 - 0x0000 007F | 0xxxxxxx |
| 0x0000 0080 - 0x0000 07FF | 110xxxxx 10xxxxxx |
| 0x0000 0800 - 0x0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
| 0x0001 0000 - 0x0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
接下來我們以 雙 字為例來進行說明:
雙:對應的 Unicode 編碼為 \u53cc,轉成二進制就是:101001111001100,這時候表格中的第一行只有 7 位存不下去,第二列也只有 11 位,也不夠存盤,所以只能存盤到第三列,第三列有 16 位,從后往前依次填補 x 的位置,填完之后還有一位空余,直接補 0,最終得到:11100101 10001111 10001100,所以雙 字就占用了 3 個位元組,當然,有些生僻字會占用到四個位元組,
所以上面的 UTF-16 編碼也是同理,如果當前字符采用的是兩位元組存盤,那么直接轉成二進制存盤即可,位數不足直接補 0 即可,而當采用 4 個位元組存盤時,則需要和 UTF-8 一樣進行一次轉換,也就是說只能將其填充到 x 的位置,x 之外的是固定格式,
需要注意的是:在 UTF-16 編碼中,2 個位元組也可能出現 4 位元組中 110110xx 或者 110111xx 開頭的格式,這兩部分對應的區間分別是:D800~DBFF 和 DC00~DFFF,所以為了避免這種歧義的發生,這兩部磁區間是是專門空出來的,沒有進行編碼,
為什么有時候亂碼都是 ? 號
在 Java 開發中,經常會碰到亂碼顯示為 ? 號,比如下面這個例子:
String name = "雙子孤狼";
byte[] bytes = name.getBytes(StandardCharsets.ISO_8859_1);
System.out.println(new String(bytes));//輸出:????
這個輸出結果的原因是中文無法用 ISO_8859_1 編碼進行存盤,而示例中卻強制用 ISO_8859_1 編碼進行解碼,
在 Java 中提供了一個 ISO_8859_1 類用來解碼,解碼時當發現當前字符轉成十進制之后大于 255 時就會直接不進行解碼,轉而直接賦一個默認值 63,所以上面的示例中的 byte 陣列結果就是 63 63 63 63,而63 在 ASCII 中就恰好就對應了 ? 號,
所以一般我們看到編碼出現 ? 基本就說明當前是采用 ISO_8859_1 進行的解碼,而當前的字符又大于 255,
拓展知識
了解了編碼發展歷史之后,接下來就讓我們一起了解下其他和編碼相關的題外話,
代碼點和代碼單元
在 Java 中的字串是由 char 序列組成,而 char 又是采用 UTF-16 表示的 Unicode 代碼點的代碼單元,這句話里面涉及到了代碼點和代碼單元,初次接觸的朋友可能會有點迷惑,但是了解了 Unicode 字符集標準和 UTF-16 的編碼方式之后就比較好理解,
- 代碼點:一個代碼點等同于一個
Unicode字符, - 代碼單元:在
UTF-16中,兩個位元組表示一個代碼單元,代碼單元是最小的不可拆分的部分,所以如果在UTF-8中,一個代碼單元就是一個位元組,因為UTF-8中可以用一個位元組表示一個字符,
平常我們呼叫字串的 length() 方法,回傳的就是代碼單元數量,而不是代碼點數量,所有如果碰到一些需要用 4 個位元組來表示的繁體字,那么代碼單元數就會小于代碼點數,而想要獲取代碼點數量,可以通過其他方法獲取,獲取方式如下:
String name = "𤭢";//\uD852\uDF62
System.out.println(name.length());//代碼單元數,輸出2
System.out.println(name.codePointCount(0, name.length()));//代碼點數,輸出1
大端模式和小端模式
在計算機中,資料的存盤是以位元組為單位的,那么當一個字符需要使用多個位元組來表示的時候,就會產生一個問題,那就是多位元組字符應該從前往后組合還是從后往前組合,
還是以 雙 字為例,轉成二進制為:0101001111001100,以一個位元組為單位,就可以拆分成:01010011 和 11001100,其中第一部分就稱之為高位位元組,第二部分就稱之為低位位元組,將這兩部分順序互換存盤就產生了大端模式和小端模式,
- 大端模式(Big-endian):顧名思義就是以高位位元組結尾,低位在前(左),高位在后(右),如
雙字就會存盤為:11001100 01010011, - 小端模式(Little-endian):顧名思義就是以低位位元組結尾,高位在前(左),低位在后(右),如
雙字就會存盤為:01010011 11001100(和我們平常計算二進制的邏輯一致,從右到左依次從2的0次方開始),
注:在 Java 中默認采用的是大端模式,雖然底層的處理器可能會采用不同的模式存盤位元組,但是因為有 JVM 的存在,這些細節已經被屏蔽,所以平常大家可能也沒有很關注這些,
BOM
既然底層存盤分為了大端和小端兩種模式,那么假如我們現在有一個檔案,計算機又是怎么知道當前是采用的大端模式還是小端模式呢?
BOM 即 byte order mark(位元組順序標記),出現在文本檔案頭部,BOM 就是用來標記當前檔案采用的是大端模式還是小端模式存盤,我想這個大家應該都見過,平常在使用記事本保存檔案的時候,需要選擇采用的是大端還是小端:

在 UCS 編碼中有一個叫做 Zero Width No-Break Space(零寬無間斷間隔)的字符,對應的編碼是 FEFF,FEFF 是不存在的字符,正常來說不應該出現在實際資料傳輸中,
但是為了區分大端模式和小端模式,UCS 規范建議在傳輸位元組流前,先傳輸字符 Zero Width No-Break Space,而且根據這個字符的順序來區分大端模式和小端模式,
下表就是不同編碼的 BOM:
| 編碼 | 16 進制 BOM |
|---|---|
| UTF-8 | EF BB BF |
| UTF-16(大端模式) | FE FF |
| UTF-16(小端模式) | FF FE |
| UTF-32(大端模式) | 00 00 FE FF |
| UTF-32(小端模式) | FF FE 00 00 |
有了這個規范,決議檔案的時候就可以知道當前編碼以及其存盤模式了,注意這里 UTF-8 編碼比較特殊,因為本身 UTF-8 編碼有特殊的順序格式規定,所以 UTF-8 本身并沒有什么大端模式和小端模式的區別.
根據 UTF-8 本身的特殊編碼格式,在沒有 BOM 的情況下也能被推斷出來,但是因為微軟是建議都加上 BOM,所以目前存在了帶 BOM 的 UTF-8 檔案和不帶 BOM 的 UTF-8 檔案,這兩種格式在某些場景可能會出現不兼容的問題,所以在平常使用中也可以稍微留意這個問題,
總結
本文主要從編碼的歷史開始,講述了編碼的存盤規則并且分析了產生亂碼的本質原因,同時也分析了位元組的兩種存盤模型以及 BOM 相關問題,通過本文相信對于專案中出現的亂碼問題,大家會有一個清晰的思路來分析問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/277658.html
標籤:其他
上一篇:第三章 灰度變換與空間濾波
