程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 程序員應該知道的字符編碼的故事

程序員應該知道的字符編碼的故事

編輯:關於JAVA
 

如果想要知道當前字符編碼的情況,最簡單的方式就是回顧下歷史,看看我們是如何到達今天這種狀況的。在ASCII碼之前的時代對現今沒有多大的影響,但那是一段有趣的時光,所以如果你有興趣的話,我就從技術的角度帶你簡單回顧下各種編碼的那段歷史。Unicode的故事是從ASCII碼開始的。由於Windows和Unix是現在程序員們只能使用的兩種操作系統,所以我只會對這兩種系統進行講述。(Linux,iOS,Android等等都是基於Unix的)首先,我會介紹下在Unicode之前基於ASCII碼編碼的一些問題,然後說一下跨平台編程的一些編程注意點。如果你不感興趣的話,你可以跳過在產生Unicode編碼之前的內容

ASCII

ASCII碼非常重要,因為它從根本上定義了各種標點符號的一個子集,而這個子集將會被各種編程語言所使用,同時操作系統也會遵循這個標准。有一個問題需要講清楚,ASCII碼是一種7比特的編碼,沒有什麼所謂8比特編碼的ASCII碼。如果你曾經聽過有人講8比特編碼的ASCII碼,他真正的意思可能是使用了ASCII碼的可打印字符(32-126)加上肯能2個或3個控制字符(LF,CR,TAB)的8比特編碼。是個,你可能還不了解,ASCII碼當初實際上是被設計為一個7比特編碼的族,其中的一些字符用來表示這些字符在國別上的其他變體(“national variants”),它們在不同的國家有不同的含義。比如說,在英國的ASCII碼的變體會將井號(#)替換為英鎊標志(£),而在挪威的變體會將方括號替換為Æ與Å,同時會將大括號替換為æ與å。對於通信來說這不是什麼問題,但是對於編程來說很明顯這會有點問題。比如,對於C語言來說,大量使用了方括號與花括號。結果就是它們會被一些三字符組代替,方括號會被??(與??)所代替,花括號會被??<與??>所代替。(更多信息ISO/IEC 646以及三字符組)鑒於這些問題,國別變體的ASCII碼很快就被8比特編碼給替代了。現在,當人們再說起ASCII碼的時候,大家討論的實際上是美帝的ASCII碼變體,它是C語言以及其他大部分現代編程語言的基礎。

基於ASCII碼的8位編碼

從7位編碼轉換為8位編碼為我們提供了額外的128個碼位,這解決了很多問題。最重要的是,它意味著所有國家變體的字符從128個編碼中清除。現在,我們有了一個真正意義上的128個通用字符可以被操作系統以及語言創始人自由的使用而不用擔心三字符組或其他組合。其次,我們也可以支持比如希臘語以及俄語,相比較國家變體的ASCII碼有限的字符來說它們需要更多的字符,現在也能滿足了。

UNIX,終端以及C1

UNIX是一個基於終端的系統。對於年輕人來說可能還不知道什麼是啞終端,它是一台擁有基於字符的屏幕(通常約為80×25個字符)以及一個鍵盤的設備,並通過一根串口線連接到UNIX服務器。如果你通過終端向串行端口發送文字,最終什麼字符顯示在屏幕上是由終端決定的。同樣道理,當你按下鍵盤上的一個按鍵,哪個字符發送到服務器也是由終端決定的。在這之上,在同一個通信裡,我們還需要發送控制序列,比如移動鼠標,清空屏幕,這些序列都是與字符編碼混在一起發送的。這意味著我們不能把所有的碼位分配給字符。我們需要控制字符的編碼,並且這些控制字符的編碼不能與其他字符沖突。

控制字符編碼0-31(我們熟知的C0塊)以及ASCII中的127都已經分配給控制編碼。他們的意義可能與ASCII碼的定義不相符,但是沒有關系。他們不能用來編碼可打印字符並且很多已經有良好的功能。接下來我們是可打印字符從32到126,幾乎所有的字符都有特殊的意義(命令的名字,目錄分隔符,shell轉義字符,編程語言符號等)並且他們都是不可以改變的。然而,編碼128以及大於它的對於所有人來說都是可以自由使用的。終端可以用它來定義控制字符,可打印字符等等任何東西,對於服務器來說沒有任何關系。只要所有連接到服務器的不同終端在可打印字符上達成一致,任何工作都會順利進行。我們甚至可以使用不同的控制字符編碼,只要控制字符不會與可打印字符沖突就行。

為了保持一致性與可互操作性,大家建議128-159保留作為第二個控制字符塊(C1塊),只有160-255被用來做可打印字符。這樣就形成了在Unicode之前Unix編碼的基礎。然而,它並不是一個硬性的規定,Unix本身對待C1塊與其他超過128的編碼沒有任何不同。

這使得Unix與語言本身脫離了關系。比如說,我們有一個Unix操作系統,有兩個文件,他們的名字裡分別包含了字符編碼68 196與68 228。當我們通過一個希臘語的終端連接到系統並且查看文件的時候,我們將會看到“DΔ”與“Dδ”。當我們通過一個法語終端連接到系統時我們將會看到“DÄ”與“Dä”,當我們通過一個泰語終端連接到系統時我們將會看到 “Dฤ”與“Dไ”,同樣的文件三個不同的終端顯示卻不同,這沒有什麼。一個泰國的用戶仍然可以使用“vi Dฤ”打開文件,希臘的用戶可以使用“vi DΔ”打開文件。至於操作系統方面,只要它們底層的編碼是一致的,一切看起來都很順利。這裡我們可能注意到“δ”在希臘語裡是“Δ”的小寫形式,“ฤ”與“ไ”在泰語裡完全不相關。這沒有關系,對於Unix來說它不會在比如文件名大小寫這樣的問題上做任何假設,對於文件內容也是如此。如果你將文件的內容顯示到屏幕上,文件內容會直接發送到終端,由終端負責渲染。操作系統不會參與。

事實上,如果原因的話,我們可以在一個Unix操作系統上同時使用許多不同的編碼。我們可以使用不同的語言分別創建子目錄,只要用戶使用相同的編碼的終端訪問那些子目錄,一切都會很順利。這樣,Unix本身不需要任何“編碼”設置。它也不必關心。在泰國,我們使用泰語終端連接服務器。在希臘,我們使用希臘語終端連接。

然而,那些需要輸出我們可閱讀(human-readable)錯誤信息的命令或者系統服務需要知道使用什麼語言來輸出。同樣道理,對於那些附加的程序,特別是那些需要執行排序或者文字處理的,需要知道它們使用的是什麼語言。但是,這沒必要成為一個系統級別的設置,只需要對某一個用戶的會話采用相應的配置即可。操作系統使用了一個環境變量(LOCALE)來指定語言以及編碼方式。

DOS/Windows

換個角度,DOS(以及之後的Windows 95/98/Me)就是另一碼事了。首先,是控制編碼的問題。因為屏幕是通過視頻適配器直接定位的,所以我們不需要任何控制字符。最初的DOS編碼(code page 437)將所有的ASCII控制碼以及所有超過128的碼位都分配給了可打印字符,但是它仍然可以被認為是一個基於ASCII碼的,因為它包括了ASCII碼中(32-126)的所有可打印字符。現在事實證明,問題有點大啊。換行符對於文件的換行很有用,但是如果我們采用直接定位將內容顯示到屏幕上時,換行符就沒什麼作用了。我們也曾遇到這樣的問題,比如我們與一台支持ASCII碼的串行打印機通信,同時使用ASCII碼的控制碼作為打印機控制命令。考慮到兼容性的問題,我們不能將C0塊指定為可打印字符,因此Windows95沒有使用C0塊作為可打印字符。但是,Windows95也確實用不著終端控制碼,所以它就把C1塊(128-159)分配成可打印字符了。

另一個與Windows 95/98/Me極其相關的問題是它使用了不區分大小寫的文件名,因此操作系統需要知道它應該采用什麼編碼。例如,196與228在Windows-1253中是相同的字母-小寫delta(”δ”)與大寫delta(”Δ”),但是在Windows-874中就成了不同的字母 (”ฤ”)與(”ไ”)。因此Windows需要一個系統范圍的編碼配置。對於一個單用戶操作系統來說這不是什麼問題,但是當我們聯網工作的時候,問題就出現了。例如,在一個支持泰語的計算機上我們有兩個文件叫做 “Dฤ”以及“Dไ”。然而如果我們試著通過網絡將這兩個文件拷貝到一台支持希臘語的計算機上,我們就會遇到問題。就支持希臘語的計算機而言,這兩個文件有相同的文件名,一個文件將會破壞另一個文件。同時,我們也不能使用單獨一台文件服務器存儲我們不同國際辦事處的文件,這行不通。同樣道理,我們不能建立一個支持不同語言的Web服務器。到這裡,你應該毫不奇怪微軟是Unicode的鐵桿支持者之一,並且是首先支持Unicode的操作系統之一。

使用8位編碼編程

現在,編程應該是相當輕松了。在大部分情況下,你甚至不用理會那些與編碼有關的軟件。例如,一個C語言編譯器沒有必要關注編碼。所有本地字符(128或者更高的)只能出現在注釋或者字符串/字符常量中。注釋會被忽略掉,而常量會原樣拷貝到data區。正則表達式(以及vi/sed/awk/等等)都不需要知道采用什麼編碼方式。所有的字符都是基於字節的,只要使用這些程序的人使用他們的語言輸入正確的表達式,一切都會正確運行。(例如,一個挪威人想要匹配所有的大寫字母需要使用這樣的正則表達式/[A-ZÅÆ]/而不是/[A-Z]/,但是正則表達式解析器沒有不要改變。)最基本的假設就是,我們端到端有相同的編碼,所以工具沒有必要做任何的轉換,輸入什麼它就輸出什麼就可以了。

如果我們確實需要做一些與語言相關的工作,比如排序或者大小寫轉換,要完成這樣的功能我們只有很小的工作量。排序其實都不是一個編碼問題,因為排序在不同語言間會不同即使它們采用相同的編碼。在C語言中,使用很少的函數就能搞定。本地設置(在中)定義了語言和編碼,是我們需要的唯一編碼庫,它包括一些函數判斷一個字符的類型(isnum(),isupper(),islower(),isalpah()等等)以及大小寫轉換的函數(toupper()/tolower()).排序與語言以及編碼相關,中的strcoll()與strxfrm()提供支持。這就是所有我們需要的,下面是一個例子,你可以自己實踐下看看效果。

#include
#include
#include
int main() {
// West Europe: 198 = Æ, 230 = æ (upper case and lower case letters)
setlocale(LC_CTYPE, ".1252"); // On UNIX, use ".iso-8859-1"
printf("%d %d\n", (isupper(198) ? 1 : 0), (isupper(230) ? 1 : 0));
printf("%d %d\n", (islower(198) ? 1 : 0), (islower(230) ? 1 : 0));
printf("%d %d\n", (isalpha(198) ? 1 : 0), (isalpha(230) ? 1 : 0));

// Thai: 198 = ฦ, 230 = ๆ (letters, but neither upper or lower case)
setlocale(LC_CTYPE, ".874"); // On UNIX, use ".iso-8859-11"
printf("%d %d\n", (isupper(198) ? 1 : 0), (isupper(230) ? 1 : 0));
printf("%d %d\n", (islower(198) ? 1 : 0), (islower(230) ? 1 : 0));
printf("%d %d\n", (isalpha(198) ? 1 : 0), (isalpha(230) ? 1 : 0));
return 0;
};
 

多字節編碼

到目前為止我們討論的所有編碼都是單字節編碼,即一個字節代表一個字符。然而,對於像中國,日本,韓國(通常簡稱CJK)這些國家,單字節不能夠囊括他們所有的文字。唯一的選擇就是使用多字節代替單字節。因為所有的這些編碼仍然使用單字節表示ASCII碼,所以它們都是變長的編碼。為了兼容基於ASCII碼的系統,這些字符通常被安排在94*94(2字節)或者94*94*94(3字節)的頁中,這既可以覆蓋36到126的字符碼(通過7bit通信-實際中只在基於SMTP的email中使用)同時也能覆蓋161到254的字符編碼(8bit編碼).在UNIX上,這些字符集的編碼方式是一種叫做EUC(擴展unix碼)的系統。它的基本思想是所有的非ASCII碼字符使用多字節編碼,並且使用大於128的編碼范圍。這意味著我們可以隨時可以使用中文或者日文改變一個文件的名字而不必對底層操作系統進行任何改變。事實上,許多解析器與編譯器也能夠很好的工作而不用為了支持多字節編碼而進行單獨的修改。我們仍然可以運行與編碼無關的C編譯器,因為多自己序列只會出現在注釋或者文字常量中。

然而,還是有些問題的。我們不能直接這樣寫 char mychar = ‘字’; 因為對編譯器來說這是兩個字符。substring也會有問題,這對正則表達式也會產生影響。假設我們有一個字符串”月月”,它是由四個字節組成的-183 238 183 238。如果我們嘗試搜索字符“詞”,將沒有匹配的結果,除非”詞”的編碼是238 183。大部分情況下,這不是什麼問題,因為我們通過正則搜索的關鍵字都是ASCII碼,但這種情況確實存在。其實,我們卻是有一個問題,幾乎任何標准庫都沒有正式的提出過,那就是顯示寬度的問題。當輸出到固定寬度的輸出設備比如UNIX終端,Windows命令行或者固定寬度的打印機時,大部分CJK字符都會占用兩個ASCII碼寬度。例如,如果我們想要將沖數據庫中查詢的數據很好的打印出來,但是我們只給那些特別長的列分配了20個字符的長度,在C標准庫或者數據庫的字符串函數中都不能計算出前20個ASCII字符的數據的寬度。mblen()能夠告訴我們數據使用了4個字節,但是沒有相應的函數告訴我們那個字符使用了兩個ASCII碼還是一個1個。

 
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved