Ascii
ASCII是基於拉丁字母的一套電腦編碼系統。它主要用於顯示現代英語和其他西歐語言。它是現今最通用的單字節編碼系統,並等同於國際標準ISO/IEC 646。
標準ASCII 碼也叫基礎ASCII碼,使用7 位二進制數來表示所有的大寫和小寫字母,數字0 到9、標點符號, 以及在美式英語中使用的特殊控制字符,共128字符。在標準ASCII中,其最高位(b7)用作奇偶校驗位。爲了表示更多的歐洲常用字符對ASCII進行了擴展,ASCII擴展字符集使用8位(bits)表示一個字符,共256字符。ASCII擴展字符集比ASCII字符集擴充出來的符號包括表格符號、計算符號、希臘字母和特殊的拉丁符號。
Anis
ANSI碼(American National Standards Institute),中文:美國國家標準學會的標準碼。
對於歐美國家的語言字符,ASCII尚能應付自如,可是隨着計算機的發展和普及,伴隨着中文、日文、韓文等語言的需求,256個字符遠遠不能表示所有的常用字符了。這時就需要對原本的ASCII進行改進以表示更多的字符,最簡單最實際的做法就是擴展字節。將128作爲分水嶺,小於128的字符還是使用正常的一個字節的ASCII進行表示,保證了英文的兼容。把大於128的字符作爲一個引導字節,來決定後邊字符的編碼的長度和內容。通過這種變長的靈活編碼方式,使得這種編碼支持了幾乎常用的所有語言的字符集,例如我們常用的GB2312、GBK、GB18030等等。由於ASCII起初是ANSI的標準字符集,因此這種變長編碼方式稱爲ANSI的多字節字符集MBCS,也稱爲爲ANSI字符集。
不同的國家和地區制定了不同的標準,由此產生了 GB2312, BIG5, JIS 等各自的編碼標準。在簡體中文系統下,ANSI 編碼代表 GB2312 編碼,在日文操作系統下,ANSI 編碼代表 JIS 編碼。
GB2312
GB2312又稱爲GB2312-80字符集,全稱爲《信息交換用漢字編碼字符集·基本集》,由原中國國家標準總局發佈,1981年5月1日實施。
GB2312是中國國家標準的簡體中文字符集。它所收錄的漢字已經覆蓋99.75%的使用頻率, 基本滿足了漢字的計算機處理需要。在中國大陸和新加坡獲廣泛使用。GB2312收錄簡化漢字及一般符號、序號、數字、拉丁字母、日文假名、希臘字母、俄文字母、漢語拼音符號、漢語注音字母,共 7445 個圖形字符。其中包括6763個漢字,其中一級漢字3755個,二級漢字3008個;包括拉丁字母、希臘字母、日文平假名及片假名字母、俄語西裏爾字母在內的682個全角字符。
GB2312中對所收漢字進行了“分區”處理,共分爲每區含有94個漢字/符號。這種表示方式也稱爲區位碼。
各區包含的字符如下:01-09區爲特殊符號;16-55區爲一級漢字,按拼音排序;56-87區爲二級漢字,按部首/筆畫排序;10-15區及88-94區則未有編碼。
GB2312是雙字節編碼,即一個GB2312編碼的字符佔2個字節。習慣上稱第一字節爲“高字節” ,而稱第二字節爲“低字節”。“高位字節”使用了0xA1-0xF7(把01-87區的區號加上0xA0),“低位字節”使用0xA1-0xFE(把01-94加上0xA0)。GB2312高字節 = “區號”+160 ,GB2312低字節 = "位號" + 160。
以GB2312字符集的第一個漢字“啊”字爲例,它的區號16,位號01,則區位碼是1601,在大多數計算機程序中,高字節和低字節分別加0xA0得到程序的漢字GB2312編碼爲0xB0A1。計算公式是:0xB0=0xA0+16, 0xA1=0xA0+1。
所以如果我們求漢字區位碼的話,我們可以通過求出該字的GB2312編碼,然後將該編碼的高字節與低字節分別減去160(0XA0)
BIG5
BIG5又稱大五碼或五大碼,1984年由臺灣財團法人信息工業策進會和五間軟件公司宏碁 (Acer)、神通 (MiTAC)、佳佳、零壹 (Zero One)、大衆 (FIC)創立,故稱大五碼。
Big5碼使用了雙字節儲存方法,一個字符佔2個字節。第一個字節稱爲“高位字節”,第二個字節稱爲“低位字節”。高位字節的編碼範圍0xA1-0xF9,低位字節的編碼範圍0x40-0x7E及0xA1-0xFE。
Unicode
Unicode(統一碼、萬國碼、單一碼)是一種在計算機上使用的字符編碼。它爲每種語言中的每個字符設定了統一併且唯一的二進制編碼,以滿足跨語言、跨平臺進行文本轉換、處理的要求。
Unicode是國際組織制定的可以容納世界上所有文字和符號的字符編碼方案。Unicode用數字0-0x10FFFF來映射這些字符,最多可以容納1114112個字符,或者說有1114112個碼位。碼位就是可以分配給字符的數字。
最初的unicode編碼是固定長度的,16位,也就是2兩個字節代表一個字符,這樣一共可以表示65536個字符。顯然,這樣要表示各種語言中所有的字符是遠遠不夠的。Unicode4.0規範考慮到了這種情況,定義了一組附加字符編碼,附加字符編碼採用2個16位來表示,這樣最多可以定義1048576個附加字符,目前unicode4.0只定義了45960個附加字符。
Unicode的編碼和實現
大概來說,Unicode編碼系統可分爲編碼方式和實現方式兩個層次。
編碼方式
統一碼的編碼方式與ISO 10646的通用字符集(UCS)概念相對應。目前實際應用的統一碼版本對應於UCS-2,使用16位的編碼空間。也就是每個字符佔用2個字節。這樣理論上一共最多可以表示216(即65536)個字符。基本滿足各種語言的使用。實際上當前版本的統一碼並未完全使用這16位編碼,而是保留了大量空間以作爲特殊使用或將來擴展。
上述16位統一碼字符構成基本多文種平面。最新(但未實際廣泛使用)的統一碼版本定義了16個輔助平面,兩者合起來至少需要佔據21位的編碼空間,比3字節略少。但事實上輔助平面字符仍然佔用4字節編碼空間,與UCS-4保持一致。未來版本會擴充到ISO 10646-1實現級別3,即涵蓋UCS-4的所有字符。UCS-4是一個更大的尚未填充完全的31位字符集,加上恆爲0的首位,共需佔據32位,即4字節。理論上最多能表示231個字符,完全可以涵蓋一切語言所用的符號。
基本多文種平面的字符的編碼爲U+hhhh,其中每個h代表一個十六進制數字,與UCS-2編碼完全相同。而其對應的4字節UCS-4編碼後兩個字節一致,前兩個字節則所有位均爲0。
實現方式
Unicode的實現方式不同於編碼方式。一個字符的Unicode編碼是確定的。但是在實際傳輸過程中,由於不同系統平臺的設計不一定一致,以及出於節省空間的目的,對Unicode編碼的實現方式有所不同。Unicode的實現方式稱爲Unicode轉換格式(Unicode Transformation Format,簡稱爲UTF)
例如,如果一個僅包含基本7位ASCII字符的Unicode文件,如果每個字符都使用2字節的原Unicode編碼傳輸,其第一字節的8位始終爲0。這就造成了比較大的浪費。對於這種情況,可以使用UTF-8編碼,這是一種變長編碼,它將基本7位ASCII字符仍用7位編碼表示,佔用一個字節(首位補0)。而遇到與其他Unicode字符混合的情況,將按一定算法轉換,每個字符使用1-3個字節編碼,並利用首位爲0或1進行識別。這樣對以7位ASCII字符爲主的西文文檔就大大節省了編碼長度(具體方案參見UTF-8)。類似的,對未來會出現的需要4個字節的輔助平面字符和其他UCS-4擴充字符,2字節編碼的UTF-16也需要通過一定的算法進行轉換。
再如,如果直接使用與Unicode編碼一致(僅限於BMP字符)的UTF-16編碼,由於每個字符佔用了兩個字節,在麥金塔電腦 (Mac)機和個人電腦上,對字節順序的理解是不一致的。這時同一字節流可能會被解釋爲不同內容,如某字符爲十六進制編碼4E59,按兩個字節拆分爲4E和59,在Mac上讀取時是從低字節開始,那麼在Mac OS會認爲此4E59編碼爲594E,找到的字符爲“奎”,而在Windows上從高字節開始讀取,則編碼爲U+4E59的字符爲“乙”。就是說在Windows下以UTF-16編碼保存一個字符“乙”,在Mac OS環境下打開會顯示成“奎”。此類情況說明UTF-16的編碼順序若不加以人爲定義就可能發生混淆,於是在UTF-16編碼實現方式中使用了大端序(Big-Endian, 簡寫爲UTF-16 BE)、小端序(Little-Endian,簡寫爲UTF-16 LE)的概念,以及可附加的字節順序記號解決方案,目前在PC機上的Windows系統和Linux系統對於UTF-16編碼默認使用UTF-16 LE。(具體方案參見UTF-16)
此外Unicode的實現方式還包括UTF-7、Punycode、CESU-8、SCSU、UTF-32、GB18030等,這些實現方式有些僅在一定的國家和地區使用,有些則屬於未來的規劃方式。目前通用的實現方式是UTF-16小端序(LE)、UTF-16大端序(BE)和UTF-8。在微軟公司Windows XP附帶的記事本(Notepad)中,“另存爲”對話框可以選擇的四種編碼方式除去非Unicode編碼的ANSI(對於英文系統即ASCII編碼,中文系統則爲GB2312或Big5編碼) 外,其餘三種爲“Unicode”(對應UTF-16 LE)、“Unicode big endian”(對應UTF-16 BE)和“UTF-8”。
目前輔助平面的工作主要集中在第二和第三平面的中日韓統一表意文字中,因此包括GBK、GB18030、Big5等簡體中文、繁體中文、日文、韓文以及越南喃字的各種編碼與Unicode的協調性被重點關注。考慮到Unicode最終要涵蓋所有的字符。從某種意義而言,這些編碼方式也可視作Unicode的出現於其之前的既成事實的實現方式,如同ASCII及其擴展Latin-1一樣,後兩者的字符在16位Unicode編碼空間中的編碼第一字節各位全爲0,第二字節編碼與原編碼完全一致。但上述東亞語言編碼與Unicode編碼的對應關係要複雜得多。
UTF-8
UTF-8(8-bit Unicode Transformation Format)是一種針對Unicode的可變長度字符編碼,也是一種前綴碼。它可以用來表示Unicode標準中的任何字符,且其編碼中的第一個字節仍與ASCII兼容,這使得原來處理ASCII字符的軟件無須或只須做少部份修改,即可繼續使用。因此,它逐漸成爲電子郵件、網頁及其他存儲或傳送文字的應用中,優先採用的編碼。
UTF-8使用一至四個字節爲每個字符編碼:
128個US-ASCII字符只需一個字節編碼(Unicode範圍由U+0000至U+007F)。
帶有附加符號的拉丁文、希臘文、西裏爾字母、亞美尼亞語、希伯來文、阿拉伯文、敘利亞文及它拿字母則需要二個字節編碼(Unicode範圍由U+0080至U+07FF)。
其他基本多文種平面(BMP)中的字符(這包含了大部分常用字)使用三個字節編碼。
其他極少使用的Unicode 輔助平面的字符使用四字節編碼。
UTF-8以字節爲編碼單元,沒有字節序的問題。它使用可變長度字節來儲存 Unicode字符,ASCII字母繼續使用1字節儲存,重音文字、希臘字母或西裏爾字母等使用2字節來儲存,而常用的漢字就要使用3字節,輔助平面字符則使用4字節。(UTF-8編碼的最大長度是4個字節)
Unicode編碼(16進制) ║ UTF-8 字節流(二進制)
000000 - 00007F ║ 0xxxxxxx
000080 - 0007FF ║ 110xxxxx 10xxxxxx
000800 - 00FFFF ║ 1110xxxx 10xxxxxx 10xxxxxx
010000 - 10FFFF ║ 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
UTF-16
UTF-16是Unicode字符編碼五層次模型的第三層:字符編碼表(Character Encoding Form,也稱爲 "storage format")的一種實現方式。即把Unicode字符集的抽象碼位映射爲16位長的整數(即碼元)的序列,用於數據存儲或傳遞。Unicode字符的碼位,需要1個或者2個16位長的碼元來表示,因此這是一個變長表示。
UTF是"Unicode/UCS Transformation Format"的首字母縮寫,即把Unicode字符轉換爲某種格式之意。UTF-16正式定義於ISO/IEC 10646-1的附錄C,而RFC2781也定義了相似的做法。
Uft-16與UCS-2的關係
UTF-16可看成是UCS-2的父集。在沒有輔助平面字符(surrogate code points)前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字符後,就稱爲UTF-16了。現在若有軟件聲稱自己支持UCS-2編碼,那其實是暗指它不能支持在UTF-16中超過2bytes的字集。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼。
Microsoft Windows操作系統內核對Unicode的支持
Windows操作系統內核中的字符表示爲UTF-16小尾序,可以正確處理、顯示以4字節存儲的字符。但是Windows API實際上僅能正確處理UCS-2字符,即僅以2字節存儲的,碼位小於U+FFFF的Unicode字符。其根源是Microsoft C++語言把wchar_t數據類型定義爲16比特的unsigned short,這就與一個wchar_t型變量對應一個寬字符,可以存儲一個Unicode字符的規定相矛盾。相反,Linux平臺的GCC編譯器規定一個wchar_t是4字節長度,可以存儲一個UTF-32字符,寧可浪費了很大的存儲空間。下例運行於Windows平臺的C++程序可說明此點:
// 此源文件在Windows平臺上必須保存爲Unicode格式(即UTF-16小尾) // 因爲包含的漢字“�”,不能在Windows默認的代碼頁936(即gbk)中表示。 // 該漢字在UTF-16小尾序中用4個字節表示, // Windows操作系統能正確顯示這樣的在UTF-16需用4字節表示的字符 // 但是Windows API不能正確處理這樣的在UTF-16需用4字節表示的字符,把它判定爲2個UCS-2字符
#include <windows.h> int main() { const wchar_t lwc[]=L"�";
MessageBoxW(NULL, lwc, lwc, MB_OK);
int i = wcslen(lwc); printf("%d\n", i); int j = lstrlenW(lwc); printf("%d\n", j);
return 0; } |
windows字符集編碼
在Windows編程時經常會遇到編碼轉換的問題,一直以來讓剛接觸的人摸不着頭腦。其實只要弄清Win32程序使用的字符編碼方式就清楚了,圖1展示了一個Win32控制檯項目的屬性中的字符集選項。這裏有兩個不同的字符集:一個是Unicode字符集,另一個就是多字節字符集MBCS(Multi-Byte Character System),即熟知的ANSI字符集。
伴隨着圖形界面計算機的出現,字符集就應運而生了。要顯示字符信息,就需要將之轉換爲二進制信息表示——編碼。“可悲”的是計算機是美國人發明的,而英語就是26個英文字母和一些常用標點符號的組合,這些字符稱爲ASCLL字符集。它是使用1個字節的長度進行編碼,也就是能表示256個不同的字符,實際上真正用到的可見字符不到128個。
對於歐美國家的語言字符,ASCLL尚能應付自如,可是隨着計算機的發展和普及,伴隨着中文、日文、韓文等語言的需求,256個字符遠遠不能表示所有的常用字符了。這時就需要對原本的ASCLL進行改進以表示更多的字符,最簡單最實際的做法就是擴展字節。將128作爲分水嶺,小於128的字符還是使用正常的一個字節的ASCLL進行表示,保證了英文的兼容。把大於128的字符作爲一個引導字節,來決定後邊字符的編碼的長度和內容。通過這種變長的靈活編碼方式,使得這種編碼支持了幾乎常用的所有語言的字符集,例如我們常用的GB2312、GBK、GB18030等等。由於ASCII 起初是ANSI的標準字符集,因此這種變長編碼方式稱爲ANSI的多字節字符集MBCS,也稱爲爲ANSI字符集。
windows關於默認字符集的設定是在
控制面板-->地區及語言選項—>Advanced-->選擇非unicode的默認字符集(更改此選項,需要重啓電腦)
windows 98由於是繼承自16位操作系統的32位操作系統,而16位操作系統只使用ANSI編碼,故windows98只支持ANSI編碼,windows系統調用函數的參數都必須是ANSI編碼;
windows 2000是一個不同於windows 98的全新操作(即它是重新開發的),它的系統調用函數的參數只支持Unicode,但是它同時也通過一種間接轉換來支持ANSI參數的系統調用,即當參數是ANSI時,操作系統會將ANSI轉化爲Unicode,然後將轉換後的參數傳給被調用的這個系統函數,同理,若這個系統函數返回值是ANSI,操作系統也會將Unicode轉換爲ANSI,然後傳遞這個返回值;因此說windows 2000是既支持ANSI也支持Unicode,但是使用ANSI會降低系統的性能;
C語言的字符編碼
在c中,char代表字節數據類型;在java中,char代表字符數據類型(內存可能佔一個或倆個字節),byte代表字節數據類型;
C中,字符串char *代表的是ansi編碼,ansi不是一種確定的編碼,那些所有使用 2 個字節來代表一個字符的各種漢字延伸編碼方式,稱爲 ANSI 編碼。在簡體中文系統下,ANSI 編碼代表 GB2312 編碼,在繁體中文系統下,ANSI 編碼代表 BIG5 編碼,在日文操作系統下,ANSI 編碼代表 JIS 編碼;
JAVA字符集編碼
Java的class文件採用utf8的編碼方式,JVM運行時採用utf16。
Java的字符串是unicode編碼的。(注:這裏說的UNICODE應該是UCS-2,2字節,支持65536個字符。UTF-16是UCS-2的一個超集,在沒有輔助平面字符(surrogate code points)前,UTF-16與UCS-2所指的是同一的意思。但當引入輔助平面字符後,就稱爲UTF-16了。現在若有軟件聲稱自己支持UCS-2編碼,那其實是暗指它不能支持在UTF-16中超過2bytes的字集。對於小於0x10000的UCS碼,UTF-16編碼就等於UCS碼。)
總之,Java採用了unicode字符集,使之易於國際化。
下面來看個問題:
// str是utf-8編碼的字符串嗎? String str = new String(webPara.getBytes("GBK"), "UTF-8")); |
簡單地說String在JVM裏是unicode的,任何byte[]到String以及String到byte[]都涉及到字符集編碼轉換。
byte[] ---> String就是將按某一個編碼後的字節數組轉換爲unicode的字符串,
String ---> byte[]正好相反,它是將unicode的字符串編碼爲唯一特定字符集編碼後的字節數組。
public String(byte[] bytes) public String(byte[] bytes, String charsetName) |
這兩個String的方法就是完成將bytes“解碼”爲unicode的String。
前者使用的是jvm默認的字符集編碼,而後者是用戶指定某一個charsetName,可以是UTF-8,GBK之類的。
兩者都是完成specCharset到unicode的過程,而不是說改變編碼格式爲charsetName指定的字符集編碼。
public byte[] getBytes() public byte[] getBytes(String charsetName) |
這兩個方法就是完成從unicode到指定字符集編碼的“轉碼”過程。
以瀏覽器爲例,http的parameter到servlet裏後,
應用服務器已經自動完成了new String(parameterBytes, browserSpecCharset)這個過程。
也就是,自動用頁面設置的charset“解碼”parameterBytes字節數組爲unicode的字符串。
因此,類似最上面的代碼
String str = new String(webPara.getBytes("GBK"), "UTF-8")); |
得到的str是用utf-8“解碼”經過gbk編碼後的字節數組而得到unicode碼的字符串。
舉個例子
“中國”.getBytes("GBK") “中國”.getBytes("UTF-8") |
上述字節數組按十六進制打印出來
//GBK編碼爲 // D6 D0 B9 FA
//UTF-8編碼爲 // E4 B8 AD E5 9B BD |
這個也說明了getBytes方法完成了unicode到gbk/utf-8的轉換。
綜上所述
String str = new String(aStr.getBytes("GBK"), "UTF-8")); |
是無法完成字符集編碼轉換地,任何一個Java String都是unicode的
對用a編碼的字節數組,用b去解碼,大部分情況都是亂碼。
Java支持哪些字符集:
即Java能識別哪些字符集並對它進行正確地處理?
查看Charset 類,最新的JDK支持160種字符集。可以通過static方法availableCharsets拿到所有Java支持的字符集。
assertEquals(160, Charset.availableCharsets().size()); |
需要在哪些時候注意編碼問題?
1. 從外部資源讀取數據:
這跟外部資源採取的編碼方式有關,我們需要使用外部資源採用的字符集來讀取外部數據:
InputStream is = new FileInputStream("res/input2.data"); |
這裏可以看到,我們採用了GB18030編碼讀取外部數據,通過查看streamReader的encoding可以印證:
assertEquals("GB18030", streamReader.getEncoding()); |
正是由於上面我們爲外部資源指定了正確的編碼,當它轉成char數組時才能正確地進行解碼(GB18030 -> unicode):
Java代碼
char[] chars = new char[is.available()];
streamReader.read(chars, 0, is.available());
但我們經常寫的代碼就像下面這樣:
InputStream is = new FileInputStream("res/input2.data"); |
這時候InputStreamReader採用什麼編碼方式讀取外部資源呢?Unicode?不是,這時候採用的編碼方式是JVM的默認字符集,這個默認字符集在虛擬機啓動時決定,通常根據語言環境和底層操作系統的 charset 來確定。可以通過以下方式得到JVM的默認字符集:
Charset.defaultCharset(); |
爲什麼要這樣?因爲我們從外部資源讀取數據,而外部資源的編碼方式通常跟操作系統所使用的字符集一樣,所以採用這種默認方式是可以理解的。
好吧,那麼我通過我的IDE Ideas創建了一個文件,並以JVM默認的編碼方式從這個文件讀取數據,但讀出來的數據竟然是亂碼。爲何?呵呵,其實是因爲通過Ideas創建的文件是以utf-8編碼的。要得到一個JVM默認編碼的文件,通過手工創建一個txt文件試試吧。
2. 字符串和字節數組的相互轉換
我們通常通過以下代碼把字符串轉換成字節數組:
"string".getBytes(); |
但你是否注意過這個轉換採用的編碼呢?其實上面這句代碼跟下面這句是等價的:
string".getBytes(Charset.defaultCharset()); |
也就是說它根據JVM的默認編碼(而不是你可能以爲的unicode)把字符串轉換成一個字節數組。
反之,如何從字節數組創建一個字符串呢?
new String("string".getBytes()); |
同樣,這個方法使用平臺的默認字符集解碼字節的指定數組(這裏的解碼指從一種字符集到unicode)。
字符串編碼迷思:
new String(input.getBytes("ISO-8859-1"), "GB18030") |
上面這段代碼代表什麼?有人會說: “把input字符串從ISO-8859-1編碼方式轉換成GB18030編碼方式”。如果這種說法正確,那麼又如何解釋我們剛提到的java字符串都採用unicode編碼呢?
這種說法不僅是欠妥的,而且是大錯特錯的,讓我們一一來分析,其實事實是這樣的:我們本應該用GB18030的編碼來讀取數據並解碼成字符串,但結果卻採用了ISO-8859-1的編碼,導致生成一個錯誤的字符串。要恢復,就要先把字符串恢復成原始字節數組,然後通過正確的編碼GB18030再次解碼成字符串(即把以GB18030編碼的數據轉成unicode的字符串)。注意,字符串永遠都是unicode編碼的。
但編碼轉換並不是負負得正那麼簡單,這裏我們之所以可以正確地轉換回來,是因爲 ISO8859-1 是單字節編碼,所以每個字節被按照原樣 轉換爲String ,也就是說,雖然這是一個錯誤的轉換,但編碼沒有改變,所以我們仍然有機會把編碼轉換回來!
ANSI的BUG
很多細心的人會發現,當新建文本文檔只輸入“聯通”2字保存再打開時將是亂碼。
當txt文檔中一切字符都在 C0≤AA(第一個字節)≤DF 80≤BB(第二個字節)≤BF 這個範圍時,notepad都無法確認文檔地格式,沒有自動依照UTF-8格式來"Display"。 而"聯通"就是C1 AA CD A8,剛好在上面地範圍內,所以不能正常顯現。
記事本默認是以ANSI編碼保存文本文檔的,而正是這種編碼存在的bug招致了上述怪現象。假如保存時選擇Unicode、Unicode(big endian)、UTF-8編碼就正常了。此外,假如以ANSI編碼保存含有某些特別符號的文本文檔,再次打開後符號也會變成英文問號。