1 字符編碼
1.1 什么是字符編碼
眾所周知,在計算機的世界中,不管是存儲還是傳輸都是通過二進制(單位:比特,bit)來表示數(shù)據(jù)的,而我們?nèi)祟惸芾斫獾奈淖帧?biāo)點符號、特殊符號、emoji表情等字符只有被映射為二進制才能被計算機所識別,而這個映射則被稱之為字符編碼(Character Encoding,以下簡稱編碼),反之稱為解碼。
1.2 現(xiàn)代編碼模型
關(guān)于編碼涉及的概念非常多,Unicode Technical Report #17給出的字符編碼模型主要包括抽象字符庫、編碼字符集、字符編碼模式、字符編碼方案四個層次。盡管是unicode出的編碼模型,對于套用在一些其他的編碼上依然適用(比如ISO8859-1、GBK等)。
(1)抽象字符庫(Abstract character repertoire)
抽象字符庫是一個系統(tǒng)支持的所有抽象字符的集合。通常情況下可以簡單地理解為是一大堆字母、數(shù)字、控制字符(如空格、回車等)、標(biāo)點符號等字符組成的集合。這里的字符是“抽象”的,與字形是不一樣的,比如“A”字符,用微軟雅黑是長這樣,用宋體又是另外一個樣了。
(2)編碼字符集(CCS:Coded Character Set)
如果將抽象字符庫中每一個字符都映射成非負(fù)整數(shù)(對)的話(如Unicode“A”-> 65,GB2312的 '啊' -> (16,01)),那么這些映射的集合就稱之為編碼字符集(也有直接稱字符集)。用于映射的非負(fù)整數(shù)的范圍,則被稱之為編碼空間(encoding space),編碼空間中的一個位置(position)稱為碼位(code point),例如在ASCII編碼字符集中,字符 'A' 的碼位值為65。編碼字符集也可以理解成把抽象字符映射為碼位值。簡單理解就是字符與整數(shù)的映射。
另外,編碼空間可以用一個整數(shù)來描述,例如:ISO-8859-1的編碼空間是256;可以用一對整數(shù)來描述,例如:GB2312的漢字編碼空間是94 x 94;也可以用字符的存儲單元尺寸來描述,例如:ISO-8859-1是一個8-bit的編碼空間;還可以用其子集來表述,如行、列、面(plane)等。但不管如何描述,字符在編碼空間內(nèi)都有對應(yīng)的碼位值(一個非負(fù)整數(shù))。
(3)字符編碼模式(CEF:Character Encoding Form)
非負(fù)整數(shù)確定后,這時候就需要把它映射成二進制的序列了。這個映射往往不能簡單地映射,例如整數(shù) 2->[10] 8->[1000],因為很明顯,當(dāng)解碼[1000] 時,到底是 [1000] 還是 [10] + [00]呢?但是假如我定好了4-bit是一個整體(基本單元),這時 2->[0010] 8->[1000],就很好分辨了。這里所說以個整體就是字符編碼模式我們常說的碼元(code unit),表示用于處理或交換編碼文本時的基本單元,碼元的長度是確定的,比如這里的4-bit、ISO 8859-1的8-bit、UTF-8的8-bit、UTF-16的16-bit等。
字符編碼模式(CEF,也有翻譯為字符編碼表),也稱為"storage format",規(guī)定了從CCS 中的非負(fù)整數(shù)到一組特定碼元序列的映射。碼元序列由一個多或多個碼元組成。
根據(jù)碼元組成的序列長度是否可變,CEF可以區(qū)分為兩大類:
- 固定寬度 (fixed width) 編碼模式:字符由一個碼元確定,碼元序列就是CCS中整數(shù)的對應(yīng)碼元長度的二進制(不夠則前面補0),如UCS-2、UCS-4、UTF-32等
- 可變寬度 (variable width) 編碼模式:碼元的數(shù)量可以有多個,如UTF-8、UTF-16等
我們常說的GBK編碼、UTF-8/16編碼等,通常指的就是這一層干的活。
(4)字符編碼方案(CES:Character Encoding Scheme)
字符編碼方案(CES),也稱作"serialization format",將一個個CEF中的碼元映射到字節(jié)(8-bit)序列,以便編碼后的數(shù)據(jù)的文件存儲或網(wǎng)絡(luò)傳輸。這時侯時真正將二進制序列放入到硬件中了。
在Unicode的相關(guān)編碼方案中,當(dāng)一個字符對應(yīng)的碼元序列為多個字節(jié)時,根據(jù)字節(jié)存儲的順序可以分為大端序(big-endian)和小端序(little-endian)。例如字符'我'在UTF-16中對應(yīng)的碼元序列為[01100010 00010001],當(dāng)采取大端序存儲時,碼元序列的高字節(jié)放在存儲器的低地址中,存儲器從低地址到高地址順序讀取時,讀取到的第一個字節(jié)為[01100010],第二個為[00010001];而采用小端序時,則反過來了,數(shù)據(jù)的低字節(jié)放在低地址中,順序讀取時第一個字節(jié)為[00010001],然后再是[01100010]。
為了告訴計算機是到底是大端序還是小端序,還需要在編碼后的字節(jié)流的開頭指定一個字節(jié)順序標(biāo)記(byte-order mark,BOM),0xFEFF表示大端,0xFFFE表示小端。
2.編碼的歷史
字符編碼的歷史,其實就是一個在不斷制定和更新標(biāo)準(zhǔn)的過程。范圍從英文到歐洲字符、漢字乃至全世界的文字和符號。
2.1 初出茅廬ASCII
上世紀(jì)60年代,美國制定了一套基于拉丁字母的編碼標(biāo)準(zhǔn),即ASCII(American Standard Code for Information Interchange,美國信息交換標(biāo)準(zhǔn)代碼),ASCII字符集共收錄了95個顯示字符(英文字母、數(shù)字、一般的符號)和33個控制字符,共128個字符,至少7bit來表達所有字符,即000 0000-111 1111,十進制為0-127,例如'A' 對應(yīng)的碼位值為65。因為ASCII的普及范圍廣,后來的編碼字符集一般都會對ASCII做兼容。
值得一提的是,“字節(jié)”最初的長度表示用于編碼單個字符所需要的比特數(shù)量,曾基于硬件為1-48比特不等,而直到二十世紀(jì)70年代開始,1字節(jié)才被標(biāo)準(zhǔn)化為8比特(某硬件的流行?)。正是因為這樣,現(xiàn)代的字符編碼(UTF-8、GBK等),碼元的長度通常為8的整數(shù)倍。ASCII用單個字節(jié)存儲的時,最開始最高位用于校驗,后來則用作了ASCII編碼拓展。
2.2 歐洲發(fā)展EASCII與ISO 8859
伴隨著歐洲強國對計算機的引入和發(fā)展,各個廠商或者是國家為了滿足自己的需求,均推出了自己的一套EASCII編碼(Extended ASCII,延伸美國標(biāo)準(zhǔn)信息交換碼),其中比較出名的有IBM PC的Code page 437。注意:EASCII指的是對ASCII空置的 [1000 0000-1111 1111] 進行拓展的那一類編碼,而不是特指哪一個。正因為這樣,當(dāng)時的編碼格局顯得有些混亂。
為了統(tǒng)一ASCII的相關(guān)拓展編碼,國際標(biāo)準(zhǔn)化組織(ISO)及國際電工委員會(IEC)聯(lián)合制定的一系列8位元(單字節(jié))字符集的標(biāo)準(zhǔn),定義了共15個字符集。分別是:
- ISO/IEC 8859-1 (Latin-1) - 西歐語言
- ISO/IEC 8859-2 (Latin-2) - 中歐語言
- ISO/IEC 8859-3 (Latin-3) - 南歐語言。世界語也可用此字符集顯示。
- ISO/IEC 8859-4 (Latin-4) - 北歐語言
- ISO/IEC 8859-5 (Cyrillic) - 斯拉夫語言
- ISO/IEC 8859-6 (Arabic) - 阿拉伯語
- ISO/IEC 8859-7 (Greek) - 希臘語
- ISO/IEC 8859-8 (Hebrew) - 希伯來語(視覺順序)
- ISO 8859-8-I - 希伯來語(邏輯順序)
- ISO/IEC 8859-9(Latin-5 或 Turkish)- 它把Latin-1的冰島語字母換走,加入土耳其語字母
- ISO/IEC 8859-10(Latin-6 或 Nordic)- 北日耳曼語支,用來代替Latin-4
- ISO/IEC 8859-11 (Thai) - 泰語,從泰國的 TIS620 標(biāo)準(zhǔn)字集演化而來
- ISO/IEC 8859-13(Latin-7 或 Baltic Rim)- 波羅的語族
- ISO/IEC 8859-14(Latin-8 或 Celtic)- 凱爾特語族
- ISO/IEC 8859-15 (Latin-9) - 西歐語言,加入Latin-1欠缺的芬蘭語字母和大寫法語重音字母,以及歐元(€)符號
- ISO/IEC 8859-16 (Latin-10) - 東南歐語言。主要供羅馬尼亞語使用,并加入歐元符號
這時歐洲幾乎所有國家的字符都有其對應(yīng)的編碼標(biāo)準(zhǔn)了(盡管你只能選擇其中一個)。
加入了96個西歐字母及符號的ISO 8859-1(Latin-1),因為標(biāo)準(zhǔn)化早且覆蓋區(qū)域廣,成為了歐美十分流行的編碼,mysql、tomcat等軟件的一些早期版本默認(rèn)的編碼就是ISO-8859-1。
套于編碼模型上,EASCII和ISO 8859編碼空間均為8bit,碼元長度8-bit。因為是定長、單字節(jié)存儲,編碼十分簡單。如 'A' -> 碼位值為 [110 0101] ,碼元序列 8-bit 固定長度,前面補個0得:[0110 0101],實際存儲沒那么多花哨的東西,直接存儲單個字節(jié) [0110 0101] 即可。
2.3 中國崛起GB2312與GBK
GB2312起源
就如最初的歐洲一般,我國也是沒有自己文字的編碼,直到1980年,中國國家標(biāo)準(zhǔn)總局發(fā)布中華人民共和國國家標(biāo)準(zhǔn)簡體中文字符集,即GB 2312(GB即"國標(biāo)"拼音首聲母)。GB 2312標(biāo)準(zhǔn)共收錄6763個漢字,其中一級漢字3755個,二級漢字3008個;同時收錄了包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母在內(nèi)的682個字符。為了裝下這幾千個字符,GB2312字符均用雙字節(jié)編碼。
分區(qū)
GB2312編碼對字符進行了“分區(qū)”處理,共94個區(qū),每個區(qū)有94個漢字/符號。區(qū)號和位號組成一個區(qū)位碼(碼位):
- 01~09區(qū)(682個):特殊符號、數(shù)字、英文字符、制表符等,包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西里爾字母等在內(nèi)的682個全角字符;
- 16-55區(qū)(3755個):一級漢字,按拼音排序。
- 56-87區(qū)(3008個):二級漢字,按部首/筆畫排序。
- 10-15區(qū)、88-94區(qū):空區(qū),留待擴展。
比如 '啊' 是GB2312編碼字符集中的第一個一級漢字,區(qū)位碼為(16, 01),十六進制為:0x1001,二進制:[00010110 00000001]。
國標(biāo)碼
GB2312字符集中并沒有定義控制字符,而是沿用了ASCII中的那32個控制字符,而這時候就出現(xiàn)了個問題了,如果直接使用,比如"啊"的區(qū)位碼作為二進制序列的話為:[00010000 00000001],而 [00010000] 在ASCII表示DLE(跳出數(shù)據(jù)通訊),這時候軟件解析時就不知道是取單個字節(jié)的DLE還是雙字節(jié)的"啊"了。
GB2312采用的辦法是將字符對應(yīng)的區(qū)位碼均偏移32(每個字節(jié)大小加上32、即0x20),解析時,只要是小于等于32的字節(jié)則認(rèn)為是ASCII控制字符,否則為GB2312的雙字節(jié)字符。這個區(qū)位碼+32的碼就是我們常說的“國標(biāo)碼/交換碼”,比如'啊'的國標(biāo)碼=0x1001+0x2020=0x3021。
機內(nèi)碼
國標(biāo)碼發(fā)布之后,我們又發(fā)現(xiàn)了一個問題。盡管GB2312定義了英文字母和數(shù)字,但是這些都是和漢字等大的字母和數(shù)字(全角),如果有一篇文章的字母既有全角又有半角的話,國標(biāo)碼就不能滿足了。國標(biāo)碼只是解決了控制字符的沖突問題,卻沒有解決其他ASCII字符沖突問題。
后來微軟在搞Windows中文版本時,索性在國標(biāo)碼的基礎(chǔ)上,最高位都取1,這時候就和ASCII就都兼容了,這個這就是我們常說的“機內(nèi)碼”。機內(nèi)碼=國標(biāo)碼+0x8080 [1000 0000]=區(qū)位碼+0x2020 + 0x8080=區(qū)位碼+0xA0A0,例如 '啊' 的機內(nèi)碼=0x3021+0x8080=0x1001+0xA0A0=0xB0A1。當(dāng)字節(jié)小于128 [1000 0000]時,直接取單個字節(jié)解析為一個ASCII字符;大于等于128的,取兩個字節(jié)組成一個GB2312字符。例如“A啊”在存儲器中的十六進制為“41 B0 A1”,共三個字節(jié)。這種儲存方法也被稱之為EUC-CN(Extended Unix Code CN,一個使用8位編碼來表示字符的方法,主要是為了兼容ASCII)。94區(qū)機內(nèi)碼的范圍為0xA1-0xFE。
GB 2312的出現(xiàn),基本滿足了漢字的計算機處理需要,它所收錄的漢字已經(jīng)覆蓋中國大陸99.75%的使用頻率。然而對于人名、古漢語等方面出現(xiàn)的罕用字和繁體字,GB 2312并不支持,直到后來的GBK出現(xiàn),這些問題才得以解決。
GBK
1995年12月,中華人民共和國全國信息技術(shù)標(biāo)準(zhǔn)化技術(shù)委員發(fā)布了漢字內(nèi)碼擴展規(guī)范,即GBK(K為"擴展"拼音第一個聲母),GBK共收錄21886個漢字和圖形符號,其中漢字21003個,圖形符號883個。
GBK是完全兼容GB2312編碼的(例如'啊'機內(nèi)碼均是'B0A1'),而拓展的可能:
(1)GB2312原本未編碼的10-15、88-94區(qū);
(2)當(dāng)初取最高位1時(+[1000 0000]),是在國標(biāo)碼的基礎(chǔ)上的,事實上,我們可以直接繞過國標(biāo)碼這個過時的玩意(或者說繞過EUC-CN的存儲方式),直接在區(qū)位碼最高位取1(區(qū)位碼+[1000 0000],當(dāng)然原來的國標(biāo)碼最高位取1依然需要兼容),原本0xA1作為機內(nèi)碼起始點,現(xiàn)在可以從0x81開始了(0x81-0xFE共126項)。而當(dāng)?shù)谝粋€字節(jié)確定非ASCII字符時,第二個字節(jié)則沒必要從0x80[1000 0000]開始了。這時候就有足夠的編碼空間了。
除了漢字,還有韓文、日文等均有自己國家的編碼標(biāo)準(zhǔn)(通常是雙字節(jié)編碼),這時候似乎又回到了歐洲編碼EASCII混亂的格局了。
2.4 一統(tǒng)江湖Unicode
unicode起源
為了統(tǒng)一全世界字符編碼,解決各種編碼的相互沖突的問題,20世紀(jì)80年代末,在電腦普及和資訊國際化的前提下,一些商業(yè)機構(gòu)以及國際標(biāo)準(zhǔn)化組織(ISO)分別成立了Unicode組織和ISO-10646工作小組。他們不久便發(fā)現(xiàn)對方的存在,大家為著相同的目的而工作。1991年,Unicode 組織與ISO/IEC委員會同意保持Unicode碼表與ISO 10646標(biāo)準(zhǔn)保持兼容并密切協(xié)調(diào)各自標(biāo)準(zhǔn)近一步的擴展。雖然實際上兩者的字集編碼相同,但實質(zhì)上兩者確實為兩個不同的標(biāo)準(zhǔn)。Unicode 1.1對應(yīng)于ISO 10646-1:1993,Unicode 3.0對應(yīng)于ISO 10646-1:2000,Unicode 3.2對應(yīng)于ISO 10646-2:2001,Unicode 4.0對應(yīng)于ISO 10646:2003,Unicode 5.0對應(yīng)于ISO 10646:2003及附錄1–3[1]。可以認(rèn)為unicode標(biāo)準(zhǔn)下的unicode字符集和ISO-10646標(biāo)準(zhǔn)下的通用字符集(Universal Character Set, UCS)是名稱不同但是實際內(nèi)容差不多的東西。
unicode字符集,通常指的是編碼模型中的第一二層,就是收錄字符(囊括了英文、歐洲字符、中文、日文、韓文等)以及每個字符都賦予一個對應(yīng)的非負(fù)整數(shù),每個字符可以命名為U+{對應(yīng)非負(fù)整數(shù)的十六進制},如“我”-> U+6211。另外emoji等表情符號在unicode中也有對應(yīng)的碼位。
unicode字符集依然兼容ascii,即在ascii中的那128個字符對應(yīng)的整數(shù)與unicode中是一致的,比如“A”都是65。
基本平面與輔助平面
如果說GBK字符集用分區(qū)來表示編碼空間的話,那么unicode字符集則可以叫“分面”了。unicode的編碼空間劃分為17個平面(plane),每個平面包含65,536(216)個碼位。17個平面的碼位可表示為從U+xx0000到U+xxFFFF,其中xx表示十六進制值從00到10(十進制就是0-16),共計17個平面。其中字符使用頻率最高的是第一個平面,即基本多語言平面(Basic Multilingual Plane, BMP),或稱第零平面(Plane 0),其他平面稱為輔助平面(Supplementary Planes)?;径嗾Z言平面內(nèi),從U+D800到U+DFFF之間的碼位區(qū)段是永久保留不映射到Unicode字符,UTF-16就利用保留下來的0xD800-0xDFFF區(qū)段的碼位來對輔助平面的字符的碼位進行編碼(詳見下文)。
UCS-2和UTF-16
UCS-2表示通用字符集下的一種編碼模式實現(xiàn)方式,把UCS中的碼位映射成碼元長度為2字節(jié)(16-bit)的碼元序列,UCS-2是一個定長的編碼,即碼元個數(shù)永遠都是1個(2字節(jié))。
UTF-16(Unicode Transformation Format, unicode轉(zhuǎn)換格式),表示Unicode字符集下的一種編碼模式實現(xiàn)方式,把unicode字符集中的碼位映射成成碼元長度為16-bit(2字節(jié))的碼元序列,UTF-16是一個變長的編碼,即單個字符對應(yīng)的碼元個數(shù)可以為1或2,即最終的序列長度可以為2字節(jié)或4字節(jié)。
最初的unicode字符集/UCS編碼空間并不大,unicode到了3.0版本時(1999年9月)也就49,259個字符,雙字節(jié)共216=65,536項,用于存儲這個字符集綽綽有余。而因為UCS和unicode字符集的對應(yīng)關(guān)系,UTF-16可看成是UCS-2的父集。在unicode編碼空間尚未突破216之前(即輔助平面還沒出來之前),UTF-16與UCS-2可以認(rèn)為是同一個編碼。
最初可能大家都捧著“65535 個字符足夠全世界用了”的觀念,為許多編程語言(如C/java等)和流行軟件(如Mysql)留下了不少的坑。
伴隨著unicode字符集的擴展,自unicode3.1版本開始(2001年3月),編碼空間突破65,536,雙字節(jié)已經(jīng)存不下了。這時候UTF-16順利成章地用上了兩個碼元(即2字節(jié)*2=4字節(jié))。而UCS-2則只能對UCS/unicode字符集較前的部分字符進行編碼。后來為了編碼所有的字符,ISO-100646推出了UCS-4編碼(等同于unicode的UTF-32),即碼元長度為4字節(jié)/32-bit(定長)。
UTF-16 基本平面編碼
在基本平面中,即U+0000至U+FFFF中,除開U+D800~U+DFFF這段無映射字符的區(qū)間,其他均被直接映射到單個碼元(16-bit)的序列。例如“我”-> U+6211,被映射為二進制序列 [0110 0010 0001 0001]
UTF-16 輔助平面編碼
為了區(qū)分字符是由單個碼元還是兩個碼元表示,對于輔助平面上的編碼,UTF-16使用兩個U+D800~U+DFFF區(qū)間的碼元表示一個字符。
以“??”(U+24B62)為例,說說碼位到碼元序列的映射過程:
- 0x24B62-0x10000=0x14B62,結(jié)果二進制為5個4位共20位:[0001 0100 1011 0110 0010]
- 將這20位分成兩個10位,即0001 0100 10 和 11 0110 0010,十六進制分別為0x52和0x362
- 前10位+0xD800=0x52+0xD800=0xD852,作為序列中的第一個碼元(高位);
- 后10位+0xDC00=0x362+0xDC00=0xDF62,作為序列中的第一個碼元(低位);
- 兩個碼元組成了最終的一對碼元序列,即 “D852 DF62”,也就是代理對(surrogate pair),高位代理為前導(dǎo)代理(lead surrogates),低位代理為后尾代理(trail surrogates)。
解碼時反著來就行。
UTF-8
UTF-16的碼元長度為16-bit(兩個字節(jié)),一個字符可以由一到兩個碼元組成;
UTF-8的碼元長度則為8-bit(一個字節(jié)),一個字符可以由一至六個碼元組成,具體字符與字節(jié)的關(guān)系如下:
- 128個ASCII字符只需一個字節(jié)編碼(Unicode范圍由U+0000至U+007F)。
- 帶有附加符號的拉丁文、希臘文、西里爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則需要兩個字節(jié)編碼(Unicode范圍由U+0080至U+07FF)。
- 基本平面中的其余的字符(如大部分的漢字)使用三個字節(jié)編碼(Unicode范圍由U+0800至U+FFFF)。
- 其他極少使用的Unicode 輔助平面的字元使用四至六個字節(jié)編碼(Unicode范圍由U+10000至U+1FFFFF使用四字節(jié),Unicode范圍由U+200000至U+3FFFFFF使用五字節(jié),Unicode范圍由U+4000000至U+7FFFFFFF使用六字節(jié))。
UTF-8的優(yōu)勢主要在于一個是和UTF-16一樣,可以編碼所有的unicode字符;二是ASCII字符僅需要占用一個字節(jié),對于以英文數(shù)字為主的數(shù)據(jù)可以省50%的空間(相對于UTF-16)。但是對于漢字字符來說,會比GBK多50%的大?。ú贿^GBK對于一些特殊字符,例如目前流行的emoji表情符號就不支持了)。
UTF-8 編碼規(guī)則
UTF-8會根據(jù)第一個字節(jié)的“情況”來區(qū)分多少個碼元(字節(jié))表示一個字符。具體的規(guī)則如下:
(1)單個字節(jié),首字節(jié)最高位為0,剩余的7-bit為可編碼位數(shù),unicode的0x00-0x7F的碼位可以映射到這里。
(2)多個字節(jié),首字節(jié)最高位開始,連續(xù)的二進制位值為1的個數(shù)就是其編碼的字節(jié)數(shù),其余各字節(jié)均以10開頭。未被指定的其他位會對Unicode碼位進行編碼映射(如下表的"x")
| 可編碼位數(shù) | 碼位范圍 | 字節(jié)數(shù)量 | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
|---|---|---|---|---|---|---|---|---|
| 7 | U+0000~U+007F | 1 | 0xxx xxxx | |||||
| 11 | U+0080~U+07FF | 2 | 110x xxxx | 10xx xxxx | ||||
| 16 | U+0800~U+FFFF | 3 | 1110 xxxx | 10xx xxxx | 10xx xxxx | |||
| 21 | U+10000~U+1FFFFF | 4 | 1111 0xxx | 10xx xxxx | 10xx xxxx | 10xx xxxx | ||
| 26 | U+200000~U+3FFFFFF | 5 | 1111 10xx | 10xx xxxx | 10xx xxxx | 10xx xxxx | 10xx xxxx | |
| 32 | U+4000000~U+7FFFFFFF | 6 | 1111 10xx | 10xx xxxx | 10xx xxxx | 10xx xxxx | 10xx xxxx | 10xx xxxx |
例如,“我”-> U+6211,二進制為[0110 0010 0001 0001],處于三字節(jié)編碼處,將其填充至對應(yīng)的"x"位置即可:[1110 0110 1000 1000 1001 0001],對應(yīng)的十六進制序列為 "E6 88 91"共三個字節(jié)。
UTF-8并不像UTF-16那樣有區(qū)分大小端序,BOM(字節(jié)順序標(biāo)記)有無并不會影響字符對應(yīng)的二進制序列。UTF-8中,無論有BOM和無BOM,“我”都是對應(yīng)“E6 88 91”,只不過有BOM在前面多加個標(biāo)記符號(“EF BB BF”)而已。
2.5關(guān)于Base64與Url編碼
Base64編碼、Url編碼和UTF-8/GBK這些不屬于一個編碼層次。前者是把UTF-8/GBK編碼后的字節(jié)序列,再進行一次映射,形成新的二進制序列,以滿足傳輸環(huán)境的限制(見下文)。
Base64
早期email的傳輸和解析只需要滿足英文就行了,也沒考慮說其他一些復(fù)雜的東西。郵箱相關(guān)的一些協(xié)議(如SMTP),往往是基于純ASCII文本的(ASCII編碼),傳輸過程中對于二進制文件或者GB2312/UTF-8等編碼后的二進制序列并不支持。如果你非要發(fā)這種圖片、視頻、GB2312編碼后的中文等,這就得先把原來的二進制序列轉(zhuǎn)換為ASCII字符(ASCII編碼)對應(yīng)的二進制序列,這樣才能保證傳輸過程中不會出現(xiàn)問題(比如GB2312的首字節(jié)被ASCII取7-bit解析為終止字符等)
Base64是一種基于64個可打印字符來表示二進制數(shù)據(jù)的表示方法,這64個字符分別是:A-Z,a-z,0-9,+,/。原本若干個字節(jié)的二進制序列,每6-bit作為一個整體(26=64),映射到對應(yīng)的可打印字符中。其映射關(guān)系為:【0-25 -> 'A'-'Z'】【26-51 -> 'a'-'z'】【52-61 -> '0'-'9'】【62 -> '+'】【63 -> '/'】,映射后的字符也就可以使用ASCII編碼了。
假如有個三字節(jié)序列:[0000 1111 0000 1111 0000 1111] 進行Base64編碼:
(1)以6-bit進行切分得到:[0000 11] [11 0000] [1111 00] [00 1111],即 3、48、60、15;
(2)找到對應(yīng)的可打印字符:D、w、8、P
(3)對字符進行ASCII編碼,得新的二進制序列:[0100 0100] [0111 0111] [0011 1000] [0101 0000]共四個字節(jié)
原本3字節(jié)24-bit的數(shù)據(jù),現(xiàn)在變成了 24/6=4個字節(jié)大小了,Base64編碼后傳輸大小為原來的4/3了;
如果原數(shù)據(jù)的字節(jié)數(shù)不能被3整除(位數(shù)不能被6整除),需要用0補足字節(jié),補上的全0映射至'='字符。例如:
(1)單字節(jié) [0000 1111],補充到三字節(jié)[0000 1111] [0000 0000][0000 0000],切分成 [0000 11] [11 0000][0000 0000] [0000 0000],即 映射成 'D'、'w'、'='、'='。
(1)雙字節(jié) [0000 1111 0000 1111],補充到三字節(jié)[0000 1111 0000 1111][0000 0000],切分成 [0000 11] [11 0000][1111 00] [00 0000],即 映射成 'D'、'w'、'8'、'='。
這也是為什么我們經(jīng)常能看到Base64字符串會以'='結(jié)尾的原因。
Base64目前在Web中的應(yīng)用場景通常是對于圖片的編碼,例如圖片的展示,或者上傳圖片時可以直接連同其他數(shù)據(jù)一起提交至服務(wù)器,服務(wù)器以接收普通字符一樣接收圖片。
Url編碼
Url編碼(URL encoding),也稱百分號編碼(Percent-encoding),是指特定上下文的統(tǒng)一資源定位符 (URL)的編碼機制。URI所允許的字符分為保留字符:【!*'();:@&=+$,/?#[]】和非保留字符:【=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.~】。保留字符是有特殊含義的,例如https://wikipedia.org中的'/'用于URL不同部分的分界符。
對于除了非保留字符之外的字符,均要使用百分號編碼(即由若干個 '%XX'組成),其中保留字符對應(yīng)的百分號編碼為:
| 字符 | 百分號編碼 | 字符 | 百分號編碼 |
|---|---|---|---|
| ! | %21 | + | %2B |
| # | %23 | , | %2C |
| $ | %24 | / | %2F |
| & | %26 | : | %3A |
| ' | %27 | ; | %3B |
| ( | %28 | = | %3D |
| ) | %29 | ? | %3F |
| * | %2A | @ | %40 |
| [ | %5B | ] | %5D |
其他的例如漢字,則是進行指定的編碼后(如UTF-8、GBK編碼),再轉(zhuǎn)換為百分號編碼。例如'我',UTF-8編碼后為二進制序列十六進制為“E6 88 91”,對應(yīng)的%編碼即為 “%E6%88%91”。
用上百分號編碼的地方:
- url的 path,如https://zh.wikipedia.org/wiki/我,實際上請求為:https://zh.wikipedia.org/wiki/%E6%88%91;
- application/x-www-form-urlencoded類型數(shù)據(jù)請求。GET請求時,如https://www.baidu.com/s?wd=我,實際為:https://www.baidu.com/s?wd=%E6%88%91;POST請求時,在請求體中的數(shù)據(jù)也會被瀏覽器進行URL編碼。
不同的瀏覽器,不同的頁面,可能進行的編碼(可能為UTF-8或者為GBK),可以參考這篇文章,注意:處理實際問題時,不同瀏覽器或版本可能不一樣,具體問題具體分析。
3 編碼選擇
這么多種編碼用哪個?我的建議是:對于一般業(yè)務(wù)數(shù)據(jù)的存儲和傳輸,UTF-8一把梭就完事了;對于我國環(huán)境下,GBK仍然是個不錯的選擇,前提是容忍部分字符的缺失(如emoji,會出現(xiàn)亂碼);在一些需要定長的場景下(例如后來的一些編程語言的字符),可以選擇UTF-32/UCS-4。
值得注意的是,各類編碼本身的定義是一回事,軟件是否支持又是另外一回事了,一定要結(jié)合具體的場景討論編碼問題。比如Mysql 5.5.3之前的版本的utf8編碼最大只支持3個字節(jié)的字符編碼,emoji都存不了。直到后面出了個utf8mb4才支持到最大四個字節(jié)。