2012年7月5日 星期四

ANSI和Unicode、UTF-8和UTF-16、BOM

這一篇是個學習紀錄,對於字元編碼其實從前是有點知其然不知其所以然,今個兒決定要徹夜苦讀,爬了些文章並自已做了測試,寫下一些學習心得。可能觀念上還是有些問題,如果有高手路過,一定要給小弟指教指教。

先實作看看

二個是字元編碼的規範,先不談理論,我們實作一下,開啟Windows 裡頭的NotePad.exe 然後輸入「DNOWBA的部落格」

接著「另存新檔」,分別存二種編碼檔案:

一個名稱就叫ANSI,編碼選擇ANSIimage

再另存一次,名為Unicode,編碼選Unicode
image

再次打開這二個檔案,比較一下,沒有差別對吧。

image

image

分別在二個檔案ANSI.txt 、Unicode.txt 上滑鼠右鍵選內容,發現不一樣了嗎?二個檔案輸入「看起來」一樣的字,結果檔案大小竟然是ANSI格式的比較小。

image

image

為什麼前面測試要用「另存新檔」,直接儲存的話,不知道檔案是存成什麼格式的呢?

我們再開一次記事本,然後輸入「DNOWBA的部落格」,這次直接儲存吧,檔案名稱就叫Unicode or  ANSI.txt,存完後在檔案上點右鍵內容,大小為14位元組…這樣你知道他存成什麼格式了嗎?
image

好了,這次我們再用記事本,輸入「鱻鱻鱻鱻鱻鱻鱻鱻」,直接儲存,注意到了,這個時候出現錯誤訊息:「這個檔案包含Unicode格式字元,如果您以ANSI 編碼的文字檔案來儲存這個檔案,將會遺失這些字元,若要保留Unicode資訊,請按下面的取消,然後從編碼下拉清單選擇一個Unicode選項」
image

為什麼會這樣呢?為什麼?為什麼?
實作到這邊先暫停,我們可以說一下理論了

ASCII 的產生:

我們在電腦前面看到的字,是有個字元呈現引擎(rendering engine)來實現的。1984的蘋果、1985的微軟相繼開啟了圖形化作業系統的大門,設計了字元呈現引擎來呈現不同的文字,這些文字依照被編寫在一個表,稱之為「字符集」,而字元呈現引擎可以支持不同的「字符集編碼」(有人叫它代碼頁、內碼表),這類的代碼頁叫做「ASCII代碼頁」。
image

image

在電腦世界,所有的數據在存取、運算都要使用「二進制」表示 ( 電腦用高電平、低電平分別表示 1 和 0 ,在電路中遊走交換訊息),所以在電腦上的字也是使用二進制,像a~z、A~Z、0~9 這些字,要用哪些二進制數表示,每個人都可以自已設定(這就叫編碼),但如果大家想要互相通信不造成系統間的混亂,就必須要使用相同的規則,於是乎美國的國家標準學會(ANSI)就制定了ASCII來做為標準。

後來這個ASCII標準也被國際標準化組織(ISO)定為國際標準,稱為ISO 646標準

是美國人制定的,當然系統剛開始時沒人會想到要讓電腦本地化,所以這套內碼表很簡單,字碼是適用於西方的拉丁語系。這邊略舉ASCII碼表的內容:

BIN Dec
10進制
HEX
16進制
字符
00000000 0 00 NUL(null 代表空字符)
00001000 8 08 BS (backspace 代表倒退)
00011000 24 18 CAN (cancel代表取消)
00100001 33 21 !
00100101 37 25 %
00110011 51 33 3
01001000 72 48 H
01111010 122 7A z
01111111 127 7F DEL (delete 代表刪除)

如上圖,ASCII內碼表裡,除了基本的英文字母、數字、標點符號,還有一些給予做為控制用,像倒退、取消都提供給主機外的設備來使用。

這樣的表示法,是用高電平4位,低電平4位,共8位來表示二進制數,所以字元集最多也只能 有256個(2的8次方)。也就是這個ASCII內碼表最多容納256個符號。電腦讀的時候是BIN,編碼的時候方便起見用10進制來編流水號,這邊故意列出最前和最後,從0~127共128個字符欄位就夠用了。注意用位元來看的話,這第0號到127號的最高位(最左邊)都是0,所以扣掉最高位,7位元的ASCII就夠表示當時西方拉丁語系的符號了。

ANSI產生:

對於發明電腦的美國人來說,這些字符集很夠用了,不過隨著圖形作業系統被廣泛使用(WINDOWS 3.1,1991年),但到了面臨電腦全球化的時候,像中文正體字這種非拼音文字系統的國家來說,就是很大的問題了,所以系統上開發了字元繪製的功能,如果我們要在電腦上輸入中文,就勢必也要發展一套像ASCII這樣的內碼表,而容量上要非常大。

事實上在8位元的ASCII內碼表時,有人就覺得字符集後面其實還有128個空位(128-255)沒用到,反正一次就是傳1位元組,不用白不用,像IBM就用了(並命為OEM內碼表)這些空位來放線條繪圖字元之類的怪東東
image

電腦就只能用二進制數來傳資料是不爭的事實,太多人想把這個區域佔為已用,畢竟每個區域使用的文字符號不同,所以ASCII後期就出現了一些變體來處理這種語系的問題。亂了一陣子,這個亂用區總算在ANSI標準(微軟的標準,標準才能賺錢…在WINDOWS上被稱作"Windows (或ANSI) 代碼頁") 下固定下來了。大家(使用微軟系統的人)同意小於128的字元集定義就不動(就是前面的ASCII),而從128開始到255,視區域不同提供不同的字符集,這些不同的系統為了分辨,用不同的頁碼(代碼頁、code page,使用數字來標記)來識別。例如:

950 — 繁體中文(大五碼)
936 — 簡體中文(GBK)
1252 — 西歐拉丁字母 ISO-8859-1

這樣子算是解了燃眉之急了,一些拼音文字用後面的128空位算是措措有餘,不過,128以後的字符集是被多個代碼頁給重複使用的,所以不同語系的電腦,是「不可能」同時顯示多語系的語言,英語+法語 可以、英語加冰島語 可以,但冰島語+法語就不行。在不同國家間卻經常出現不相容的情況。很多傳統的編碼方式都有一個共同的問題,即容許電腦處理雙語環境,但卻無法同時支援多語言環境。

而到了漢字這種單體文字,128-255這128個空位很明顯的就是不夠,漢字的字集太大了,所以採用了雙位元組字元集(DBCS),以二個位元組來安放一個字,意思就是字集的容量上限變成了65536個(2^16) ,對於漢字來說勉強夠用,不過遇到一些不常用字「犇」「鱻」「堃」「喆」之類的字在這樣的系統下就打不出來。所以當時有字符集裡也有一個大家「默認」的造字區,windows也有個造字程式,讓你自已補缺少的字。
image

image

這也解釋了上面我們實作了「鱻鱻鱻鱻鱻鱻」存檔時碰到錯誤訊息的原因了,因為ANSI裡頭找不到他的位置。即然字集裡沒有這個字,我們怎麼打出來的???請耐心往下看。

我想和編字典一樣,這麼大量的字集編下來多少會有「重複」等錯誤的問題。而字元集中的某些字母是一個位元組來存,其他字則要用兩個位元組,這在編譯上也造成了向後移很容易、往前倒推字元的難度,形成「衝碼」的問題。

個人認為最慘的莫過於,因為國情的關係,漢字在不同的地區(中國、香港、台灣)的寫法不大一樣。在編碼上當時就被歸成不同套,所以產生了不同的內碼表,如GB2312、GBK、BIG5…

所以如果是繁體中文版的WINDOWS,和簡體中文版的WINDOWS,預設使用的字符集就是不同。繁體中文的WINDOWS 裝簡體軟體、開簡體文字會呈現亂碼…就是這種亂象

我們的狀況就是如此慘烈,明明開口說的都是中文,文字上雖然略有不同但也多能望文生義。頂多腔不同,卻被活生生切割,可憐的我們要負擔這樣的歷史共業。其他的拼音語系國家沒有這麼沉重的痛,對他們來說一個字還是只佔用一個位元組(8個位元)。而當時這種問題也沒有大多困擾,反正電腦就是一台一台的各自為政,只要不要把檔案搬到其他語系的電腦就沒問題。

對於現在的使用環境來說雖然不是一個很好的解決方案,但當時可是很合適偉大的設定,這也顯示出大家是想盡辦法為不同的語系來編碼。這樣的想法沒有停止,後來由民間團體促成的Unicode 編碼就此誕生了。這個時間點已經是WINDDOWS ME (WINDOWS 2000,2000年)後的事。剛好當時網路Internet興起,字串開始不是只在自已的電腦0101的動,開始也透過網路線在電腦間移動,竄來竄去的結果就竄出一堆火來,用中文語系的電腦上個日文網頁就都是亂碼…中國人逛中國人的網頁…竟然還會亂碼…中國人有這麼亂嗎?這真的會讓人一把火…

讀語文系的我,剛好經歷過那一段電腦歷史,真的很無言感慨,所以我覺得如果是要編寫程式的中國人,必須要了解這一個歷史,記取教訓,就是這種無聊的政治角力,讓二岸的知識學問無法交流。

