帶你走進字符編碼的世界

思考一下,為什麼有字符編碼這種東西?

當然是為了讓計算機“聽話”唄。我們知道,計算機的世界只有01這兩個字符,而我們現實世界有成千上萬的字符。如何用01的組合去和現實中的字符一一對應呢?這就是需要制定相應的編碼規則來實現了。明白了這點,我們正式開始編碼的講解。

ASCII碼

我們知道,在計算機內部,所有的信息最終都表示為一個二進制的字符串。每一個二進制位(bit)有0和1兩種狀態,因此八個二進制位就可以組合出256種狀態(-128~127),這被稱為一個字節(byte)。也就是說,一個字節一共可以用來表示256種不同的狀態,每一個狀態對應一個符號,就是256個符號,從0000000到11111111。

上個世紀60年代,美國製定了一套字符編碼,對英語字符與二進制位之間的關係,做了統一規定。這被稱為ASCII碼,一直沿用至今。

ASCII碼一共規定了128個字符的編碼,比如空格“SPACE”是32(二進制00100000),大寫的字母A是65(二進制01000001)。這128個符號(包括32個不能打印出來的控制符號),只佔用了一個字節的後面7位,最前面的1位統一規定為0。

ASCII碼用了1個字節,1個字節可以表示256種狀態,但ASCII碼只用了128種,也就是一個字節的後七位,最前面的1位都是0。

非ASCII編碼

英語用128個符號編碼就夠了,但是用來表示其他語言,128個符號是不夠的。比如,在法語中,字母上方有注音符號,它就無法用ASCII碼錶示。於是,一些歐洲國家就決定,利用字節中閒置的最高位編入新的符號。比如,法語中的é的編碼為130(二進制10000010)。這樣一來,這些歐洲國家使用的編碼體系,可以表示最多256個符號。

但是,這裡又出現了新的問題。不同的國家有不同的字母,因此,哪怕它們都使用256個符號的編碼方式,代表的字母卻不一樣。比如,130在法語編碼中代表了é,在希伯來語編碼中卻代表了字母Gimel (ג),在俄語編碼中又會代表另一個符號。但是不管怎樣,所有這些編碼方式中,0—127表示的符號是一樣的,不一樣的只是128—255的這一段。

至於亞洲國家的文字,使用的符號就更多了,漢字就多達10萬左右。一個字節只能表示256種符號,肯定是不夠的,就必須使用多個字節表達一個符號。比如,簡體中文常見的編碼方式是GB2312,使用兩個字節表示一個漢字,所以理論上最多可以表示256x256=65536個符號。


中文編碼的問題需要專文討論,這篇筆記不涉及。這裡只指出,雖然都是用多個字節表示一個符號,但是GB類的漢字編碼與後文的Unicode和UTF-8是毫無關係的。

Unicode編碼

正如上一節所說,世界上存在著多種編碼方式,同一個二進制數字可以被解釋成不同的符號。因此,要想打開一個文本文件,就必須知道它的編碼方式,否則用錯誤的編碼方式解讀,就會出現亂碼。為什麼電子郵件常常出現亂碼?就是因為發信人和收信人使用的編碼方式不一樣。

可以想象,如果有一種編碼,將世界上所有的符號都納入其中。每一個符號都給予一個獨一無二的編碼,那麼亂碼問題就會消失。這就是Unicode,就像它的名字都表示的,這是一種所有符號的編碼。

Unicode(統一碼、萬國碼、單一碼)是計算機科學領域裡的一項業界標準,包括字符集、編碼方案等。Unicode 是為了解決傳統的字符編碼方案的侷限而產生的,它為每種語言中的每個字符設定了統一併且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。

Unicode是國際組織制定的可以容納世界上所有文字和符號的字符編碼方案。目前的Unicode字符分為17組編排,0x0000 至 0x10FFFF,每組稱為平面(Plane),而每平面擁有65536個碼位,共1114112個。然而目前只用了少數平面。UTF-8、UTF-16、UTF-32都是將數字轉換到程序數據的編碼方案。

