關(guān)于字符集和亂碼的思考 2011-12-12 16:05:58
分類: Delphi
重要提示:本文并非學(xué)術(shù)文章,本人也并非語言和文字學(xué)領(lǐng)域人士,只是出于好奇心,根據(jù)自己的理解寫下這篇文章。本文的參考文檔都來源于互聯(lián)網(wǎng),而且并未一一考證其準(zhǔn)確性和權(quán)威性,因此本文僅供參考。
字符集和字符編碼的問題一直困擾著我,之間曾經(jīng)多次嘗試把這個問題理解清楚,但始終由于有些細(xì)節(jié)問題無法自圓其說因而放棄。網(wǎng)上的資料多數(shù)描述過于簡單,又或者作者本人對問題也了解不深入,容易產(chǎn)生誤導(dǎo)。最近我終于下定決心將之前對“亂碼”問題的思考更進(jìn)一步,否則將始終是一絲遺憾。這里也不得不感嘆,老外的“科普”做的好啊,網(wǎng)上有很多質(zhì)量相當(dāng)高的文章,表述嚴(yán)密,引用充分,例證豐富,我相信在國內(nèi)各領(lǐng)域的專家也不少,計(jì)算機(jī)和語言學(xué)方面都有很多有建樹的大牛,也許是太忙吧。對我個人而言,最重要的一片文章是“Character set encoding basics”,在本文最后有鏈接的地址,本人的翻譯版本在這里:
http://blog.chinaunix.net/space.php?uid=11187&do=blog&id=30344931.字符集的基本概念
什么是字符集?什么是字符編碼?
按照“Character set encoding basics”文中的定義,字符集的編碼模型分為以下4個層次
1)抽象字符清單Abstract character repertoire (ACR),無序,無編碼;
2)已編碼字符集Coded character set (CCS),有序,有編碼;
3)字符編碼規(guī)則Character encoding form (CEF),有序,有編碼;
4)字符編碼方案Character encoding scheme (CES),有序,有編碼,有傳輸和儲存規(guī)則(字節(jié)序);
這種分層方式,比較偏于學(xué)術(shù)化,不太容易理解。按我個人的理解,GB2312/GBK/GB18030/ASCII這些字符集編碼規(guī)則,由于都基于8-bit字節(jié),是屬于前三層的,可以認(rèn)為是三層合一。如果拿Unicode來說明的話,Unicode中定義的所有字符的集合,是第一層;我們通常說的Unicode編碼,是指的第二層;現(xiàn)在最常見的UTF-8,是指的第三層。當(dāng)UCS-4在以8-bit為基礎(chǔ)的計(jì)算機(jī)中存儲和傳輸時(shí),就要涉及字節(jié)序的問題,就是第四層,分為big-endian和little-endian。
借用“程序員趣味讀物:談?wù)刄nicode編碼”中舉的一個記事本例子(內(nèi)容不同):
1)打開記事本(windows自帶的那個),輸入“我”;
2)另存為 我_ansi.txt,注意,編碼選擇“ANSI”;
3)另存為 我_unicode.txt,注意,編碼選擇“Unicode”;
4)另存為 我_unicode_big.txt,注意,編碼選擇“Unicode Big Endian”;
5)另存為 我_utf8.txt,注意,編碼選擇“UTF-8”;
保存完以后,看一下4個文件的大小,很有意思吧,分別是2/4/4/6個字節(jié),再用二進(jìn)制方式(推薦使用ultraedit)查看一下其中的內(nèi)容:(高位字節(jié)在前)
1)ansi:CE D2
2)unicode:FF FE 11 62
3)unicode_big:FE FF 62 11
4)UTF-8:EF BB BF E6 88 91
第一個文件,ansi,比較好解釋,2字節(jié),就是GB2312/GBK/GB18030編碼,即簡體中文windows的默認(rèn)內(nèi)碼
第二個文件,unicode,就是Unicode編碼,“我”的編碼是0x62 0x11,不過前面多了2字節(jié)的前導(dǎo)符,F(xiàn)F FE,表示為little-endian
第三個文件,unicode_big,也是Unicode編碼,不過前導(dǎo)符變?yōu)镕E FF,表示big-endian
第四個文件,UTF-8,是在Unicode基礎(chǔ)上的二次編碼,分別將FE FF(big-endian)和62 11進(jìn)行了二次編碼,詳細(xì)編碼過程參見“程序員趣味讀物:談?wù)刄nicode編碼”
常見字符集(字符編碼規(guī)則)
ASCII,讀作阿斯克碼,7bit表示,美國國家標(biāo)準(zhǔn)信息編碼,是最常用英文字母和符號、數(shù)字的集合及編碼;它的常見別名是ISO 8859-1 ,Latin1
EASCII,擴(kuò)展ASCII碼,完整的利用一個字節(jié),在ASCII的基礎(chǔ)上擴(kuò)展了一些不常用字符
GB2312,國標(biāo)中文字符編碼,1980年制定并頒布;
GBK,國標(biāo)碼,1995年
GB18030,國標(biāo)碼,2000年
以上這三個編碼標(biāo)準(zhǔn)都是向下兼容的,兼容的意思有兩方面,其一是指字符的集合,其二是指編碼。另外,在微軟操作系統(tǒng)中(其實(shí)也影響到了Linux領(lǐng)域),經(jīng)常出現(xiàn)“代碼頁”(code page)的概念,這些代碼頁,只是微軟自己的定義,可以理解為CP936=GBK。
Unicode,UTF-8,我原來一直以為這兩個東東是一回事,后來發(fā)現(xiàn)其實(shí)理解錯了,UTF-8可以理解為是以Unicode為基礎(chǔ)進(jìn)行二次編碼的,詳見這篇文章:“程序員趣味讀物:談?wù)刄nicode編碼”,
http://pcedu.pconline.com.cn/empolder/gj/other/0505/616631_1.html2.關(guān)于亂碼的思考
什么是亂碼
個人認(rèn)為,如果在儲存或傳輸過程中,計(jì)算機(jī)中的信息不能被正常解析,從而導(dǎo)致在信息展示的時(shí)候出現(xiàn)無法被正確理解的情況,可以認(rèn)為出現(xiàn)了“亂碼”。常見的亂碼有兩種表現(xiàn)形式:
1)部分中文字符能夠正常展示,另外的中文字符被展示為方框;
這種情況多數(shù)是由于缺少相應(yīng)的字體支持,例如,在虛擬機(jī)上安裝完linux之后,如果沒有安裝圖形界面,默認(rèn)的字符窗口其實(shí)是沒有相應(yīng)的字體支持的,這時(shí)的中文只能顯示為方框,安裝zhcon以后才能夠正常展示GBK/UTF8的中文字符。
還有一個場景,部分網(wǎng)頁上的字符,并不能被所有瀏覽器支持,或者該瀏覽器對某種編碼方式的支持不完整,會出現(xiàn)部分字符展示為方框的情況。
另外,如果能夠以GBK/GB2312正常展示的網(wǎng)頁,如果手工將encoding變更為utf-8,則所有中文字符都會變成方框。
2)幾乎所有字符都不能正常展示,許多字符被顯示為“?”,或者被顯示為一大堆不可理解的古怪字符;
這種情況很可能是由于字符編碼不配套,需要具體分析。例如,在瀏覽器中能夠正常顯示的頁面,如果將其編碼更改為其他不兼容的編碼,則很多會展示為“?”和亂七八糟字符的組合
亂碼產(chǎn)生的原因
產(chǎn)生亂碼的原因很復(fù)雜,也正是這個原因?qū)е铝藢y碼問題的分析很難全面和徹底。但是,綜合我目前遇到的亂碼問題來看,只要將字符展示的過程剖析清楚,一段段的調(diào)整,總能找到解決的辦法。
字符在計(jì)算機(jī)中,都是以二進(jìn)制的方式進(jìn)行存儲的,而且文本本身是不能夠標(biāo)識它使用的編碼方式的。也就是說,同一段二進(jìn)制字節(jié)流,可以用很多種不同的編碼方式去解碼,然后根據(jù)解碼后的結(jié)果(也是二進(jìn)制字節(jié)流),在操作系統(tǒng)中按照預(yù)定義好的字體進(jìn)行展示。所謂字體庫,或者字庫,其實(shí)就是數(shù)字和相應(yīng)展示方式(點(diǎn)陣、truetype等)的組合。計(jì)算機(jī)本身是不會“體會”到“亂碼”的發(fā)生的,它只是按照用戶選定的字體,根據(jù)不同的數(shù)字進(jìn)行展示而已,無論展示的結(jié)果如何,都只有人才能判斷“亂碼”與否。亂碼的產(chǎn)生,其實(shí)只有兩個原因,一是沒有使用正確的解碼規(guī)則來解釋字節(jié)流,二是使用了錯誤的展示字體。實(shí)際應(yīng)用當(dāng)中,編碼規(guī)則的問題居多。
單字節(jié)的編碼通常情況下不會出現(xiàn)亂碼的問題,特別是英文字符,而雙字節(jié)由于多數(shù)情況下編碼規(guī)則復(fù)雜,另外存在中間截?cái)嗟膯栴},會比較復(fù)雜。從產(chǎn)生問題的渠道來看,常見的有以下幾類:
1)網(wǎng)頁展示亂碼
多數(shù)情況下,可以通過更改頁面編碼方式來解決。少數(shù)情況下,瀏覽器本身處理多語言字符集有缺陷的時(shí)候,無論怎樣修改編碼方式,都不能徹底解決亂碼問題。例如,截至本文定稿,IE9就存在部分UTF-8中文編碼無法解析的問題,同樣的網(wǎng)頁在Chrome和firefox中都沒有問題。
2)UNIX/LINUX終端顯示亂碼
2.1)終端的中文環(huán)境;
如果沒有合適的中文環(huán)境(字庫支持),無論解碼方式如何正確,也不可能正常展示中文。在常用的終端工具中,例如:Xshell/Secure CRT/Putty,都可以設(shè)置終端的字符編解碼方式,通常設(shè)置的值有兩個系列:
其一,GB2312/GBK/GB18030/CP936/ANSI/Default等,其實(shí)都是兼容的編碼,或者僅僅是名稱不一樣;
其二,UTF-8,這個是在互聯(lián)網(wǎng)上最常見的編解碼方式了;
另外,如果不是windows下的終端工具,而是系統(tǒng)自身的字符終端,則可以安裝字符終端專用的中文環(huán)境,例如linux下的zhcon
2.2)cat顯示文本文檔內(nèi)容
通過類似cat命令的方式顯示純文本文檔的內(nèi)容,通常只受一個因素的影響,即終端的工具的字符編碼方式,常用工具中都可以進(jìn)行設(shè)置。只要文本內(nèi)容的編碼方式與終端的編碼方式一致(或兼容),則一定不會出現(xiàn)亂碼。
2.3)命令行的中文提示(CLI)
命令行接口Command Line Interface的提示語言,是通過環(huán)境變量進(jìn)行設(shè)置的,好幾個變量都可以設(shè)置,但優(yōu)先級有區(qū)別,其中LC_ALL > LC_XX > LANG,如果想用中文顯示提示信息,可以這樣設(shè)置:
export LC_ALL=zh_CN.gbk
其中zh表示使用中文輸出提示信息,gbk表示使用GBK編碼方式輸出中文提示信息,這個編碼方式要與終端的設(shè)置一致或者兼容才可以正常顯示;
2.4)輸入中文信息
從Shell環(huán)境輸入中文,與vi/vim這種編輯器的情況稍有不同,編輯器的情況放到下一節(jié)說明。按照一般的理解(我原來就是這樣理解的),只要能正常顯示中文的地方,一定能夠正常輸入中文。但是,實(shí)測的情況略有不同,詳見下面的表格。
輸入中文信息,我暫時(shí)只考慮了以下三種情況:
a)在SHELL命令行中輸入中文
這種場景下,如果終端字符集是GBK,LC_ALL為UTF-8時(shí),輸入的中文字節(jié)流亂序(第一個中文字符的高字節(jié)被放到字節(jié)流的末尾),無法正常展示。
b)使用cat等方式輸入中文,并重定向到文件中
這種場景下,任何時(shí)候,都能夠正常輸入中文
c)使用文本編輯器,詳見下一節(jié)描述
與僅僅顯示中文信息不同,輸入中文的時(shí)候?qū)嶋H上經(jīng)歷了更多的步驟。最開始從終端工具中輸入中文編碼字節(jié)流,然后經(jīng)過網(wǎng)絡(luò)協(xié)議傳輸?shù)椒?wù)端,服務(wù)端收到字節(jié)流以后,根據(jù)終端設(shè)置的情況,再推送顯示信息到終端工具,終端工具進(jìn)行呈現(xiàn)。在SHELL命令行中輸入中文不正常的情況,很有可能是由于服務(wù)端的處理邏輯不健全。
2.5)文本編輯器,例如vim
文本編輯器的種類很多,emacs/vi等,vi的版本也很多,各個主流UNIX平臺的商業(yè)版本實(shí)現(xiàn)都不相同,還有vim。本文暫以vim為例子進(jìn)行說明,其他編輯器的情況應(yīng)該是類似的。可以參考:“
讓vim認(rèn)識更多的編碼”,
http://blog.chinaunix.net/space.php?uid=20147410&do=blog&id=3018800。
遺憾的是,這篇文章并未給出vim處理這三個內(nèi)部變量的順序,經(jīng)摸索,順序應(yīng)該為:
1)fileencoding
2)endocing
3)termencoding
這三個內(nèi)部變量均可以通過set xxx=xxx的方式進(jìn)行設(shè)置,并可以通過set和set all進(jìn)行查看。另外,上面提到的博文中是使用LANG變量來改變vim的encoding變量值,但由于LANG的優(yōu)先級最低,實(shí)際使用過程中,使用LC_ALL的效果最好,當(dāng)然其實(shí)也可以直接在vim中使用set進(jìn)行設(shè)置。
個人理解,如果僅僅從輸入和輸出(顯示)的角度來看,其實(shí)vim等文本編輯工具并沒有必要設(shè)置3個不同的變量來進(jìn)行處理,這大概也是大多數(shù)商用unix平臺的vi版本都沒有類似設(shè)置的原因。提供3個變量的原因在于,vim試圖提供一些編碼轉(zhuǎn)換的方式,例如,通過設(shè)置fileencoding變量,可以改變vim寫入和讀出使用的編碼,而termencoding僅僅改變顯示時(shí)使用的編碼方式,而encoding其實(shí)只是提供緩沖。這與數(shù)據(jù)庫的字符集(編碼)處理方式是類似的。也可以這樣理解,無論這三個變量設(shè)置為何值,其實(shí)并不見得不會影響數(shù)據(jù)的輸入和展示。例如,在我們輸入輸出中文信息的時(shí)候,即便fileencoding=encoding=termencoding=iso8859(英文字符集),只要文本文件的編碼方式與終端的編碼方式一致(兼容),比如都是GB2312,文本信息都可以正常展示和輸入。
附表:不同設(shè)置情況下的中文顯示結(jié)果
服務(wù)端環(huán)境:ubuntu 11.10 服務(wù)器版
客戶端環(huán)境:中文win7+Xshell 4
文件編碼終端字符集LC_ALLcat輸出SHELL輸入VIM相關(guān)備注
vim顯示vim輸入encodingfileencodingtermencoding
GBKGBKGBK正常正常正常正常GBKGBK
UTF-8GBKGBK亂碼正常正常GBKUTF-8
GBKUTF-8GBK亂碼正常亂碼亂碼GBK如果將encoding或termencoding改為utf-8,則可以正常顯示
UTF-8UTF-8GBK正常亂碼亂碼GBKUTF-8如果將encoding或termencoding改為utf-8,則可以正常顯示
GBKGBKUTF-8正常亂碼亂碼亂碼UTF-8latin1如果將encoding或termencoding改為latin1,則可以正常顯示;或者將encoding和fileencoding改為GBK(cp936)
UTF-8GBKUTF-8亂碼亂碼亂碼UTF-8utf-8如果將encoding或termencoding改為cp936,則可以正常顯示
GBKUTF-8UTF-8亂碼正常亂碼亂碼UTF-8latin1如果將fileencoding改為cp936,則可以正常顯示
UTF-8UTF-8UTF-8正常正常正常UTF-8
3)數(shù)據(jù)庫亂碼
數(shù)據(jù)庫中與編碼/字符集相關(guān)的設(shè)置主要有兩個,一個是數(shù)據(jù)庫本身的編碼,另一個是客戶端環(huán)境的編碼。網(wǎng)上有很多關(guān)于數(shù)據(jù)庫亂碼問題的討論,多數(shù)并沒有涉及到問題的本質(zhì)。數(shù)據(jù)庫中保存的數(shù)據(jù),其實(shí)與文件方式保存的數(shù)據(jù)沒有什么兩樣,都只是字節(jié)流而已,而字節(jié)流本身通常是不能自我標(biāo)識的,例如,如果僅僅根據(jù)二進(jìn)制的編碼,無法判斷出它的內(nèi)容是采用GB2312編碼,還是EASCII編碼,或者是一個圖像信息。也許,正是由于字節(jié)流無法標(biāo)識自己,因此需要有一個參數(shù)來標(biāo)識數(shù)據(jù)庫使用的文字編碼。在客戶端與服務(wù)端的編碼設(shè)置統(tǒng)一的時(shí)候,無論在數(shù)據(jù)庫的字段中存儲什么樣的數(shù)據(jù),都是不影響數(shù)據(jù)的儲存和展示的,原因是,不會發(fā)生編碼轉(zhuǎn)換。
例如,網(wǎng)上很多帖子討論到亂碼問題的時(shí)候,給出的建議都是,將數(shù)據(jù)庫的字符集設(shè)置為utf-8,這當(dāng)然不會有什么問題,utf-8編碼是被最廣泛使用的編碼標(biāo)準(zhǔn),所以支持也相當(dāng)完備,特別是utf-8編碼幾乎可以被所有軟件“識別”出來(特征碼)。這樣一來,實(shí)際上掩蓋了編碼的問題。其實(shí),如果僅僅為了儲存和展示中文信息,將數(shù)據(jù)庫的字符集設(shè)置為iso-8859-1(單字節(jié))編碼,客戶端的語言環(huán)境也設(shè)置為同樣的編碼方式,存取中文數(shù)據(jù),也不會有任何亂碼的情況發(fā)生。之所以產(chǎn)生亂碼,是由于在某些地方出現(xiàn)了編碼方式的不匹配。
比如,數(shù)據(jù)庫的編碼設(shè)置為GBK,但是客戶端的設(shè)置為UTF-8,那么如果在客戶端使用UTF-8的編碼方式輸入中文數(shù)據(jù),當(dāng)客戶端軟件發(fā)現(xiàn)這種不一致時(shí),會執(zhí)行從UTF-8到GBK的編碼轉(zhuǎn)換,然后通過網(wǎng)絡(luò)插入到數(shù)據(jù)庫的具體字段中。當(dāng)這段數(shù)據(jù)被讀取時(shí),如果客戶端的設(shè)置為UTF-8,那么同樣要發(fā)生GBK到UTF-8的轉(zhuǎn)換,最終以UTF-8的形式展示數(shù)據(jù)。但是,如果數(shù)據(jù)被讀取時(shí),客戶端的設(shè)置為GBK,則數(shù)據(jù)無需轉(zhuǎn)換就可以以GBK的形式直接呈現(xiàn),然而,如果客戶端是設(shè)置為UTF-8編碼的網(wǎng)頁,但使用GBK方式訪問數(shù)據(jù)庫,那么數(shù)據(jù)被最終呈現(xiàn)時(shí)就會出現(xiàn)亂碼。
總之,數(shù)據(jù)庫提供設(shè)置數(shù)據(jù)庫和客戶端編碼方式的選項(xiàng),只是為了更好的提供編碼轉(zhuǎn)換工作,并不是必需的,無論設(shè)置成何種編碼方式,與實(shí)際存儲在字段中的數(shù)據(jù)都沒有必然聯(lián)系,只是會在編碼轉(zhuǎn)換的時(shí)候提供方便,否則,這些轉(zhuǎn)換工作就只能完全交給客戶端來完成。
3.尚未解決的疑問
1)關(guān)于windows剪貼板的實(shí)現(xiàn)機(jī)制中,是否包括了編碼轉(zhuǎn)換
從現(xiàn)象上來看,當(dāng)從一個ansi編碼的文本中拷貝中文字符,再到utf-8編碼的文本中進(jìn)行粘貼,沒有出現(xiàn)亂碼,但是這兩種編碼方式是不同的,也就是說,必然在這個過程中出現(xiàn)了編碼轉(zhuǎn)換,個人懷疑是利用剪貼板進(jìn)行復(fù)制的時(shí)候,進(jìn)行了編碼轉(zhuǎn)換,將復(fù)制的文本保存為操作系統(tǒng)內(nèi)碼,然后粘貼的時(shí)候由應(yīng)用程序進(jìn)行內(nèi)碼到utf-8編碼的轉(zhuǎn)換,完成粘貼。
2)輸入法輸入不同編碼的文本時(shí),采取什么機(jī)制?
當(dāng)打開一個cp936編碼的文件進(jìn)行編輯時(shí),輸入法的輸出是cp936編碼的,但打開一個utf-8編碼文件進(jìn)行編輯時(shí),輸入法的輸出變成了utf-8的,輸入法是如何知道什么時(shí)候應(yīng)該使用什么編碼的?個人猜想,有可能輸入法的輸出只是操作系統(tǒng)的內(nèi)碼,在文本編輯器中進(jìn)行內(nèi)碼到其他編碼的轉(zhuǎn)換。