猛然間看到這篇文章,才發(fā)現(xiàn)原來自己理解的uniocde還是表面,這篇文章又說明了很多深層次的內(nèi)容,值得一看。
From:http://jawahh.sjtubbs.org/2008/07/unicodepython.html2008年7月21日
復雜的Unicode,疑惑的Python
Python 3000決定采用Unicode作為字符的默認編碼。這不是什么新聞了,也是國際化的大勢所趨。但實際上似乎沒有那么簡單。最近python-dev郵件列表吵的一個問題就很有意思。
7月2日,一個叫Jeroen Ruigrok van der Werven的人以UCS2/UCS4 default 為 標題說了問題。Python雖然采用unicode作為默認字符,但語言內(nèi)部用什么方法表示unicode字符串并沒有一致的規(guī)定。在編譯的時候可以選擇 用UCS2或者UCS4編碼。作者說,隨著unicode中的CJK象形字擴展B(CJK ideographs extension B)已經(jīng)加入最新的unicode規(guī)范,擴展C已經(jīng)在投票表決階段,擴展D已經(jīng)在開發(fā),使用UCS2編碼已經(jīng)不能滿足預期未來的應用了。所以應該默認使用 UCS4編碼。而且作者認為允許UCS2和UCS4兩種編碼會產(chǎn)生編程一致性的問題。比如,在用UCS2編碼的python 3中:
>>> len("\N{MUSICAL SYMBOL G CLEF}")
2
>>> len("\N{MUSICAL SYMBOL G CLEF}")
1
然后這個問題就引起了一場大混戰(zhàn)。這個問題到底是什么問題呢?到底為什么會允許那么奇怪的事情發(fā)生:同一個字符串在不同的編譯版本中長度不一樣呢?其實這個問題根植在unicode復雜的規(guī)范和歷史中。
Unicode的一個中文名字叫“萬國碼”。這個翻譯很明確指出了Unicode的任務,為人類使用的文字都編一個號碼,解決他們在計算機中共存的 問題,消除計算機世界里面萬碼奔騰的兼容性問題。現(xiàn)在unicode 5.1規(guī)范已經(jīng)于2008-04-04發(fā)布,而且也確實很大部分消除了計算機世界里面兼容問題,成為了事實規(guī)范。現(xiàn)在哪個稍大的新程序不支持 Unicode,是說不過去的。不過這不表示unicode里面沒有什么犄角旮旯影響著大家的使用。
第一個問題是,unicode到底要給什么編號?對中文來說,當然是給字編號——在中文里面是這樣,但unicode并不是中文編碼,這個世界的書 寫文字五花八門,中文概念里面的字在其他書寫文字里面并不一定存在。在這里要澄清的兩個概念是glyph和character。Glyph是文字的圖像, 是我們書寫的最小單元,是屏幕上看到的,打印機上打出來的最小單元。Character是一種邏輯上和語言學上的描述,它并不完全等同于Glyph。 Glyph和Character之間有多種組合發(fā)生。一種情況是一個Glyph代表多個character。舉中文里面的多音字說明,比如“沒”字,它有 兩個讀音,而且意義完全不一樣,但只有一個表現(xiàn)形式。“沒”代表著一個glyph,代表著兩個character。如果再加上日語和韓語,這種同一個 glyph卻有多個character的情況就更多了。還有一個情況是多個character才能組成一個glyph。主要是一些字母的音調(diào)問題。中文里 面也有這個問題,比如“e”和“ˊ”這兩個character可以組成一個glyph“é”。還有一種我們不太熟悉的情況是多個glyph只是一個 character,主要出現(xiàn)在阿拉伯文中。在阿拉伯文中,一個字母在不同的書寫情況下可能有不同的表現(xiàn)形式(glyph)。還有一種情況 是,character沒有對應的glyph,比如我們常見的回車符。事實上,unicode并沒有給出一個標準的說法說明到底給什么編號,它走的是務實 主義。Unicode大致可以說的是給character編號,但也會照顧到各種語言的現(xiàn)實,會給glyph編號。對于編程,一個簡單的計算字符串長度就 會發(fā)生歧義。到底我們是計算unicode字符串的character數(shù)量還是glyph數(shù)量?在ascii編碼中沒有這個問題,因為它的 character和glyph是統(tǒng)一的。Unicode解決這個問題的方法是不僅僅給character編號,還給每個character編訂了 unicode character property。軟件可以計算character數(shù)量,也可以根據(jù)character查詢屬性,用于計算和顯示glyph。
第二個問題是,unicode到底打算使用多少個編號?現(xiàn)在的Unicode使用了21bit的數(shù)字去編號。目前看來21bit在可預見的將來是足 夠使用的——除非人類發(fā)現(xiàn)了外星人文明,需要為他們編號。現(xiàn)在的unicode編號從0x0-0x10FFFF分為17個Plane,編號從0-16。從 0x0-0xFFFD為BMP(Basic Multilingual Plane),也就是前16bit,集中了大多現(xiàn)代書寫系統(tǒng);從0x10000-0x1FFFD為SMP(Supplementary Mulitilingual Plane),包括了大多在歷史上曾經(jīng)使用的書寫系統(tǒng);從0x20000-0x2FFFD為SIP(Supplementary Ideographic Plane),用于每年新增加的象形文字;然后11個plane尚未使用;Plane 14(0xE0000-0xEFFFD)為SSP(Supplementary Special-Purpose Plane),存放一些爭議性比較大的字符(語義上比較模糊或者會給文字處理帶來麻煩),使用這些字符都需要多加小心;后面兩個Plane 15(0xF0000-0xFFFFD)和Plane 16(0x100000-0x10FFFD)為保留區(qū),任何人都可以私自定義這個區(qū)域,當然Unicode規(guī)范也不保證這些區(qū)域可以在異構系統(tǒng)上順利交 換。還有一個特殊字符編號0xFEFF是BOM(Byte Order Mark),包括它對判斷Byte Order有特殊用途,所以它的另外一面0xFFFE也就被規(guī)定為非Unicode字符(也就是為什么Plane的結束都是0xFFFD的原因)。上面的 說法看起來沒有什么太大的問題,但這不是故事的全部。最開始的Unicode只打算用16bit的數(shù)字,也就是現(xiàn)在的BMP去實現(xiàn)它的目的,這個跟當年的 兼容和效率考量有關。但這顯然是不夠的,尤其對于龐大的CJK象形文字——至今Unicode已經(jīng)包含了7萬多個CJK象形文字。這是個不幸的歷史。所以 早期的Unicode實現(xiàn)中,并沒有考慮到16bit以外的問題。比如大量使用的Windows和Java構建的系統(tǒng)。Unix系統(tǒng)倒是塞翁失馬焉知非 福,對Unicode的支持比較晚,所以大多都是用32bit去表示21bit的unicode編號。歷史的悲劇就這么產(chǎn)生了,雖然都是Unicode, 但歷史遺留系統(tǒng)和現(xiàn)代系統(tǒng)的不同表示還是給所有希望實現(xiàn)Portability的應用帶來尷尬的處境。Python的UCS2/UCS4問題就是其中之 一,但這不是造成這個問題的全部原因,還有下一個原因。
第三個問題是,unicode到底是什么?這是個很嚴肅的問題。Unicode只是字符編號,字符編號的屬性,以及相關說明的集合。Unicode 不是平常所說的編碼(Encoding)。Unicode規(guī)范只是規(guī)定了每個字符的編號(Code Point)。雖然它是為計算機設計的規(guī)范,但這個編號和計算機如何存儲,如何表示這些字符沒有直接關系。在這一層,叫做CCS(Coded Character Set)。理論上如何表示Unicode字符是應用程序的自由,喜歡怎么表示就怎么表示,只要你的表示方法能找到字符對應的Code Point就行。當然,大家不能讓這種混亂出現(xiàn),所以有了CEF(Character Encoding Form)這一層。這一層關注的是在計算機理論上如何從數(shù)字到映射Code Point,也就是如何在8bit為單位的計算機系統(tǒng)中表示21bit的Unicode Code Point。其中UCS2,UCS4,UTF32,UTF16,UTF8等等都是實際上使用的方案。理論上映射和實踐上映射不完全一樣,實踐上還要考慮異 構系統(tǒng)的可交換特性,也就是解決大小端問題的CES(Character Encoding Scheme)。所以又會有UTF32-LE,UTF32-BE,UTF16-BE,UTF16-LE,UTF8(對的,UTF8是CEF,也是 CES)。還有最后一層,是TES(Transfer Encoding Syntax)。這一層解決的問題是在特定傳輸環(huán)境中的編碼問題,比如把UTF8字符串再用base64編碼用于電子郵件傳輸。跟Python有關的是 CEF這一層。前面說過,歷史上Unicode的code point是16bit的,所以無論是UCS2,UCS4,UTF32,UTF16,UTF8都可以相安無事。對于前四者來說,都是一個code unit對應一個code point(code unit是CEF的最小單位,對于UCS4和UTF32是32bit,對于UCS2和UTF16是16bit,對于UTF8是8bit);對于UTF8來 說是1到3個code unit對應一個code point。這時候的UCS4和UTF32是等價的,UCS2和UTF16是等價的。后來unicode擴展為21位了。對于UCS4和UTF32來說, 還是一個code unit對應一個code point,對應用程序來說變化不大。而對于UTF8來說,變成了1-4個code unit對應一個code point(為什么是擴展到21bit這么奇怪的數(shù)字呢?我猜測是為了UTF8的效率,因為UTF8中4個code unit正好最多可以表示21bit的code point),因為UTF8本來就是長度可變的編碼,問題也不大。但對UCS2和UTF16來說,問題就比較頭疼了。UCS2和UTF16本來是固定長度 編碼,但現(xiàn)在無論如何也不可能用16bit的存儲表示21bit的code point。UCS2和UTF16在這里就分道揚鑣了。UCS2的處理方法很暴力,只保留低16位信息,忽略高5位的信息,也就是只兼容BMP中的 code point。而UTF16就變成了可變長度編碼。解決方法是在BMP中劃分出兩個保留區(qū)域,分別是0xD800-0xDBFF的High Surrogate Area和0xDC00-0xDFFF的Low Surrogate Area。編碼方案是,假如有一個大于0xFFFF的code point是X,那么讓Y=X-0x10000;Y顯然是介于0x00和0xFFFFF之間的20bit數(shù)據(jù)(這也就是為什么unicode雖然擴展到 21bit,但只有17個plane——理論上21bit可以表示32個plane)。假如Y這個數(shù)字的分隔為高10bit和低10bit(假如是 xxxxxxxxxxyyyyyyyyyy),那么X的UTF16編碼110110xxxxxxxxxx 110111yyyyyyyyyy,正好落在Surrogate Area里面。就這樣UTF16變成了長度可變編碼。用Python表示就是:
high = ((X-0x10000)>>10)&0x3FF+0xD800
low = (X-0x10000)&0x3FF+0xDC00
但對使用UTF16編碼的程序來說,一個字符串的code point數(shù)量(也就是unicode character的數(shù)量)和code unit的數(shù)量不再是恒等的——如果代碼里面曾經(jīng)簡單的恒等這兩個數(shù)量,代碼就出錯了。
回到Python的問題上,由于歷史原因,現(xiàn)有的系統(tǒng)即有使用UCS2(Windows,Java——Java從1.5開始改為支持UTF16)也 有使用UCS4(Unix/Linux)的。為了在各個系統(tǒng)上的最大兼容性,Python的Unicode字符串在內(nèi)存中的表示方式一直都有兩種,在編譯 的時候指定(有--with-wide-unicode的時候用UCS4)。對于同一個不在BMP范圍內(nèi)的字符"\N{MUSICAL SYMBOL G CLEF}",UCS4的Python內(nèi)部表示成一個UCS4 code unit,計算長度的時候自然就是等于一,因為code unit的數(shù)量和code point的數(shù)量是恒等的;但UCS2的Python為了不丟失信息,首先用UTF16的編碼方式把不在BMP范圍內(nèi)的字符編碼成兩個UTF16 code unit,但計算長度的時候,返回的是code unit的數(shù)量,而不是code point的數(shù)量!所以郵件列表上有人說,UCS2的Python用UTF16的編碼處理了字符輸入,又按照UCS2的方式在內(nèi)部使用。UCS2和 UTF16之間的混淆不清大概就是這個問題的根源。
平心而論,UTF16并不是一個糟糕的編碼。它的優(yōu)點是對于大多常用的字符(在BMP范圍內(nèi)的)更緊湊,無論是分割字符,計算字符,都和UTF32 一樣。但問題就在于不是BMP范圍內(nèi)的字符。要以code point為單位處理這些字符的UTF16編碼需要跟復雜和低效的算法,比如隨機訪問字符串中的某個字符從O(1)變成了O(N)。Java也是有這個問 題的語言,它曾經(jīng)是UCS2,但從1.5開始,增加了一套處理UTF16字符的API(String.codePointCount / String.codePointAt / String.codePointBefore / String.offsetByCodepoints)。所以在Java中,原來的代碼會保持原來的UCS2處理方式,當你要使用超過BMP范圍的字符 時,可以使用新的API處理;這樣在兼容和正確處理之間找一個妥協(xié)方案。但Python由于用了兩種內(nèi)部表示方案,問題就變得更復雜。程序員不僅要注意到 code unit和code point的區(qū)別,還要注意到UCS4和UCS2中的code unit區(qū)別。
郵件列表中爭吵到最后的結果大概是在文檔中增加對這些區(qū)別的說明,同時增加一套新的API用于按照code point為單位處理(假如有人做的話),并不改變舊有的API的行為(isalpha之類的API可以改變,因為不影響兼容性)。跟現(xiàn)實世界 javascript:void(0)一樣,歷史問題總是不能完美解決的。
最后推薦一本書,O'REILLY出版的Fonts & Encoding,前半部分關于unicode的討論可以學到不少關于unicode的知識。
的處理