剛好 Unicode 在那時已經成型的差不多了,成了名符其實的救火大隊…

Unicode 的產生:


Unicode 又叫統一碼、萬國碼、單一碼、標準萬國碼,目的就是要統一字符集,解決上述的問題。這個統一的方式很厲害,目標是把世界所有的符號都用單一個字元集(就是不要頁碼了),所有的書寫系統全部都在同一頁上,很偉大的目標,花了十幾來年,目前來說,很溫和的成長著。

我們可以用WINDOWS裡的Charmp.exe工具把unicode叫出來看一看 (windows2000/xp/7都有,現在知道為什麼是2000以後才有了嗎?)

如下圖,「 H」的unicode是U+0048,就算你換了字型(FONT),同一個符號的字碼還是一樣,「H 」還是 U+0048。這種想法沒什麼好爭的吧。
image

所以在漢字裡也是這樣,長得類似的字也是被歸為同一個字碼,我們繼續操作這個charmp字元對應表,例如愛這個字
image

image

image

上面三個圖是分別用日文漢字(平假名)、中文繁體注音符號、中文簡體拼音的方式找到的愛字,在意義上,三個字的意義是相同的,如果是之前的BIG5碼,表意文字系統的學者,就會刻意把這三個字放在同一字碼不同代碼頁上。不過到了想大一統的Unicode,大家都要編在同一張代碼頁上,所以就用長像來區分,日文、中文的愛是長得很像的,所以都被歸為U+611B,但簡體的愛長得不同,被放到U+7231去了。

U+在這裡是一個識別的符號,目的就是分辨這個字是什麼編碼,相當於十六進制數的「0x」,而U+後面的數字我只能說是透過code point 技術來編出的號碼(有人叫他魔法數字」,不是十六進制數,上圖中括號裡的才是十六進制,只要是Unicod的字集就一定會有0x,字集中的每個字都一樣有前綴字「U+」,所以也可以不看作是字集編碼的一部分,像上面的愛,簡單的說就是編在88A4這個位置,所以還是雙位元組編碼的方式。

Unicode 的編碼方式也是 16 位,理論上一共最多可以表示216(即65536)個字元,但用了「輔助平面」的方式,使得可以涵蓋一切語言使用的符號。

有關 Unicode 編碼方式相關的內容,老實說我已經「很多」看不懂了,我只知道中間有個code point 的技術,而且Unicode是目前最好的編碼方案,下面可能理解上有錯,請小心= =

我們拿工具UltraEdit來檢視一下文字符號在各編碼格式下的長像好了:

用UltraEdit軟體打開剛剛建置的ANSI.txt,看看他十六進制的情形
image

換個方式寫好了,底下是ANSI.txt
image

這個是Unicode.txt
image

注意二件事:
◎中文字「的、部、落、格」的代碼已經完全改變的,不變的是不管用ANSI還是Unicode都是十六進制數。
◎英文字裡,Unicode維持一貫的體系,為英文字、數字、拉丁語系的常用的標點符號,不過保留是保留,卻都被強制在低八位加上00。

UTF-8 和 UTF-16

上面這種編碼法,是Unicode的本意,據說是要解決不同系統開發廠商系統存取資料的方法,就把所有的符號一律改成16進制數,沒用到也得用0來補…至於要解決系統開發廠商什麼問題,用Mac和Microsoft二個開發系統來說好了,二個系統對字節的理解順序是不同的,Mac 系統主要是 Big Endian(BE), PC 系統則是使用 Little Endian(LE)。是電平讀取順序的問題,為什麼Mac要先處理高電平,再處理低電平…為什麼微軟就要用LE,先處理低八位再處理高八位,為什麼二個不統一一下呢?說真的只有他們自已知道。

以上面的文字字串「部落格」來說,
若是用LE,那麼Unicode存到記憶體是        E8 90    3D 84    3C 68
若是用BE,那麼Unicode存到記憶體會變成 90  E8   84 3D     68 3C
這樣子的話二個系統間交換資料時不就大亂

所以設計Unicode的人又被迫想一些厲害有趣的方式來解決問題,因為Unicode存取任格字符都是用二個字節的空間,所以為了解決BE和LE的問題,每個Unicode字串的開頭存一個FE FF,稱之為Unicode Byte Order Mark,簡寫就稱作「BOM」,中文叫中文叫「位元組順序記號」,如果因為系統不同,高低位元組對調,標記就會變成FF FE,讀字串的人就知道其他位元組都要對調。其他的字碼就可以反向得到正確的資料了。

我們用UltraEdit軟體再次打開Unicode.txt,前面的二個位元組就紀錄了所謂的BOM , FFFE 。這是軟體在儲存檔案時自已偷偷加進去的。(當然不是每個軟體開發者一定都會做BOM的機制來存Unicode)
image

這個方式感覺很理想,不過在實際使用上,卻被一些程式設計者抱怨,抱怨的無非就是以前用「44」一個位元組就可以得到「D」這個字,現在要用「4400」二個位元組,看看那些多出來的0,英語語系國家只會覺每個字元佔用2個位元組太浪費了,因為英語、數字、符號這些在ASCII 只佔了1Byte,ANSI 也保留了0-127位供ASCII使用,所以本來用16位元的Unicode ,出於節省的目的,也就發展了Unicode轉換格式(Unicode Transformation Format,簡稱為UTF)

如果一個僅包含基本7位ASCII字元的Unicode文件,每個字元都使用2位元組的原Unicode編碼傳輸,其第一位元組的8位始終為0。這就造成了比較大的浪費。對於這種情況,可以使用UTF-8來進行演算,將字元轉換成一種可長可短的編碼,這樣可能節省大量的容量。對於網際網路傳輸資料尤其節省頻寬,所以成了電子郵件、網路檔案傳送最愛的一種編碼格式。

標準的Unicode是使用16位,而透過UTF-8轉出來的字符,如果是英語、數字…等,就會被轉成8位,其他的字才是16位;為了區別這個差別,有人會稱Unicode是UTF-16,其實UTF是沒有在轉換16位的,因為Unicode本來就是16位,不需要再轉換,所以Unicode基本上就是UTF-16。

UTF-8對於英語系的國家來說是很好的節省方式,對於中文來說呢?我們再把TXT檔拿出來說明一下檔案大小的問題。

ANSI:
字串:D   N   O   W   B    A   的   部   落  格
大小:1 + 1 + 1 + 1 + 1 + 1 + 2 + 2 + 2 + 2  =14 位元組

UTF-16:
字串:D   N   O   W   B    A   的   部   落  格
大小:2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2 + 2  = 20 位元組
和上面的Unicode.txt的圖對照,怎麼內容顯示的是22位元組?因為BOM的FFFE佔二個位元組啊!,所以20+2=22

UTF-8節省了空間,不過因為一下長一下短的編碼反倒是花費演算的時間,降低了編碼的效率問題。為了解決BE和LE的問題,不知道哪個鬼才搞了個BOM,在UTF-8上面還是一樣,至今仍然有的軟體,還是在我們不注意的情況下加上BOM,如下圖 UTF-8 就是EF BB BF…這次還有三個字元。
image

不過還好的是UTF-8並不像UTF-16 那樣,一定要加上BOM檔頭,UTF-8因為在資料的判讀比較容易,用「逐一比對」的機制可以判斷出來所寫的內容是ANSI編碼還是UTF-8編碼(例如: 連續3個byte為 [E0~EF] , [80~BF] , [80~BF] 或 連續2個byte為 [C0~DF] , [80~BF] 就一定是UTF-8 的文件),所以也可以存成檔頭不含BOM表的UTF-8格式。

前面提到這個UTF-8是節省了空間,如果在網路發達的現在來說是很利於訊息傳輸的,但對使用漢字的中文Chinese、日文Japan、韓文Korea 的人來說可能就不大樂觀了,怎麼轉換的我不知道,竟然把漢字集搞到3個位元組了。下圖是表示UTF-8和UTF-16在字符大小的比較,漢字在欄位上是CJK,UTF-8佔三個位元組。image

那麼對使用中文的我們來說,這樣子反而浪費了空間了,沒錯,所以我們的硬碟就是要買的比外國人大顆,怎麼國外的APPLE手機來台灣後,台灣一定買最高容量…國外16G很夠用了,我們的中文因為UTF-8的關係,消耗的內存就是比人家多。

不過網路世界不能這麼算,因為雖然我們看到的是中文網頁,都是中文字的情況下讓我們想用UTF-16來節省一下,但實際上,網頁背後執行的英文程式也得算進去,一篇文章算好了,是中文多還是英文多呢?表音文字和表意文字的差異這個時候也看出來了,所以就讓外國人佔便宜吧,在網頁上改成UTF-16的人,反而會讓檔案變大。
(相較於網路程式,支持多國語言的單機軟體開發商愛用的是UTF-16喔)

好了,我們來算算檔案大小吧
UTF-8:
字串:D   N   O   W   B    A   的   部   落  格
大小:1 + 1 + 1 + 1 + 1 + 1 + 3 + 3 + 3 + 3 + 3 (BOM佔的Byte) = 21
image 

BOM BOM BOM

以前有人在寫PHP程式的時候,經常會發生一個問題,就是在INCLUDE文件時,在前台顯示時會多了一條縫隙或空格,導致版面大亂,不管怎麼修改HTML或CSS都沒有用,這個時候,你可能就是踩到地雷 BOM 了。

歸納前面的理論,Unicode 格式是大家最愛使用的,而實現的方式 可以是UTF-16 或是後面的 UTF-8 ,UTF –16 因為字節都是2個位元組,在辨別(演算)難度上的關係在檔頭加了FFFE做為BE、LE系統的讀取順序辨別。到了UTF-8 因為字符編碼長度不一,相對辨別度變的較高,所以BOM不加在表頭也很容易透過軟體程式去識別。不過舊有的BOM支持者還是在UTF-8上加了EF BB BF,這個BOM不是用來區別先讀高八位還低八位的問題,唯一的價值只是告訴別人,這個檔案用的是UTF-8格式,謹此而已。

網際網路時代,W3C是沒有在承認這種用了utf-8還加bom的怪異行徑,因為網際網路可是穿梭在世界各個角落,大於127的有上百種的編碼方式,想猜都猜不到,這時候的發展,網頁上有個位置,可以自已承認自已的網頁用了什麼編碼格式,不用別人的瀏覽器一直猜一直猜你是誰。例如:

電子郵件email:電子郵件來說,郵件表頭應該會有一個字串
Content-Type: text/plain; charset="UTF-8"

網頁:HTML檔案的Content-Type放在HTML檔案裡
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

網頁的meta tag,一定要放在<head>段落非常前面。因為網頁瀏覽器一看到這個tag就會停止分析,然後改用指定的編碼方式重新解析整個網頁。

所以在寫程式中發生這種問題的,大概都是踩到了bom,原因可能是編寫程式的軟體出了問題,最典型的是,以前在人在windows上架apache,然後放php網頁,要修改直接拿微軟的notepad改,一存成utf-8就中了bom了,微軟的notepad會在存檔時幫你把bom塞到檔頭去…

image

參考文章:
http://zh.wikipedia.org/wiki/ASCII

http://zh.wikipedia.org/wiki/%E5%A4%A7%E4%BA%94%E7%A2%BC#.E5.AD.97.E7.AF.80.E7.B5.90.E6.A7.8B

http://zh.wikipedia.org/wiki/%E5%AD%97%E7%AC%A6%E9%9B%86

http://zh.wikipedia.org/wiki/Unicode

http://zh.wikipedia.org/zh-tw/%E4%BB%A3%E7%A0%81%E9%A1%B5#Windows_.28ANSI.29.E4.BB.A3.E7.A2.BC.E9.A0.81

http://zh.wikipedia.org/wiki/UTF-8

http://zh.wikipedia.org/wiki/UTF-16

http://www.asciima.com/

http://atedev.wordpress.com/2007/09/19/bom-bom-bom/

14 則留言:

  1. 謝謝 寫得很仔細! 前陣有遇到過Bom不過不是在寫程式 而是在用cue檔的時候常常會亂碼,現在終於懂了Bom是什麼東東 哈哈

    回覆刪除
    回覆
    1. 謝謝你的鼓勵,只是寫出我知道的,很高興也能解決你的問題。

      刪除
    2. 謝謝你的鼓勵,只是寫出我知道的,很高興也能解決你的問題。

      刪除
  2. 這篇真是太受用了,寫的超詳細。

    回覆刪除
  3. 我有個記事本用左unicode格式來存儲(因為裡面有D簡體字)
    我有點原因 我洗機了
    我又忘記copy個記事本
    那我洗機之後就用《安易数据恢复软件》
    將個記事本恢復 回來
    但弄回來之後就一大堆亂碼
    我想知道有沒有辦法弄好那些亂碼呢
    變回原本的樣子

    PS:
    不是unicode格式的記事本 恢復后是正常的(那些字都正常可以看到的,跟之前一樣)
    就是unicode格式的記事本就變了亂碼看不到

    回覆刪除
  4. 雖然不能解決我面臨的問題,但就觀念上很實用

    回覆刪除
  5. 感謝你的撰寫,寫得非常詳細

    回覆刪除
  6. 寫的真的很棒, 受益匪淺 感謝大大!!

    回覆刪除
  7. 謝謝分享 但是BE LE的例子好像寫反了?

    回覆刪除
  8. 很專業, 很用心, 給你大大讚

    回覆刪除

Related Posts Plugin for WordPress, Blogger...
// Dnow Function