終於進入最後一個主題了,對很多人來說這次的主題應該才是值得關注的重點吧!前面的主題都在告訴大家文字編碼的歷史以及規格,而這次的主題將是作者我在Unicode程式設計上對於文字編碼所遇到的問題、心得經驗、與對策;但也由於這次內容是以我的經驗與心得為主,因此難免較為主觀。
這次談論的所有話題將圍繞在Windows平臺上,不是因為這是擁有最多桌面用戶的作業系統也不是因為作者我喜歡Windows,而是因為幾乎只有在Windows上寫Unicode程式才會遇到一堆奇奇怪怪的問題;至於在其它的作業平臺上寫Unicode程式,通常來說大家只要像往常一樣的寫程式就可以了!
C/C++與Wide Character
有很多的人會把寬字元和Unicode畫上等號,事實上這是一個不正確的觀念(雖然絕大部分作業平臺上的寬字元的確是採用Unicode編碼)。C語言從C90開始加入了wchar_t這個基本型態,地位與char型態類似,但專用來表達8位元以上的寬字元。從語言標準上來看,這個型態遇到的多數問題之一就是型態尺寸的不統一,一個wchar_t變數所使用的位元組大小在不同系統平臺上是不一樣的,就筆者所聽過的作業系統就有1位元組、2位元組、和4位元組這三種。光是這樣的問題就足以讓寫資料傳輸程式和跨平臺程式的人煩惱了,有些人甚至稱呼wchar_t是C語言上一個錯誤的設計。
Windows上的Unicode程式設計
由於歷史的因素,Windows選擇了UTF-16LE做為系統核心的字元編碼機制,因此在Windows上要寫支援Unicode的程式就會需要使用到寬字元這種資料型別。使用寬字元來寫程式會面臨很多問題,這些問題以及因應對策將在下面一一說明。
使用寬字元型態改寫程式
第一步,你需要使用wchar_t來改寫你的程式,並且記得它佔用2位元組寬度(注意其他平臺不一定是這個值),最好是用它來取代所有原先使用char的地方,除非那個地方的char是真的被拿來處理位元組資料用的。如果你是個程式設計新手,這一個步驟其實沒什麼困難;但若你不巧是個已經擁有一堆寫好的程式的程式老手,也不要太過沮喪,因為最後你可能會認為這個步驟其實是所有問題當中最單純的,因為筆者我就曾經嘗試把自己寫的所有程式改成寬字元版!
使用寬字元版的標準函式庫
有些字串的處理我們並不會自己來做,而是呼叫標準函式庫的字串處理函式。這些標準函式絕大部分有寬字元版的函式可以用,比方說strlen對應wcslen、strncpy對應wcsncpy等等,總之就是strXXX要改成wcsXXX,至於C++的部份是std::string對應std::wstring、cout對應wcout等等。
然後你會發現有些函式並沒有寬字元對應版本,比方說用來開檔的fopen。如果你是個愛遵照語言標準的潔癖程式師,那麼這些問題可能會讓你想發火,因為C/C++標準對於寬字元的支援並不完整。但是如果你願意使用一些非標準的東西,則Windows提供了寬字元版的對應函式如_wfopen對應fopen、以及wmain對應main等等。
指令列模式對寬字元的支援很差
當你終於搞定了寬字元函式的呼叫後,你也許還會發現另一個令人吐血的事情,那就是作業系統本身對於寬字元IO的部份實在是慘不忍睹。下面這段是我用來測試的程式碼,有興趣的人可以在自己的電腦上跑跑看:
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
using namespace std;
#ifndef _WIN32
#error This program is design for Windows!
#endif
int main(int argc, char *argv[])
{
// Test sample :
// [test][測試][测试][テスト][테스트]
const char sample_utf8[] =
"\x5B\x74\x65\x73\x74\x5D"
"\x5B\xE6\xB8\xAC\xE8\xA9\xA6\x5D"
"\x5B\xE6\xB5\x8B\xE8\xAF\x95\x5D"
"\x5B\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88\x5D"
"\x5B\xED\x85\x8C\xEC\x8A\xA4\xED\x8A\xB8\x5D"
“\x00”;
const char data_utf16le[] =
"\xFF\xFE\x5B\x00\x74\x00\x65\x00\x73\x00\x74\x00\x5D\x00"
"\x5B\x00\x2C\x6E\x66\x8A\x5D\x00"
"\x5B\x00\x4B\x6D\xD5\x8B\x5D\x00"
"\x5B\x00\xC6\x30\xB9\x30\xC8\x30\x5D\x00"
"\x5B\x00\x4C\xD1\xA4\xC2\xB8\xD2\x5D\x00"
“\x00\x00”;
const wchar_t* const sample_utf16le = (wchar_t*)data_utf16le;
printf ( "Print by PRINTF : %s\n", sample_utf8);
wprintf (L"Print by WPRINTF : %s\n", sample_utf16le);
cout << "Print by COUT : " << sample_utf8 << endl;
wcout << L"Print by WCOUT : " << sample_utf16le << endl;
system("pause");
return 0;
}
在筆者電腦上測試的結果是:
UTF-8的部份就算了,但是各位可以看到寬字串的輸出簡直是亂七八糟。雖然說以筆者所用的Ubuntu 12.04上也有不支援寬字元輸出的問題,但是反正Linux有UTF-8,所以並沒有什麼實際的影響。那麼如果讓Windows的指令列支援UTF-8呢?我們來試試看將指令列的字碼系統改為UTF-8 (UTF-8在Windows上為65001號字碼頁):
還要記得更改支援UTF-8的字型:
測試結果變成:
至此,UTF-8的問題終於好些了,雖然說我真的不知道為什麼Visual C++的printf和cout結果會不一樣!
把指令列編碼環境改成UTF-8還有其他的副作用:
-
系統基本指令所輸出的所有的文字訊息變成了英文,也就是不能支援UTF-8的意思。
-
這東西總不能自爽就好,其他人寫的程式如果不支援UTF-8則協同工作的問題會很多。
-
雖然感覺上指令模式變成UTF-8編碼了,但筆者使用的過程中發現那些其他程式所自動產生的指令視窗還是原來的樣子!
-
如果你的程式裡有做locale相關的設定那你可能會再次吐血,他們與剛剛所設定的字碼頁似乎是沒有關聯。
總而言之,Windows的指令列對於Unicode的支援實在是零零落落,寬字元行不通、UTF-8字碼支援也不完整,總覺得微軟好像忽視指令列的存在了。寫指令列程式的人就像是被拋棄的孤兒一般(不是只有在文字編碼上的問題而已,我想常在各系統的指令模式下工作的人會懂的),只能在混亂的多語系編碼系統下寫程式。
其他程式庫與程式工具的問題
如果你只在Windows上寫GUI程式、又沒有對於語言標準的潔癖的話,可能可以避開上面所說的各種問題;但是除非你打算所有的功能都自己寫,否則不能躲避其他程式庫或程式工具很可能沒有寬字元版本的問題。當我們好不容易把自己的程式改寫程寬字元版本後卻發現我們所需要的第三方程式庫沒有提供寬字元版本,一切不都白搭了嗎?甚至有時候是開發工具本身不支援Unicode呢,比方說用Dev-C++寫程式的人,或者現在還在使用經典C++ Builder 6寫程式的人。
對於這樣的問題,由於對方的程式碼不是掌握在自己手上的,因此無解。我們只能去找尋支援寬字元的程式庫、升級支援寬字元的開發工具,然而仍不能保證能解決問題,因為有些程式庫找不到替代,而有些工具即使是當前的最新版本也還是不支援寬字元。最後,若我們仍堅持寬字元的話,只能捨棄自己的習慣去更換開發工具,甚至有些東西免不了會需要自己重寫。
跨平臺程式與網路程式的問題
對於這輩子只打算在Windows上開發軟體、並且不碰網路的人可以忽視這個問題的存在,但對於有寫跨平臺程式需求的人而言,我想寬字元的應用應該算個紮紮實實的災難。光是資料寬度的不同就可以引發出很多的問題,再加上各系統對於寬字元的支援存在著各種不同的問題,因此筆者認為使用寬字元來寫程式並不是個值得使用的好方法。
我可以放棄寫寬字元程式嗎?
有鑑於在Windows下寫寬字元程式會面臨如此多的困難,加上許多已存在的舊程式和舊專案,微軟於是提供了放棄寬字元程式的方法,讓身在迷途中的程式人員可以索性回到傳統的開發執行環境下(其實微軟的主要目的是為了向後相容舊的程式才是)。
微軟規定所有的Unicode程式專案都要定義一個「UNICODE」巨集,如此所有對於WindowsAPI的呼叫就會改叫寬字元版本的函式,如果沒有這個巨集則會呼叫傳統編碼的函式,但也因此讓原來就亂糟糟的windows.h真正成為程式人員的噩夢。想當初我在自訂的class下寫了CreateObject函式,執行起來卻是異常的奇怪,除錯老半天才發現問題的根源就是那個萬惡的windows.h。
由於Windows需要提供一致的API介面,又需要很容易的切換為寬字元模式以及傳統字元模式,因此Windows API中所有和字串有關的型態通通以TCHAR這個巨集或它的衍生巨集來定義,這個巨集在檢測到UNICODE巨集時會被定義成wchar_t,反之被定義成char。同時微軟也期望所有的程式人員以TCHAR或其衍生巨集來取代原來程式中使用char或wchar_t的地方,這樣當你有一天想要嘗試寬字元程式的時候才能比較順利。當然,先決條件是你要能忍受那個萬惡的windows.h!
再來是字面字串的問題,由於微軟希望大家的程式都可以很順利的切換為寬字元模式或傳統模式,但是這兩種型態的字面常數宣告方式又不一樣,寬字串要寫成「L"Text"」而一般字串為「"Text"」。因此微軟又希望大家都把字面常數字串以「_T」巨集包圍使成為「_T("Text")」,至於那個「_T」會在不同的模式下被切換為不同的修飾字。
我所認為的最佳做法
寫個寬字元程式會遇到那麼多奇奇怪怪的問題,就算我想要放棄寬字元程式,但只要心中存有一點未來可以更改為寬字元程式的想法,則程式寫起來只能用痛苦來形容。最後我得出一個結論,就是不要用寬字元寫程式,還是回歸以往的char最好了。但是我們怎麼能夠自外於Unicode的潮流而反著科技的發展方向來行進呢?
因此我主張最佳的做法是程式裡面只使用char及其衍生的類型,並且唯一使用UTF-8字串編碼格式。這樣我們只要在Windows API呼叫時的資料傳遞上將UTF-8格式的字串與UTF-16LE格式的寬字串做互相轉換就行了,這樣做的好處有:
-
可以拋棄所有的TCHAR、_T等不便,對於大部份的程式碼來說只要像往常一樣的寫法就可以了。
-
幾乎所有的作業系統對於8位元字碼的處理都很健全。
-
絕大多數的第三方程式庫幾乎都可以良好的處理UTF-8字串,事實上很多程式庫根本就不管它到底實際上使用的是什麼編碼。
而使用UTF-8的唯一壞處就是當我們要呼叫Windows API時需要多一層轉換。
最後,雖然我沒有確實察明,微軟應該也不會公開承認,可是我總覺得微軟刻意的在冷落UTF-8,相關徵兆大家將會在下面看到。
Windows程式的UTF-8字碼轉換
在Windows程式裡要將UTF-8字串與寬字串做轉換,我們可以自己來(像筆者就是自己做這部份的工作),或者使用系統API。Windows API提供了WideCharToMultiByte與MultiByteToWideChar兩個函式用做一般字串與寬字串的互相轉換,而若在字碼頁參數選項傳入CP_UTF8則其效用將等同於UTF-8與UTF-16LE的字串轉換。
可見Windows內部是有實現UTF-8字碼系統的,而且微軟把UTF-8當成傳統多國語系編碼裡的其中一種編碼方式。雖然UTF-8屬於Unicode編碼,但由於他與傳統多位元組編碼性質實在太接近以至於程式員可以這樣子來學習與理解UTF-8,這點我們從前有提過。雖然這麼做有點汙辱UTF-8的名號,但若這樣能解決問題的話又何嘗不可?於是我們來看看Windows的地區及語言設定:
這設定中的「非Unicode程式的語言」就是指當執行非寬字元程式時系統應該要使用哪一個編碼頁的意思。不過很可惜的,當中並沒有UTF-8這項。作者我從前就常常望著這個畫面在幻想如果微軟在這上面建立一個使用UTF-8語言的虛擬國家的話該有多好?Windows上一切對於Unicode的阻力就都沒了!況且實做的機制微軟都早就做出來了不是?不過幻想歸幻想,微軟最終就是沒有這樣做。那麼別的地方有沒有可能出現具有希望的設定呢?我想有深入研究Windows的人可以試試,作者我是沒有找到,要是找到了就不用寫這麼長的探討文字編碼與解決方案的文章了!
程式碼檔案格式
談了那麼多程式執行時期的編碼轉換,當然我們不能忘了程式碼本身也有字碼格式的議題。其中最讓我不解的是,既然微軟自己也那麼想推動Unicode (UTF-16),又為什麼所有的文字檔案在存檔時卻是預設為傳統多語系編碼?我想這個問題應該是當今所流通的檔案中仍然存在著大量傳統語系編碼格式的最大原因吧!
微軟自己出的程式工具對於UTF-16一向都有很好的支援(指令列程式除外),但是把檔案存成UTF-16最大的問題就是微軟以外的程式工具不支援的比例很高,如果讀者中有想寫網頁程式或是跨平臺程式的人,我還是奉勸大家使用UTF-8存檔會比較好。
雖然使用UTF-16會遇到軟體不支援的問題,但悲慘的是使用UTF-8也會遇到BOM的問題。筆者我就曾在Visual C++ 2008上使用UTF-8(No BOM)程式碼遇到鬼打牆的問題,重點是Visual C++的文字編輯器設定選項裡明確的有著對於無BOM UTF-8格式的支援。最後我把程式碼加上BOM記號,結果就沒有問題了…… 但問題還沒結束,因為這份程式碼變成在Linux下不能被編譯……
筆者我並沒有收集所有版本的Visual C++編譯器,但曾聽說版本2003以前的Visual C++是可以和無BOM的UTF-8相處融洽,看來這個功能是被刻意拿掉的呢!
我想,若要完全避免程式碼所造成的有的編碼問題的話,唯一的解法大概就是嚴格的只用純ASCII來寫程式碼了。
Windows記事本
由於Windows的使用者眾多,加上一般人不太會額外去安裝其他的軟體,因此Windows記事本軟體便順理成章成為世界上最多人使用的文字檔案編輯與檢視器,如同Internet Explorer一樣。如果說這個記事本本身做的不錯也就算了,偏偏這個程式存在著不少令人詬病的問題。對於軟體本身編輯檢視上的功能虛弱就不多提了,這次只探討與今日主題相關的編碼格式部份,請先看一看大家應該都熟悉的Windows記事本的存檔畫面:
不曉得有多少人在看過這一系列文章前會去注意到我們一般所認知的「純文字」檔案竟然還有不同格式?在這個Windows記事本上我們就看到了四種,請看下面說明:
-
ANSI:
其實這個格式和ANSI一點關係都沒有,除非你文件中的字元都在ASCII的範圍內。其實微軟的ANSI格式指的就是傳統的多語系編碼,也就是記事本將會使用在地區及語言選項中那個非Unicode語言所設定的字碼頁來存檔的意思。
-
Unicode:
我想微軟大概認為全世界只有UTF-16LE是正統的Unicode,其他編碼通通都不看在眼裡吧!不錯,這個選項指的就是UTF-16LE。
-
Unicode big endian:
又臭又常,不過這個就是指UTF-16BE的意思。
-
UTF-8:
嗯,與前面不同,完全沒有Unicode的字樣在裡面,大概是想要呼嚨普通老百姓說這個東西和Unicode沒有關聯吧!另外,以這個格式存檔的話會被強制加上BOM,想改都沒得改。
Windows記事本最大的問題就是儲存文字時預設格式竟然是傳統編碼!真讓人不能相信現在已經是2014年了!大部分的使用者大概都不知道字元編碼是什麼,很多記事本的使用者根本就不是程式人員。偏偏這種純文字的檔案因為輕便簡單使得適合使用的時機非常多,各種軟體、文件、資料等等的簡短說明或簡單使用方法說明等等都很喜歡使用純的文字檔案來儲存,但也因為這些文字編輯器不知為何腦殘的選用傳統編碼做為預設編碼的緣故,使得在2014年仍然處處可以見到不同語系的亂碼文件。
這個問題理論最好的解決方式就是把預設存檔格式改成UTF-8或者至少改成UTF-16,不過目前似乎還找不到更改預設存檔類型的方法。第二個解法就是每次存檔時記得都去改一改檔案格式,但這種做法很累,對於不知編碼為何物的一般使用者來說很快就會放棄了!而且還有一個問題無解,那就是無論怎麼樣都沒辦法存檔為無BOM的UTF-8格式。
因此最佳的解決方案只剩下一個:丟棄如同垃圾搬的Windows記事本,改用其他更好用的文字編輯器,比方說Notepad++。
微軟對UTF-8的態度
綜和全部的狀況,可以非常合理的懷疑微軟有意刻意打壓UTF-8或採取不合做的態度,對於這點作者我實在是不能理解。UTF-8又不像Linux處處與微軟作對,難不成好好的支援UTF-8會讓微軟損失什麼商業利益不成?明明很多技術的東西都已經實做在Windows內部,但就是不放出良好支援的介面。尤其維基百科上還寫了這樣一句話:「自1996年起,微軟的CAB(MS Cabinet)規格就明確容許在任何地方使用UTF-8編碼系統。但有關的編碼器實際上從來沒有實作這方面的規格。」雖然不明白緣由,但微軟只在非支援不可的地方心不甘情不願的支援一下這個現象,看來是實際存在並且應該會持續很久的事情。
結論
在經歷一連串的編碼歷史、原理、和問題探討後,對於現代寫程式、或是寫Unicode程式時對於字元編碼的部份,總結以下結論:
-
不要用寬字元來寫程式,使用原來用的多位元組模式就可以了,問題也是最少的。
-
建議把原來多位元組格式的文字當作UTF-8編碼來對待,在呼叫Windows API或使用COM元件等時機再將字串格式做轉換。
-
建議純文本文件(如程式碼等)使用無BOM的UTF-8格式來儲存;若這樣會對某些解析程式造成問題的話再存成帶BOM的UTF-8;若同一份文件不論帶不帶BOM都會有某個解析程式發生問題的話,請換工具、或捨棄某些工具、或用純ASCII文件格式來解決吧!。
-
任何的文字文件請不要再存成傳統編碼了!對於不需要送給解析程式處理的文字檔案,其實無論存成UTF-8、UTF-16、帶BOM不帶BOM都沒有關係,只要記得都存成Unicode就好,大家都為掃除亂碼盡一份力。
-
建議拋棄Windows記事本或與其同等弱智的文字編輯器,改用其他的。
參考資料:
-
大五碼、倚天中文 – 小木偶的網頁
-
國內中文字碼之發展 – 交大資科BBS
-
王安電腦的興衰 – Jserv's blog
-
「許功蓋」的問題 – 網管很忙
http://203.64.20.7/lifetype126/index.php?op=ViewArticle&articleId=15&blogId=1
-
中文標準交換碼全字庫
-
Unicode編碼表
-
淺釋Unicode – novus
http://novus.pixnet.net/blog/post/30371481-%E6%B7%BA%E9%87%8B-unicode
-
談程式碼使用的字集 – novus
-
每個軟體開發者都絕對一定要會的Unicode及字元集必備知識 – 周思博
http://local.joelonsoftware.com/mediawiki/index.php/The_Joel_on_Software_Translation_Project:%E8%90%AC%E5%9C%8B%E7%A2%BC
-
Unicode是用幾個位元來進行編碼? – LSS實驗室Beta
-
非常經典的UTF-8 – Gea-Suan Lin's BLOG
http://blog.gslin.org/archives/2013/10/01/3670/%E9%9D%9E%E5%B8%B8%E7%B6%93%E5%85%B8%E7%9A%84-utf-8/
-
UTF-8 Everywhere
-
認識中文字元碼 – 中研院計算中心 曾士熊
-
Wikipedia
http://zh.wikipedia.org/wiki/%E5%A4%A7%E4%BA%94%E7%A2%BC
http://en.wikipedia.org/wiki/Wide_character
http://zh.wikipedia.org/wiki/Unicode
http://zh.wikipedia.org/wiki/%E8%BC%94%E5%8A%A9%E5%B9%B3%E9%9D%A2
http://zh.wikipedia.org/wiki/UTF-32
http://zh.wikipedia.org/wiki/UTF-16
留言列表