關於Unicode和URL encoding入門的一切以及注意事項

本文同時也發表在我另外一篇獨立博客 《關於Unicode和URL encoding入門的一切以及注意事項》(管理員請注意!這兩個都是我本身的原創博客!不要踢出首頁!不是轉載!已經誤會三次了!)javascript

有感於,咱們天天用各類的編輯器,嘴裏喊着utf-8,BOM頭,gbk,encode,decode,卻鮮有人知道它們的由來和爲何這樣作(好吧,也有可能就我一我的不知道)。最近找了不少資料,在這裏作一個整理,和你們分享。php

第一部分:關於Unicode,UTF8,Character Sets的前生今世(原創譯文)
ASC II

總所周知計算機只能處理數字而不能處理字符,字符也老是使用數字進行表示,因此把哪些字符由哪些數字表示統一塊兒來(而不是每臺計算機有每臺計算機的標準)很是重要。html

好比說個人計算機用1表示A,2表示B,3表示C,諸如此類。而你的計算機用0表示A,1表示B,2表示C等等。那麼當我發送給你一條內容爲「HELLO」的消息時,數字8,5,12,12,15就經過光纜傳輸到了你那,但由於數字表示的字符不一樣,當你收到這串數字時,你會把它解碼(decode)成「IFMMP」。因此爲了有效的交流,咱們必須對如何編碼(encoding)這些字符達成一致。java

終於,在十九世紀60年代,美國標準協會(American Standards Association)發明了7位(7-bit)編碼方式,稱爲美國信息交換標準碼(American Standard Code for Information Interchange),就是咱們熟知的ASCII。在這種編碼標準下,HELLO表示的數字是72,69,76,76,79,而且會以二進制1001000 1000101 1001100 1001100 1001111的形式進行傳輸。7位編碼一共提供了128種可能,從0000000到1111111。那時全部的拉丁字符 的大小寫,通用的標點,縮進,空格和其它一些控制符都能在ASCII中有一席之地。在1968年,美國總統Lyndon Johnson宣佈全部的計算機必須使用和能讀懂ASCII標準,ASCII成爲官方標準。python

8-bit

電報一類的工具固然樂於使用七位編碼去傳輸信息,可是到了七十年代,計算機的處理器更樂意與2的次方打交道——他們已經能夠用8位來存儲字符,也就是說這將提供256種可能。mysql

一個8位字符被存儲的數字最大是255,可是ASCII標準最大隻到127。這就意味着從128到255被空了出來。IBM用這些多餘的數字來存儲一些形狀,希臘字母。好比200表明一個盒子的左下角╚,244表明小寫的希臘字母α。這種編碼方式的全部字符編碼都列在代碼頁(Code page)437中web

但不像ASCII標準,128至255的字符序列歷來都沒有被標準化過。不一樣國家都用本身的字母表來填充這些多餘的序列。不是所人都贊成224表明α,甚至希臘人本身都有分歧,由於在希臘另外一代碼頁737中,224表明小寫ω。這個時期至關數量的新的代碼頁出現了,好比俄羅斯的IBM計算機使用的是代碼頁885,224表明的是西里爾(Cyrillic)字母Яsql

即便在存在分歧的狀況下,十九世紀八十年代微軟也推出了本身的代碼頁,在西里爾文代碼頁Windows-1251中,224表明西里爾文字母a,而以前的Я的位置是223.數據庫

到了九十年代末期,你們作了一次標準化的嘗試。15種8位字符集被推出,涵蓋不一樣的字母表,好比西里爾文,阿拉伯文,希伯來文,土耳其文和泰文。它們被稱爲 ISO-8859-1 up to ISO-8859-16(字母12被拋棄)。在西里爾標準ISO-8859-5中,224表明字母р,而Я的位置是207.windows

若是一位俄羅斯朋友發給你一份文檔,你必須知道他使用的是哪種字符集。文檔自己只是數字序列而已,224表明的多是Я,a 或者р。若是用錯了字符集打開這份文檔,那會是很是糟糕的一件事。

給1990年作個總結

在1990年左右的狀況大體是這樣的,文檔能夠用不一樣的語言書寫,保存,而且在不一樣語言間交換。可是你必須得知道他們用的是哪種字符集。固然不可能在一份文檔中用多種語言。像中文和日文只能用徹底不一樣的編碼體系。

終於,互聯網出現了!國際化和全球化讓這個問題被放的更大,一個新的標準亟需出現。

Unicode來拯救人類了

從八十年代後期開始,一種新的標準已經被提出。它能給每一種語言中的每個字符賦予惟一的標識,固然遠遠大於256.它被稱爲Unicode,至今爲止它的版本是6.1,包括了超過110000個字符。

Unicode的頭128個字符與ASCII一致。128至255則包含了貨幣符號,經常使用符號,還有附加符號和變音符(accented characters)。大部分都是借鑑自ISO-8859-1。在編號256以後,還有更多的變音符。在編號880以後開始進入希臘文字符集,而後是西里爾文,希伯來文,阿拉伯文,印度語,泰文。中文,日文和韓文從11904開始,其中也夾雜了其餘的語言。

絕不含糊的說這的確是一件福利,每個字符都由屬於本身獨一無二的數字表示。西里爾文的Я永遠是1071,希臘文的α永遠是945.而224永遠是à,H仍然是72.注意官方書寫的Unicode編碼是以U+爲開頭的十六進制表示。因此H的正確寫法應該是U+48而不是72(把十六進制轉化爲十進制:4*16+8=72)

最主要的問題是超出256的那部分。想固然8位已經容不下這些字符了。而Unicode並非一個字符集或者是代碼頁。因此這也不是Unicode制定協會的問題。他們只是給出了一個解決問題的想法而剩下實際操做的問題則留給其餘人去辦了。下兩節咱們會討論這個問題。

瀏覽器中的Unicode

8位已經容不下Unicode了,甚至16也已經容不下,雖然只有110116個字符真正被使用,可是已經定義字符已經升至了1114112個,這就意味着至少須要21位

從七十年代開始計算機已經變得很是先進了。八位的處理器早就過期了。如今的計算機已經擁有64位的處理器,因此咱們爲何不能夠把超出8位容納範圍的字符轉移至32位或64位呢

咱們固然能夠這麼作!

大部分的軟件都是用C或者C++完成的,這兩種語言支持一種名爲"wide character"的數據類型。這種32爲的字符數據類型被稱爲wchar_t,它是對C語言的8位數據類型char的一種拓展。

從理論上來講,現代瀏覽器徹底可使用上面所說的這種字符類型,理論上它們就能夠毫無壓力的處理超過40億個徹底不一樣的字符,這對Unicode來講是莫大的喜訊——現代瀏覽器是可使用Unicode的

UTF-8又來拯救人類了

既然瀏覽器能夠應付32位的Unicode字符,那麼還有什麼問題?如今的瓶頸是,字符的傳輸和讀寫。

使用Unicode的障礙仍然存在是由於:

  • 至關數量的現存軟件和各類協議都只能傳輸和讀寫8位字符
  • 使用32位來存儲或發送英文內容會讓帶寬變爲原來的四倍

雖然瀏覽器內部對處理Unicode來講毫無壓力,但你仍需從服務器端獲取數據或者發送數據到服務器,你也須要把數據存在文件或者數據庫中。因此你仍然須要用8位來存儲110000個字符。

UTF-8用一種很睿智的方式辦到了。它的工做原理相似於鍵盤上的shift換擋鍵,通常來講你按下鍵盤上的H鍵打印出的是小寫的h,可是若是按住shift鍵的同時按下H鍵,你就能打印出大寫H