Unicode當然是一個很大的集合,現在的規模可以容納100多萬個符號。每個符號的編碼都不一樣,比如,U+0639表示阿拉伯字母Ain,U+0041表示英語的大寫字母A,U+4E25表示漢字“嚴”。具體的符號對應表,可以查詢unicode.org,或者專門的漢字對應表。

Unicode的問題

需要注意的是,Unicode只是一個符號集,它只規定了符號的二進制代碼,卻沒有規定這個二進制代碼應該如何存儲。比如,漢字“嚴”的unicode是十六進制數4E25,轉換成二進制數足足有15位(100111000100101),也就是說這個符號的表示至少需要2個字節。表示其他更大的符號,可能需要3個字節或者4個字節,甚至更多。

這裡就有兩個嚴重的問題:1. 如何才能區別unicode和ascii?計算機怎麼知道三個字節表示一個符號,而不是分別表示三個符號呢?2. 我們已經知道,英文字母只用一個字節表示就夠了,如果unicode統一規定,每個符號用三個或四個字節表示,那麼每個英文字母前都必然有二到三個字節是0,這對於存儲來說是極大的浪費,文本文件的大小會因此大出二三倍,這是無法接受的。

它們造成的結果是:

  1. 出現了unicode的多種存儲方式,也就是說有許多種不同的二進制格式,可以用來表示unicode。
  2. unicode在很長一段時間內無法推廣,直到互聯網的出現。

UTF-8

互聯網的普及,強烈要求出現一種統一的編碼方式。UTF-8就是在互聯網上使用最廣的一種unicode的實現方式。其他實現方式還包括UTF-16和UTF-32,不過在互聯網上基本不用。重複一遍,這裡的關係是,UTF-8是Unicode的實現方式之一

UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個字節表示一個符號,根據不同的符號而變化字節長度

UTF-8的編碼規則很簡單,只有二條:

  1. 對於單字節的符號,字節的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。

  2. 對於n字節的符號(n>1),第一個字節的前n位都設為1,第n+1位設為0,後面字節的前兩位一律設為10。剩下的沒有提及的二進制位,全部為這個符號的unicode碼。

下表總結了編碼規則,字母x表示可用編碼的位。

Unicode符號範圍 | UTF-8編碼方式
(十六進制) | (二進制)
--------------------+---------------------------------------------
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

下面,還是以漢字“嚴”為例,演示如何實現UTF-8編碼。

已知“嚴”的unicode是4E25(100111000100101),根據上表,可以發現4E25處在第三行的範圍內(0000 0800-0000 FFFF),因此“嚴”的UTF-8編碼需要三個字節,即格式是“1110xxxx 10xxxxxx 10xxxxxx”。然後,從“嚴”的最後一個二進制位開始,依次從後向前填入格式中的x,多出的位補0。這樣就得到了,“嚴”的UTF-8編碼是“11100100 10111000 10100101”,轉換成十六進制就是E4B8A5。

那unicode和UTF-8有何區別?
Unicode 是「字符集」
UTF-8 是「編碼規則」

字符集

:為每一個「字符」分配一個唯一的 ID(學名為碼位 / 碼點 / Code Point)
編碼規則:將「碼點」轉換為字節序列的規則

BCD碼

上面講的是字符編碼,是指一個字符對應的一個二進制數。而BCD碼是計算機在對十進制數做運算或存儲時採用的二進制格式

Binary-Coded Decimal‎,簡稱BCD,稱BCD碼或二-十進制代碼,亦稱二進碼十進數。是一種二進制的數字編碼形式,用二進制編碼的十進制代碼。這種編碼形式利用了四個位元來儲存一個十進制的數碼,使二進制和十進制之間的轉換得以快捷的進行。

BCD碼的優點是效率高:比如十進制要以二進制的形式在計算機中存儲,十進制直接轉換成與之對應的BCD碼比十進制通過除法取餘再轉換的效率來的高。

Base64編碼

定義:

Base64是網絡上最常見的用於傳輸8Bit字節碼的編碼方式之一,Base64就是一種基於64個可打印字符來表示二進制數據的方法。

為什麼會有base64?
由於HTTP協議是文本協議,所以在HTTP協議下傳輸二進制數據需要將二進制數據轉換為字符數據。然而直接轉換是不行的。因為網絡傳輸只能傳輸可打印字符

問: 什麼是“可打印字符”呢?
答: 在ASCII碼中規定,0~31、128這33個字符屬於控制字符,32~127這95個字符屬於可打印字符,也就是說網絡傳輸只能傳輸這95個字符,不在這個範圍內的字符無法傳輸。

問: 那麼該怎麼才能傳輸其他字符呢?
答: 其中一種方式就是使用Base64。Base64一般用於在HTTP協議下傳輸二進制數據。

base64實現原理
Base64的索引與對應字符的關係如下表所示:

帶你走進字符編碼的世界


也就是說,如果將索引轉換為對應的二進制數據的話需要至多6個Bit(2^6=64)。然而ASCII碼需要8個Bit來表示,那麼怎麼使用6個Bit來表示8個Bit的數據呢?6個Bit當然不能存儲8個Bit的數據,但是46個Bit可以存儲38個Bit的數據啊

帶你走進字符編碼的世界


可以看到“Son”通過Base64編碼轉換成了“U29u”。這是剛剛好的情況,3個ASCII字符剛好轉換成對應的4個Base64字符。但是,當需要轉換的字符數不是3的倍數的情況下該怎麼辦呢?Base64規定,當需要轉換的字符不是3的倍數時,一律採用補0的方式湊足3的倍數,具體如下表所示:

帶你走進字符編碼的世界


每6個Bit為一組,第一組轉換後為字符“U”,第二組末尾補4個0轉換後為字符“w”。剩下的使用“=”替代。即字符“S”通過Base64編碼後為“Uw==”。這就是Base64的編碼過程。

好了,原理懂了,那麼如果要進行base64編碼,我們該怎麼做呢?自己擼一個方法?找一個庫?都行,但是HTML規範中已經規定了base64轉換的API,window對象上可以訪問到base64編碼和解碼的方法,直接調用即可。
window.atob() // 對base64編碼過的字符串進行解碼
window.btoa() // 對ASCII編碼的字符串進行base64編碼(不支持漢字,漢字可通過URIencode預處理後再編碼)

base64有哪些應用場景
前端將較小的icon編碼為base64直接在文檔中加載,減少http請求
電子郵件傳輸二進制文件時,通常用base64編碼後再傳

注意
base64編碼後的數據量是要比編碼前大的,所以base64不能用於減少數據量。
base64不能用於加密數據,即使使用私有的索引表也是不安全的。

關於轉中文出錯:btoa("中文") // The string to be encoded contains characters outside of the Latin1 range.意思就是超出支持範圍,ASCII。

但是,如果你非要使用btoa來base64轉碼中文,也不是不行,就是略微蛋疼。如下:

MIME類型

每個MIME類型由兩部分組成,前面是數據的大類別,例如聲音audio、圖象image等,後面定義具體的種類

常見的MIME類型(通用型):
超文本標記語言文本 .html text/html
xml文檔 .xml text/xml
XHTML文檔 .xhtml application/xhtml+xml
普通文本 .txt text/plain
RTF文本 .rtf application/rtf
PDF文檔 .pdf application/pdf
Microsoft Word文件 .word application/msword
PNG圖像 .png image/png
GIF圖形 .gif image/gif
JPEG圖形 .jpeg,.jpg image/jpeg
au聲音文件 .au audio/basic
MIDI音樂文件 mid,.midi audio/midi,audio/x-midi
RealAudio音樂文件 .ra, .ram audio/x-pn-realaudio
MPEG文件 .mpg,.mpeg video/mpeg
AVI文件 .avi video/x-msvideo
GZIP文件 .gz application/x-gzip
TAR文件 .tar application/x-tar
任意的二進制數據 application/octet-stream

URI編碼解碼

URL傳輸過程?
HTTP協議中參數組件的傳輸是key=value鍵值對的形式,如果要傳輸多個參數就需要用“&”符號對鍵值對進行分隔。例如?name1=value1&name2=$value2,這樣在服務器收到這種字符串的時候,會用“&”分隔出每一個參數,然後再用“=”來分隔出參數值。

