HTML與javascript中常用編碼淺析
在日常的前端開發工作中,我們會經常的與HTML、javascript、css等語言打交道,和一門真正的語言一樣,計算機語言也有它的字母表、語法、詞法、編碼方式等,在這裡我簡單的談一下前端HTML與javascript日常工作中常碰到的編碼問題。
在計算機中,我們儲存的資訊都是用二進位制碼錶示的。我們認識的、螢幕上顯示的英文、漢字等符號和儲存用的二進位制程式碼的互相轉換,就是編碼。
有兩個基本概念需要說明,charset 和 character encoding:
charset ,字符集,也就是某個符號和某個數字對映關係的一個表,也就是它決定了107 是koubei 的 ‘a’,21475 是口碑的“口”,不同的表有不同的對映關係,如 ascii,gb2312,Unicode. 通過這個數字和字元的對映表,我們可以把一個二進位制表示的數字轉換成某個字元。
chracter encoding
對於 ‘koubei.com’ 這樣的 字串來說,是美國人的常用字元,他們就制定了一個 叫做ASCII 的字符集,全稱是 american standard code of information interchange 美國標準資訊交換碼,用0–127這128個數字,(2的7次方,0×00-0×7f) 代表了123abc這樣的常用的128個字元。一共是 7 bits,再加上第一個是符號位,要用來去補碼反碼錶示負數什麼的,一共8 bits 構成一個 byte。當年美國人就是小氣了點,要是一開始就設計成一個 byte 是16 bits、32 bits,世界上會少很多問題,不過當時,估計他們覺得 8 bits 就夠了,可以表示128個不同的字元呢!
介於計算機這玩意兒是美國人搞出來的,所以他們自己省事,把自家用的符號都編碼好了,用的挺爽的。但當計算機開始國際化的時候,問題出來了,拿中國舉例吧,漢字就好幾萬,怎麼辦?
現有的 8 bits 一個 byte 的系統是基礎,不能破壞,不能去改到 16 bits之類的,否則改動太大了,只能走另一條路:用多個 ascii 的字元去表示一個其他字元,也就是 MBCS ( Multi-Byte Character System,多位元組字元系統)。
有了這個 MBCS 的概念,我們可以表示更多個字元了,比如我們用 2 個 ascii 字元,就有 16 bits, 理論上有 2 的 16 次方 65536 個字元。但這些編碼怎麼分配到字元上呢?比如口碑的”口”的 Unicode 編碼就是 21475,誰決定的呢?字符集,也就是剛剛介紹的charset。ascii就是最基礎的一個字符集,在此之上,我們有類似於 gb2312, big5這樣針對簡體中文和繁體中文的MBCS的字符集等等。終於有個叫 Unicode Consortium 的機構,決定做一個囊括所有字元在內的字符集(UCS, Universal Character Set)和對應編碼方式的標準,即 Unicode。從1991年開始,它釋出了第一版 Unicode 國際標準,ISBN 0-321-18578-1 ,國際標準化組織 ISO 也參與了這個的定製,ISO/IEC 10646 : the Universal Character Set。總之,Unicode 是個基本覆蓋了所有已經存在的地球上的符號的字元標準了,現在正在被越來越廣泛的使用,ECMA 標準也規定,javascript語言的內部字元使用 Unicode 標準(這意味著,javascript的變數名、函式名等是允許中文的!)。
對於身在中國的開發者來說,可能碰到比較多的問題就是 gbk, gb2312, utf-8 之間轉換之類的問題了。嚴格的說這個說法不是很準確,gbk,gb2312是字符集 (charset),而 utf-8 是一種編碼方式 (character encoding) ,是 Unicode 標準中 UCS 字符集的一種編碼方式,因為使用 Unicode 字符集的網頁主要用UTF-8編碼,所以大家常常就把它們並列了,其實是不準確的。
有了 Unicode 後,至少人類文明沒有碰到外星人之前,這是一把萬能鑰匙了,都用它吧。而現在使用最廣泛 Unicode 的編碼方式就是 UTF-8 (8-bit UCS/Unicode Transformation Format) 了,它有幾個特別好的地方:
- 編碼 UCS 字符集,全世界通用
- 是一種變長編碼方式(variable-length character encoding),相容 ascii
第二點是個很大的優點,它使得以前使用純 ascii 編碼的系統相容,而且不會增加額外的儲存量(假設定長的編碼方式,規定每個字元由2個 bytes 組成,那麼這時候 ascii 字元佔用的儲存空間將增大一倍)。
要把 UTF-8 說清楚,引入一個表會更方便了:
U-00000000 – U-0000007F: 0xxxxxxx
U-00000080 – U-000007FF: 110xxxxx 10xxxxxx
U-00000800 – U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
U-00010000 – U-001FFFFF: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
U-00200000 – U-03FFFFFF: 111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
U-04000000 – U-7FFFFFFF: 1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
要看懂這個表呢,我們看前兩行就夠了
U-00000000 – U-0000007F:
0xxxxxxx 第一行是這樣的,意思是說,如果你發現一個utf-8編碼的 byte 的二進位制碼是0xxxxxxx,是0開頭的, 即十進位制的0-127之間,那麼他就是單獨的這一 byte 代表一個字元,而且是擁有和 ascii 碼完全一樣的含義。其他所有的 utf8 編碼的二進位制值都是用1開頭的1xxxxxxx,大於127的,而且都需要至少2 bytes才能代表一個符號。所以一個位元組的第一位是一個開關,代表這個字元是不是一個 ascii 碼。這個就是剛才談到的相容性,從英文定義上看,就是utf8編碼的兩個屬性:
UCS characters U+0000 to U+007F (ASCII) are encoded simply as bytes 0×00 to 0×7F (ASCII compatibility). This means that files and strings which contain only 7-bit ASCII characters have the same encoding under both ASCII and UTF-8.
All UCS characters >U+007F are encoded as a sequence of several bytes, each of which has the most significant bit set. Therefore, no ASCII byte (0×00-0×7F) can appear as part of any other character.
然後我們看看第二行:
U-00000080 – U-000007FF: 110xxxxx 10xxxxxx
先看第一個位元組:110xxxxx,它的含義是,我不是一個 ascii 碼(因為第一位不為0),我是一個多 bytes 字元的第一個 byte (第二位為1),我參與表示的這個字元是由2個 bytes 組成的(第三位為0),從第四位開始,就是字元的資訊儲存的位置。
再看第二個位元組:10xxxxxx,它的含義是:我不是一個 ascii 碼(因為第一位不為0),我不是一個多 bytes 字元的第一個 byte (第二位為0),第三位開始是字元的資訊儲存的位置。
從這個例子中可以總結出來,utf-8編碼方式中,在一長串連續的二進位制 byte 碼中,可能由2個至6個 bytes 來表示一個符號,那麼相比較於用一個 byte 表示符號的 ascii 碼,我們需要空間來儲存兩個額外資訊: 一,這個符號開始位置,一個“starter”的位置,用生物學上的話來說,就是蛋白質翻譯時候起始密碼子AUG的位置了;二,這個符號使用的 bytes 數(其實如果每個符號都有 starter,這個長度是可以不提供的,但是提供長度資訊增加了在部分 bytes 丟失時的容錯能力)。解決方案是:用一個 byte 的第二位是否是1來代表這一 byte 是否是一個字元的起始 byte (因為一個 byte 裡面的第一位剛才已經被使用了,0表示ascii碼,1表示非ascii ),即,一個多位元組符號的第一 個bytes一定是 11xxxxxx,一個192到255之間的二進位制數。接下來,從第三位開始,提供長度資訊,第三位是0表示這個符號是2位元組的,第三位開始每多一個1,字元佔用的 bytes 數加一。utf-8 最多定義到了 6 位元組字元,需要比 110xxxxx 這樣的表示2位元組的starter多 4 個 1,所以這個starter就是 1111110x,如上表所示。
再看看英文定義的標準吧,表達的同樣的意思:
The first byte of a multibyte sequence that represents a non-ASCII character is always in the range 0xC0 to 0xFD and it indicates how many bytes follow for this character. All further bytes in a multibyte sequence are in the range 0×80 to 0xBF. This allows easy resynchronization and makes the encoding stateless and robust against missing bytes.
真正的資訊位(即,真正的charset字符集中的數字資訊),是直接用二進位制的方式,依順序放在上面這個表的’x'上的。用我們中國程式設計師接觸最多的漢字來說吧,它們的編碼區間是在 U-00000800 – U-0000FFFF 之間的,從上面的表中可以查到,這個區間的 utf-8 編碼是用三個位元組來表示的(這就是 utf-8 編碼的漢字會比每個字元佔用2 bytes的 EUC-CN 編碼的 gb2312 字符集的漢字使用更多儲存空間的原因),還是用 口碑的”口”字舉例吧,口字在 Unicode 中的編號是這樣的:
口: 21475 == 0×53e3 == 二進位制 101001111100011
在 javascript 中,run這段程式碼(使用 firebug 的 console,或者編輯一個HTML將下列程式碼插入一對 script 標籤之間):
alert(’/u53e3′); //get ‘口’
alert(escape(’口’)); // get ‘%u53E3′
alert(String.fromCharCode(’21475′)); // get ‘口’
alert(’口’.charCodeAt(0)); // get ’21475‘
alert(encodeURI(’口’)); //get ‘%E5%8F%A3′
可以看到,string直接量可以用/u+十六進位制 Unicode 碼的形式得到字元 ‘口’,而fromCharCode 方法接受 10 進位制的 Unicode 碼,得到字元 ‘口’。
其中第二個alert得到的是 ‘%u7545′ , 這是一種不標準的Unicode編碼,是屬於 URI 的 Percent encoding 一部分,但這種使用方法已經正式被 W3C 拒絕了,任何一個 RFC中都沒有這個標準,ECMA-262 標準中規定了 escape 的這種行為,估計也是暫時的。
比較有意思的是第五次alert得到的 ‘%E5%8F%A3′ 這是什麼呢?怎麼得到的呢?
這就是在URI上用的比較多的 Percent encoding,百分號編碼,RFC 3986 標準中規定的。
RFC 3986 規定,Percent encoding的非保留字如下:
Unreserved characters, per RFC 3986 (January 2005)
A B C D E F G H I J K L M N O P Q R S T U V W X Y Z
a b c d e f g h i j k l m n o p q r s t u v w x y z
0 1 2 3 4 5 6 7 8 9 - _ . ~
也就是說,這些字出現在 URI 中的時候,不進行編碼,因為他們和URI的格式沒有關係,只是表示原義的字元
另外,保留字如下:
Reserved characters, per RFC 3986 (January 2005)
! * ‘ ( ) ; : @ & = + $ , / ? % # [ ]
這些字元,是有特殊意義的,如果在不代表那些特殊意義而代表原意的時候出現,必須經過編碼,如下:
Reserved characters after percent-encoding
! * ‘ ( ) ; : @ & = + $ , / ? % # [ ]
%21 %2A %27 %28 %29 %3B %3A %40 %26 %3D %2B %24 %2C %2F %3F %25 %23 %5B %5D
而 % 號後面就是一個2位的十六進位制數,這個數,就是 Unicode 的 UTF-8 編碼的另一種表現形式。
讓我們詳細還原一下’口’ 字為什麼是 ‘%E5%8F%A3′ 吧。
剛才我們談到 ‘口’ 的 Unicode 編碼 21475 的二進位制形式是:
101001111100011
剛才我們又聊到,對於一個的漢字,它的UTF-8編碼的形式是:
U-00000800 – U-0000FFFF: 1110xxxx 10xxxxxx 10xxxxxx
現在我們做個填空題,把 ‘口’ 二進位制碼切開填進去替換掉 x:
101001111100011 = ----0101 --001111 --100011
101001111100011 = 1110xxxx 10xxxxxx 10xxxxxx
第一個位元組少一位,左邊加個0補齊,得到:
11100101 10001111 10100011
讓我們把這三個二進位制數轉換成16進位制,並且加上百分號,執行如下javascript程式碼:
alert(
‘%’ + parseInt(’11100101′, 2).toString(16) +
‘%’ + parseInt(’10001111′, 2).toString(16) +
‘%’ + parseInt(’10100011′, 2).toString(16)
) // get ‘%e5%8f%a3′
怎麼樣,得到 %e5%8f%a3 了吧。
另外javascript的內建函式 encodeURI、decodeURI、encodeURIComponent、decodeURIComponent 就是進行的 Percent Encode,只是在對待 : / ; ?等特殊字元的時候有區別。
另外,再介紹一下 HTML 中的 Numeric character reference, NCR編碼
相信大家都知道,HTML中的特殊字元是需要編碼的,比如 & 需要被編碼為: & 還有 ® 這樣的特殊字元。其實HTML也是可以利用 Unicode 編碼來顯示任何一個字元的,編輯一個如下的html檔案:
<html>
<body>
口
口
口
</body>
</html>
結果就是三個“口”字。
還有一種常用的編碼是 base64 編碼,base64編碼本來是為了在 email 這樣的非純 8-bit 的傳輸層傳輸二進位制資料而設計出來的,這樣就可以在 email 中傳遞二進位制的附件。它用 a-z A-Z 0-9 +/= 這64個字元來表示原有的資料,並且將連續的三個字元編碼為四個,長度增加33%。
這個編碼方式在一些比較超前的 javascript 應用中比較常用,例如 這個超級瑪麗遊戲 ,它裡面的音樂就是寫 javascript 檔案中的。例如 這個 利用 canvas 作圖的例子,裡面的頭像也是寫在 javascript 原始碼中的。這就是 RFC 2397 規定的 data URIs 協議,Firefox 瀏覽器支援,IE8也開始部分支援了,利用 data URIs 和 base64 編碼,我們可以不借助任何外來的音樂、影象等多媒體檔案而創造出豐富的效果。
以上就是我想介紹的 javascript 和 html 中常用到的編碼和原理,最後還想提到一句,很多的黑客行為都和編碼有關,用編碼後的程式碼來通過一些簡單的過濾,如下js程式碼:
var a = ‘口碑’;
/u0061 = ‘koubei.com’;
alert(a); //get ‘koubei.com’
當然,黑客們會有更專業的方式來逃避過濾、注入程式碼(如 sql injection, XSS 攻擊等)。
謝謝大家的閱讀,我是 stauren, 雅虎口碑UED團隊的前端開發工程師粽子,這是我第一次在Koubei的UED blog上發表文章,如果有錯誤的地方請大家指出。 並且提供線上 Hex、NCR、Percent encode、Base64編碼解碼工具:http://stauren.net/lab