UTF-8把0至127留給ASCII,192至247做爲換擋鍵(key),128至192做爲被換擋的對象。舉個例子,使用字符208和209能切換進入西里爾文字符範圍,字符175後跟換擋鍵208對應符1071,也就是西里爾文中的Я,具體的計算方法是 (208%32)*64 + (175%64) = 1071。字符224至239是雙倍換擋鍵。字符190跟換擋鍵226,在加上字符128對應字符12160:⾀ 字符240和以後的換擋鍵是三倍換擋鍵。

所以UTF-8被稱爲多字節(multi-byte)可變寬度(variable-width)編碼,稱之爲多字節是由於好比Я這種的字符須要不止一個字節標識它,稱之爲可變寬度是由於一些像H這樣的字符可能須要1個字節也可能須要4個字節。

Unicode還有一個好處在於它向前兼容ASCII。不像其餘的一些解決方案,因此用純ASCII編碼的文檔,也都能轉爲有效的UTF-8編碼。這大大的解決了帶寬和轉化的麻煩。

給2003年作一個總結

UTF-8已經稱爲互聯網最受歡迎的國際字符集。但當你瀏覽一份非英語語言文檔時,你仍然須要知道它用的是什麼字符集。爲了能讓全部的網頁能最大範圍的流通,網頁全部者們必須保證他們的網頁使用的是UTF-8編碼。

許許多多的問題

若是每個人使用都是UTF-8編碼——很是好,天下太平。但事實並不是如此,這便致使了一些混亂,想象一下一個很是典型的操做,一個用戶給一篇博客添加評論:

  1. 網頁顯示一個供提交評論的表單
  2. 用戶填寫評論而且提交
  3. 評論被提交至服務器而且存在數據庫中
  4. 該評論可能會從數據庫中讀出來而且展示在頁面上

就這簡簡單單的流程會以不少方式走岔了,產生了如下這些問題:

HTML實體集(ENTITIES)

假設你對字符集一無所知,剛剛半小時看的東西全忘了。上面提到的那篇博客是用ISO-8859-1字符集顯示的,字符集不能識別俄語,泰文或者中文,只能識別小部分希臘文。若是你把任意拷貝來的東西粘貼而且提交,現代瀏覽器會嘗試把它們轉化爲HTML實體集,好比把Я轉化爲Я

轉化後的結果會存在數據庫中,當評論被顯示在網頁上時是沒有問題的,可是當你想把它導出爲PDF或者輸出在Email中,又或者在數據庫中進行搜索時,問題就來了。

讓人疑惑的字符

想象一下你有一個俄語網頁,可是你忘了給你的網頁指定字符集。有一個俄羅斯用戶使用的默認字符集是ISO-8859-5。他想打一聲招呼,"Hi",他會用鍵盤打出Привет,當這個用戶提交時,對字符的編碼是根據的是發送頁面的字符編碼。在這個例子中,Привет被編碼成的序列是191,224,216,210,213,226。這些數字會經過網絡傳輸到服務器,被存進數據庫中

若是其餘人也用ISO-8859-5字符集去查看這條評論,那麼他能看到正確的文本。可是若是其餘人使用的不一樣的俄羅斯字符集Windows-1251,他看的結果是їаШТХв,雖然仍然是俄文,可是已經徹底沒有意義了

英鎊和版權符號

在英國一個很是廣泛的問題是,英鎊符號£被轉成了£。英鎊標誌£在Unicode和ISO-8859-1中的代碼都是163.回憶一下在UTF-8中任何大於127的字符被表示是至少須要兩個數字。在這種狀況下,在UTF-8的序列應該是194/163,由於這個序列計算的結果(194%32)*64 + (163%64) = 163

這樣看來,若是你用ISO-8859-1字符集來閱讀UTF-8序列,它的前面必然會多一個Â,也就是ISO-8859-1中的194.一樣的事情也會發生在全部的Unicode序列爲161至191的字符上,包括©,®, ¥

帶有問號的黑鑽石

相反,若是把俄文Привет用ISO-8859-5來保存,他儲存的序列就是191,224等。若是你嘗試用UTF-8打開,你會發現不少帶有問號的黑鑽石�。不像其餘多字節(multi-byte)字符編碼,你老是能知道你在UTF-8的哪一個位置。當你看見一個數字介於192-247之間,你就知道你在一個多字節序列的頭,若是你看到的數字位於128-191之間,你就知道在一個多字節序列的中間。

但這也就意味着191後跟224這種狀況是不可能出現的,因而瀏覽器不知道該怎麼辦了,因而顯示了��。

這個問題也可能出如今£帶有©的詞中。在ISO-8859-1中 £50表明的數字序列是163,53和48。53和48都不會引發問題,可是163歷來都不會獨自出現,因而在UTF-8中就會顯示�50。同理若是你看見了�2012,八成是由於©2012在 ISO-8859-1中被輸入,而在UTF-8中顯示

空,問號和方形

即便你使用所有都是UTF-8編碼,瀏覽器也有可能出現不知如何正確一個字符的狀況,ASCII字符的1-31大多數是用來控制電報(teleprinters)傳送的(好比收到(Acknowledge )和中止(Stop)),若是你想嘗試顯示它們,瀏覽器可能會打印出一個問號,空或者一個帶有數字的方形

須要注意的還有,由於Unicode定義了超過110000個字符,你的瀏覽器不可能有全部的字體來顯示它們。有一些字體缺省的字符就會顯示問號,空或者方形。在一些古老的瀏覽器中,一般只要不是英文字符就會被顯示方形。

數據庫

上面的討論都忽略一個步驟,就是把數據存進數據庫中。相似MySQL的數據庫都能對數據庫,表,列指定字符集。可是這些指定都沒有對網頁指定字符集重要。

當從數據庫中讀和寫數據時,MySQL實際上只是和數字打交道。若是你告訴儲存163,它就這麼作。若是你給他208/159,那麼它就儲存這兩個數字。當你從數據庫讀數據時,你讀出的也是一樣的數據。

字符集對於數據庫重要的地方在於,當你使用數據庫函數進行比較,轉化,測量數據的時候。好比一個字段的`LENGTH`屬性就依賴它的字符集,用`LIKE`和`=`進行比較的時候也是。尤爲是對兩個字符串進行定序(collation)比較時。詳細狀況能夠參考這篇博客

一個解決方案

上面全部的問題都是由於提交和讀取使用的字符集不一致引發的。因此解決之道是確保你的每個網頁使用的都是UTF-8編碼。你能夠在你的<head>標籤後添加:

<metacharset="UTF-8"><metahttp-equiv="Content-type"content="text/html; charset=UTF-8">

這必須是你在製做網頁時候的第一件事,它會讓瀏覽器從新審視網頁的代碼。考慮到及時和效率,這個動做必須越早完成越好。你也能夠給你的數據庫指定UTF-8編碼

須要注意的是,用戶是能夠用他們的瀏覽器編碼中覆蓋你的網頁比編碼,這種狀況很是少,可是也說明這個解決方案並非萬能的。爲了安全起見,你能夠再網站後端對用戶的輸入進行編碼校驗。

已有的網站

若是你的網頁已經海納了百國語言,你須要把這些數據都轉爲UTF-8編碼。若是數量很少,你能夠在網頁中把這些個字符找出來,用瀏覽器把這些數據轉爲UTF-8編碼

第二部分:我碰見的編碼問題

相信上面的文章已經能夠解決了不少疑惑很問題了,我還想聊幾個我遇到的問題。

印象中Mac下的terminal顯示字符用的就是Unicode編碼,因此使用命令行時基本不會碰見編碼問題。可是在Window下的不管是cmd就悲劇了。

先把下面的文本存在window下的記事本中,好比叫text.txt中,供測試使用

ASCII     abcde xyz
German    äöüÄÖÜß
Polish ąęźżńł
Russian абвгдежэюя CJK 你好

首先,若是你用原生的記事本工具編輯確定是不能把上面的文本保存在記事本中,默認記事本使用的ASCII編碼,它會提示你另存一份Unicode版本。咱們固然須要使用的是這個Unicode版本。

在CMD中查看文本內容type text.txt,你看到的結果是

ASCII     abcde xyz
German    盲枚眉脛脰脺
Polish 膮臋藕偶艅艂
Russian 邪斜脅諧寫械卸褝褞褟 CJK 浣犲ソ

爲何輸出的全是亂碼?綜上一節所述,緣由無非是下面兩個

  • 使用的編碼不正確
  • 即便是編碼正確,也可能CMD的字體沒能顯示某個字符

首先看第一個問題:CMD使用的編碼是什麼?

輸入命令chcp(change code page),結果是活動頁代碼936。這是罪魁禍首之一。code page 936是什麼?咱們在維基百科上能夠找到答案:

936 GBK SupportsSimplifiedChinese

沒錯,cmd默認使用的編碼居然是GBK——GBK是什麼?GB這兩個字母表明「國標」二字拼音首字母,大家感覺一下吧……

據我stackoverflow的結果,cmd在不一樣語言中使用的是當地語言的編碼,而不是Unicode。

那麼咱們能夠把它的編碼改成Utf-8嗎?固然能夠,執行命令chcp 65001,更多code page代碼能夠從上面的那個維基百科頁面上找到。

讓咱們再用cmd查看文本結果:

  S C I I           a b c d e   x y z
 G e r m a n         äöüÄÖÜß
 P o l i s h

爲何仍是有問題?由於剛剛說過的字體。

此時須要改變cmd的字體,在cmd窗體標題單擊鼠標右鍵,選擇屬性,在字體中選擇Lucida Console

此時再進行打印,你就能看到正確的結果了。

 

若是你有python開發經驗的話,在python文件也須要註明文件的編碼(至少在3.0版本之前須要這麼作):

# -*- coding: utf-8 -*- 

做用與在html文件head中聲明的做用是同樣的。讓編譯器從新審視文件編碼

即便如此,當你在python直接定義一個包含字符串的變量而且嘗試打印時,結果依然是亂碼:

# -*- coding: utf-8 -*- 
str ="測試"print str // 嫺嬭瘯

其實你看到是已經被UTF-8編碼過的字符串,而後又被控制檯顯示出來。若是你但願顯示正確,必須再通過解碼

print str.encode("utf-8")// 測試

固然最好的辦法在定義時就定義爲Unicode編碼

str = u"測試"

 

其實Unicode還有不少地方能夠聊,好比UTF-16 LE與UTF-16 BE,也就是big endian和little endian等等。但這就要開始牽扯更深,好比CPU,進制轉換等,這些就不是我所擅長的範圍了。在文章最後悔會給出一些文章連接,有興趣的同窗能夠繼續深刻研究。

URL encoding

在談URL encode以前,先要了解一下URL的語法。

你可能會以爲奇怪URL還有語法?有固然有。

通常咱們接觸的網頁URL好比像http://qingbob.com,語法(如下的翻譯可能不許確)很是簡單:

協議(Scheme) http
主機地址(Host address) qingbob.com

但一個包含了完整語法的URL應該是這個樣子的:

https://bob:bobby@qingbob.com:8080/file;p=1?q=2#third

對應的語法應該是

協議(Scheme) https
用戶(User) bob
密碼(Password) bobby
主機地址(Host address) qingbob.com
端口(Port) 8080
路徑(Path) /file
路徑參數(Path parameters) p=1
查詢參數(Query parameters) q=2
片斷(Fragment) third

更詳細的圖解能夠參考維基百科

看過了上面的語法,你大概已經回憶起十之八九,單個部分就不詳解了,直接進入URL語法。

URL語法定義瞭如何把URL的各個部分組裝起來,以及如何把他們區分開來。好比://符號就是把協議與主機名區分開;主機名與路徑用/區分開;查詢參數只能跟在?以後。這也就意味着有一些符號做爲保留字爲語法服務。可能有的符號在整個URL中都做爲保留字,有的只是在某個部分做爲保留字。一旦某個保留字在它不該該出現的地方出現了(好比在文件名中出現了一個?)。這個字符就須要被編碼(encode)。

URL編碼就是把一個字符轉變成另外一種對URL無損的表現形式,而且讓它失去URL語法含義。經過字符編碼(character encoding)把這個字符轉化爲十六進制,而且以百分號%開頭,好比一個問號通過URL編碼以後就是%3F

想象咱們有一條指向某張圖片的URL:http://example.com/to_be_or_not_to_be?.jpg。轉化以後的結果就是http://example.com/to_be_or_not_to_be%3F.jpg,這樣就避免了URL中有歧義的問號以後的查詢語法。

一些陷阱
不一樣部分的不一樣保留字

一個空格在路徑參數(path fragment)部分會被編碼成%20(毫不是"+"),而+在參數路徑部分則能夠保留而不被編碼。

而在查詢部分(query)中,空格會被編碼成"+"(爲了向前兼容),或者"%20",而"+"會被編碼成"%2B"。

舉個例子,一樣的字符串blue+light blue在不一樣的部分會被編碼成不一樣樣子:

var url ="http://example.com/blue+light blue?blue+light blue"

encodeURL(url)// http://example.com/blue+light%20blue?blue%2Blight+blue
保留字不是你想的那樣

大部分人都忽略了"+"在路徑部分是能夠被容許的。還有一些其餘的例子:

  • "?"在查詢部分能夠容許不被轉義的(unescaped)
  • "/"在查詢部分能夠容許不被轉義
  • "="在路徑參數或者查詢參數部分能夠容許不被轉義
  • ":@-._~!$&'()*+,;="在路徑片斷部分能夠容許不被轉義
  • "/?:@-._~!$&'()*+,;="在片斷部分能夠容許不被轉義

也就是說下面這個URL片斷,是不會被編碼是合法的:

http://qingbob.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$'()*+,;=/?:@-._~!$'()*+,;

咱們把上面這條URL好好總結一下:

協議(Scheme) https
主機地址(Host address) qingbob.com
路徑(Path) /:@-._~!$&'()*+,=
路徑參數名稱(Path parameters name) :@-._~!$&'()*+,
路徑參數值(Path parameters value) :@-._~!$&'()*+,==
查詢參數名稱(Query parameters name) /?:@-._~!$'()* ,;
查詢參數值(Query parameters value) /?:@-._~!$'()* ,;==
片斷(Fragment) /?:@-._~!$&'()*+,;=
URL在編碼以後是不能被分析的

URL語法只在用來讀被編碼以前的URL。

假設咱們有這麼一條URLhttp://example.com/blue%2Fred%3Fand+green,分析結果以下

協議(Scheme) http
主機(Host) example.com
路徑碎片(Path segment) blue%2Fred%3Fand+green
解碼以後的路徑碎片(Decoded Path segment) blue/red?and+green

這樣翻譯的結果就成了,咱們在尋找一個名爲"blue/red?and+green"的文件。

可是若是咱們在解碼以後再進行翻譯http://example.com/blue/red?and+green

協議(Scheme) http
主機(Host) example.com
路徑碎片(Path segment) blue
路徑碎片(Path segment) red
查詢參數名稱(Query parameter name) and green

翻譯的結果是,在"blue"文件夾中的名爲"red?and+green"的文件。

明顯結果就徹底不一樣了,對URL語法進行分析必定要在編碼以前進行進行。

 

最後參考文獻:

相關文章
相關標籤/搜索