針對name1=value1&name2=value2我們來說一下客戶端到服務器端的概念上解析過程:

上述字符串在計算機中用ASCII碼(16進制)表示為:6E616D6531 3D 76616C756531 26 6E616D6532 3D 76616C756532

服務器端在接收到該數據後就可以遍歷該字節流,首先一個字節一個字節的讀取,當讀到3D這個字節的時候,服務器端就知道前面讀到的字節串表示一個key,繼續讀取,如果遇到了26,表示從剛才讀到的3D到26字節之間的字節串是上一個key的value,按照此方法就可以解析出客戶端傳過來的參數。

現在又這樣一個問題:如果我的參數值中就包含=或者&這樣的特殊子字符的時候,該怎麼辦。比如說name1=value1,其中value1的值是va&lu=e1,那麼在傳輸過程中就會變成name1=va&lu=e1。用戶傳輸的本意是隻有一個鍵值對,但是服務器端會解析成兩個鍵值對,這樣就自然的產生了歧義。

如何解決上述問題帶來的歧義呢?解決之法就是對URL進行編碼!!!

URL編碼只是簡單的在特殊字符的各個字節(16進制)前加上”%”即可。例如,我們對上述會產生歧義的字符(“va&lu=e1”)進行編碼後的結果:name1=va%26lu%3D,這樣服務器會把緊跟在”%”後的字節當成普通的字節,不會把它當成各個參數或鍵值對的分隔符。

另外一個問題是,為什麼要用ASCII碼傳輸,可不可以用別的編碼?
因為一些歷史的原因URL設計者使用US-ASCII字符集表示URL。(原因比如ASCII比較簡單;所有的系統都支持ASCII)。當然可以用別的編碼,你可以自己開發一套編碼然後自己進行解析。就像大部分國家都有自己的語言一樣。但是國家之間要怎麼進行交流呢,用英語吧,英語的使用範圍最廣。

通常如果一樣的東西需要編碼,就說明這樣的東西並不適合傳輸。至於原因有多種多樣,size過大,包含隱私數據等等。對於URL來說,之所有要進行編碼,是因為URL中有些字符會引起歧義。
例如,URL參數字符串中如果包含”&”或者”%”勢必會造成服務器解析錯誤,所以需要對其進行編碼。
又如,URL的編碼格式採用的是ASCII碼而不是Unicode,這也就是說你不能在URL中包含任何非ASCII字符,比如中文。否則如果客戶端瀏覽器和服務器端瀏覽器支持的字符集不同的情況下,中文可能會造成問題。

URL編碼的原則就是使用安全的字符(沒有特殊用途或者特殊意義的可打印字符)去表示那些不安全的字符。

哪些字符需要編碼
RFC3986文檔規定,URL中只允許包含英文字母(a-zA-Z)、數字(0-9)、- _ . ~4個特殊字符以及所有的保留字符。RFC3986文檔對URL的編碼解碼問題做出了詳細的建議,指出了哪些字符需要被編碼才不會引起URL語義的轉變,以及對為什麼這些字符需要編碼做出了相應的解釋。

US-ASCII字符集中沒有對應的可打印字符:URL中只允許使用可打印的字符。US-ASCII碼中的10-7F字節全都表示控制字符,這些字符不能直接出現在URL中。同時對於80-FF字節,由於已經超出了ASCII碼定義字符的範圍,因此也不能放在URL中。

保留字符:RUL可以劃分為幹了組件,協議、主機、路徑等。有一些字符(: / ? # [ ] @)是用作分隔不同組件的。例如:冒號用於分隔協議和主機組件,斜槓用於分隔主機和路徑,問號用於分隔路徑和查詢參數,等等。還有一些字符(! $ & * + , ; =)用於在每個組件中起到分隔作用,如等號用於表示查詢參數中的鍵值對,&符號用於分隔查詢多個鍵值對。當組件中的普通數據包含這些特殊字符時,需要對其進行編碼。

RFC3986中指定了以下字符為保留字符: ! * ’ ( ) ; : @ & = + $ , / ? # [ ]

不安全字符:還有一些字符,當他們直接放在URL中的時候,可能會引起解析程序的歧義。這些字符被視為不安全的字符,原因有很多。

空格:URL在傳輸的過程,或者用戶在排版的過程中,或者文本處理程序在處理URL的過程,都有可能引入無關緊要的空格,或者將那些有意義的空格給去掉。
引號 以及 <>:引號和尖括號通常用於在普通文本中起到分隔URL的作用。
警號#:通常用於表示書籤或者錨點。
%:百分號本身用作對不安全的字符進行編碼是使用的特殊字符,因此本身需要編碼。
{ } | ^ [ ] ’ ~:某一些網關或者傳輸代理會篡改這些字符

需要注意的是,對於URL中的合法字符,編碼和不編碼是等價的,但是對於上邊提到的這些字符,如果不經過編碼,那麼它們可能會造成URL語義的不同。因此對於URL而言,只有普通英文字符和數字,特殊字符$ - _ . + ! * ’ ( )還有保留字符,才能出現在未經編碼的Url中,其他字符均需要編碼之後才能出現在URL中。
但是由於歷史原因,目前尚存在一些不標準的編碼實現,例如對於”~”符號,雖然RFC3986文檔規定,對於波浪號~不需要進行URL編碼,但是還是有很多老的網關或者傳輸代理會進行編碼。

如何對URL中的非法字符進行編碼?

URL編碼通常也被稱為百分號編碼,是因為它的編碼方式非常簡單,使用%加上兩位字符———[0-9A-F]———代表一個字節的十六進制的形式。URL編碼默認使用的字符集是US-ASCII碼,例如a在US-ASCII碼中對應的字節值是0x61,那麼URL編碼之後得到的就是%61,我們在地址欄中輸入http://g.cn/search?q=%61%62%63,實際上就等於在google中搜索abc。又如@符號在ASCII字符集中對應的字節為0x40,經過URL編碼之後得到的就是%40。

對於非ASCII字符,需要使用ASCII字符集的超集進行編碼得到相應的字節,然後對每個字節執行百分號編碼。對於Unicode字符,RFC文檔建議使用utf-8對其進行編碼得到相應的字節,然後對每個字節執行百分號編碼。如”中文”使用UTF-8編碼得到的字節是0xE4 0xB8 0xAD 0xE6 0x96 0x87,經過URL編碼之後得到%E4%B8%AD%E6%96%87。

如果某個字符對應的ASCII字符集中的某個非保留字符,則此字節無需使用百分號表示。例如”Url編碼”,使用UTF-8編碼得到的字節是0x55 0x72 0x6C 0xE7 0xBC 0x96 0xE7 0xA0 0x81,由於前三個字節對應著ASCII中的非保留字符”Url”,因此這三個字節可以用非保留字符”Url”表示。最終”Url編碼”經過編碼之後得到的是Url%E7%BC%96%E7%A0%81,當然,如果你用%55%72%6C%E7%BC%96%E7%A0%81也是可以的。

由於歷史原因,有一些Url編碼實現並不完全遵循這樣的原則。JS中提供3個函數對URL進行編碼和解碼:escape/unescape,encodeURI/decodeURI,encodeURIComponent/decodeURIComponent。

區別
這三對函數的安全字符(即不需要編碼的字符)範圍也不同,如下所示:

escape(69個):/@+-._0-9a-zA-Z
encodeURI(82個):!#$&'()+,/:;=?@-._~0-9a-zA-Z
encodeURIComponent(71個):!'()*-._~0-9a-zA-Z

現在對比encodeURI和encodeURIComponent,從名稱上可看出encodeURI是針對整個URI進行編碼,我們以特殊的URI--URL來說明下。

對於URL為http://www.baidu.com而言,如果用encodeURI編碼,返回的仍是“http://www.baidu.com”;如果用encodeURIComponent編碼,返回的為"http%3A%2F%2Fwww.baidu.com"。

encodeURI所針對的是整個URI,並不會對分隔符如/,?,=符號進行編碼,否則破壞了URI的原有含義,而encodeURIComponent則是針對URI的

某一部分進行編碼,如查詢字符串部分的&會被轉義。


分享到:


相關文章: