帶你走進字元編碼的世界
- 2019 年 11 月 10 日
- 筆記
思考一下,為什麼有字元編碼這種東西?
當然是為了讓電腦「聽話」唄。我們知道,電腦的世界只有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,使用兩個位元組表示一個漢字,所以理論上最多可以表示256×256=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,這對於存儲來說是極大的浪費,文本文件的大小會因此大出二三倍,這是無法接受的。
它們造成的結果是:
- 出現了unicode的多種存儲方式,也就是說有許多種不同的二進位格式,可以用來表示unicode。
- unicode在很長一段時間內無法推廣,直到互聯網的出現。
UTF-8
互聯網的普及,強烈要求出現一種統一的編碼方式。UTF-8就是在互聯網上使用最廣的一種unicode的實現方式。其他實現方式還包括UTF-16和UTF-32,不過在互聯網上基本不用。重複一遍,這裡的關係是,UTF-8是Unicode的實現方式之一。
UTF-8最大的一個特點,就是它是一種變長的編碼方式。它可以使用1~4個位元組表示一個符號,根據不同的符號而變化位元組長度。
UTF-8的編碼規則很簡單,只有二條:
- 對於單位元組的符號,位元組的第一位設為0,後面7位為這個符號的unicode碼。因此對於英語字母,UTF-8編碼和ASCII碼是相同的。
- 對於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)
編碼規則:將「碼點」轉換為位元組序列的規則
參考:字元編碼筆記:ASCII、Unicode、UTF-8 和 Base64
BCD碼
上面講的是字元編碼,是指一個字元對應的一個二進位數。而BCD碼是電腦在對十進位數做運算或存儲時採用的二進位格式。
Binary-Coded Decimal,簡稱BCD,稱BCD碼或二-十進位程式碼,亦稱二進碼十進數。是一種二進位的數字編碼形式,用二進位編碼的十進位程式碼。這種編碼形式利用了四個位元來儲存一個十進位的數碼,使二進位和十進位之間的轉換得以快捷的進行。
BCD碼的優點是效率高:比如十進位要以二進位的形式在電腦中存儲,十進位直接轉換成與之對應的BCD碼比十進位通過除法取余再轉換的效率來的高。
Base64編碼
什麼是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轉碼中文,也不是不行,就是略微蛋疼。如下:
btoa(escape("中文")) // "JXU0RTJEJXU2NTg3" unescape(atob("JXU0RTJEJXU2NTg3")) // "中文" btoa(encodeURI("https://zweizhao.com/文章/JS常用轉碼URI與Base64.md")) // "aHR0cHM6Ly96d2Vpemhhby5jb20vJUU2JTk2JTg3JUU3JUFCJUEwL0pTJUU1JUI4JUI4JUU3JTk0JUE4JUU4JUJEJUFDJUU3JUEwJTgxVVJJJUU0JUI4JThFQmFzZTY0Lm1k" decodeURI(atob("aHR0cHM6Ly96d2Vpemhhby5jb20vJUU2JTk2JTg3JUU3JUFCJUEwL0pTJUU1JUI4JUI4JUU3JTk0JUE4JUU4JUJEJUFDJUU3JUEwJTgxVVJJJUU0JUI4JThFQmFzZTY0Lm1k")) // "https://zweizhao.com/文章/JS常用轉碼URI與Base64.md" btoa(encodeURIComponent("https://zweizhao.com/文章/JS常用轉碼URI與Base64.md")) // "aHR0cHMlM0ElMkYlMkZ6d2Vpemhhby5jb20lMkYlRTYlOTYlODclRTclQUIlQTAlMkZKUyVFNSVCOCVCOCVFNyU5NCVBOCVFOCVCRCVBQyVFNyVBMCU4MVVSSSVFNCVCOCU4RUJhc2U2NC5tZA==" decodeURIComponent(atob("aHR0cHMlM0ElMkYlMkZ6d2Vpemhhby5jb20lMkYlRTYlOTYlODclRTclQUIlQTAlMkZKUyVFNSVCOCVCOCVFNyU5NCVBOCVFOCVCRCVBQyVFNyVBMCU4MVVSSSVFNCVCOCU4RUJhc2U2NC5tZA==")) // "https://zweizhao.com/文章/JS常用轉碼URI與Base64.md"
參考:
從base64到atob和btoa的一些理解
JS常用轉碼URI與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的
某一部分進行編碼,如查詢字元串部分的&會被轉義。
參考:為什麼要進行URL編碼
結尾彩蛋
關於字元編碼,來點有意思的emoji圖標:?, ??, ??, ??♂️, ????。
看看這些可愛的小圖標,放在上個世紀,這隻能用圖片做,但現在都這些都是一個個真實的字元。感興趣的可以研究下Emoji與Unicode、從Emoji的限制到Unicode編碼