正確處理文本,特別是正確處理Unicode。是個老生常談的問題,有時甚至會難倒經驗豐富的開發者。並不是因為這個問題很難,而是因為對軟件中的文本,開發者沒有正確理解一些關鍵概念及其表示方法。在StackOverflow上搜索關於UnicodeDecodeError相關的問題,可以看到很多人都有這樣的誤解。這些錯誤的概念可以追溯到Unicode出現之前。那時許多現今的開發者還沒入職,也包括我自己。如果這些錯誤的概念沒有散布開來,其實不是個問題。現在很多人都有這些錯誤概念,部分原因是因為有些非常流行的語言傳播,甚至固化了這些錯誤概念,使得糾正起來反而變得很困難。
根據對Unicode的支持情況,編程語言可以划分為4類:
-
在Unicode出現或流行之前編寫的語言。C和C++就屬於這一類。這類語言對unicode的支持參差不齊。或沒有內置到語言中,或很難正確的使用。因此開發者常常會用錯。
-
對Unicode支持稍好一點。這些語言在Unicode廣泛流行后才出現的,但語言中對unicode的操作方式是嚴重錯誤的。雖然這些語言誕生較晚,但依然含有第一類語言中的所有缺點。以我的經驗,其中代表語言就是PHP。盡管還有其他語言也同樣糟糕。
-
對Unicode支持基本正確,但有少數致命缺點的語言。這一類語言比較“現代”,且能理解Unicode,但依然無法讓開發者正確的處理unicode,導致在這些語言中對unicode會出現一些嚴重不足。讓我很沮喪的是,Python 2.x就屬於這一類(下文會詳細介紹)。
-
能正確處理Unicode的語言。這些語言完全支持Unicode,可以用Unicode方便快速的完成任務,且不易出錯。Java和.NET平台就屬於這一類語言。
那么,Unicode到底是什么,我們在Unicode上犯了哪些錯誤?Joel這篇The absolute minimum every software developer absolutely, positively must know about unicode絕對是每個軟件開發者必須閱讀的文章。為了為簡潔起見,以及照顧那些天生耐心不夠的朋友,我會在本文中對其進行總結。
字符和字節
基本事實是,若想正確的處理文本,就必須了解字符的抽象概念。不嚴謹的定義一下,字符表示的是文本中的單個符號。更重要的是,一個字符不是一個字節。我再強調一遍!一個字符不是一個字節!!!而且,一個字符有許多表示方法,不同的表示方法會使用不同的字節數。就像前面我說的那樣,字符就是文本中最小的單元。
Unicode以大家都認可的方式定義了一系列的字符。可以將Unicode理解成一個字符數據庫,每個字符都與唯一的數字關聯,稱為code point。這樣,英文大寫字母A的codepoint是U+0041。而歐元符號的codepoint是U+20A0,其他類似。一個文本字符串就是這樣一系列的codepoint,表示字符串中每個字符元素。
當然,你遲早會需要儲存和傳輸這些理論上的Unicode字符串。如果選擇一種其他人可以理解的方式以字節方式進行表示,就可以以大家都理解的方式互相發送文本。這里就需要引入字符編碼(encoding)。
字符編碼是在理想的字符和實際的字節表示方法之間的映射。這種映射無需面面俱到,即在某種編碼中也許無法表示一些特定的字符。同時也無須為每個字符使用相同的內存空間,譬如某些字符使用單字節編碼,而其他字符需要多個字節。
由於同一個字符的字節表現形式不止一種。這意味着當遇到了一串字節,如果不知道使用的是什么編碼,即使知道這些字節表示的是文本,也不知道是什么意思。所能做的就是猜使用的編碼。簡而言之,字節不是文本。即使忘了文中介紹的所有內容,也要記住這句話。為了讀寫文本,歸根結底就是要知道其中使用的編碼方式,不管是從約定、標識信息、或是其他方法得知。
Python是如何處理Unicode
從這里開始介紹Python的Unicode支持。在Python的類型層次中,有3種不同的字符串類型:“unicode”,表示Unicode字符串(文本字符串)、“str”,表示字節字符串(二進制數據);“basestring”。表示前兩種字符串類型的父類。在我看來,Python在這里犯了一個錯誤,根據前面的定義,這讓Python成為第三類語言,而沒有成為第四類。
我用了很長的篇幅苦口婆心的強調字節和字符在本質上是不同的東西,只有通過字符編碼才能互相轉換。但不幸的是,Python犯了兩個互不相關的錯誤,輕輕松松的就會讓你忘掉這些。
第一個錯誤的嚴重性值得商榷:即將一串字節視為字符串。是否應該這樣做還有爭議。Java和,NET認為這樣做是不對的,而其他一些語言卻持有相反的態度。無論如何,你可能希望對文本進行某些操作,如正則匹配、字符串替代等。將這些操作應用到字節序列上都是沒有意義的。而Python將字節序列作為另一種類型的字符串對待,允許在這兩者上執行同樣的操作。
第二個錯誤的嚴重性大一些,Python試圖在字節串和字符串之間以不為人所察覺的方式進行轉化。在不同的轉換中,在條件允許的情況下,Python會試圖在字節串和unicode字符串直接進行轉換。例如將字節串和unicode字節串連接到一起時。根據前面的介紹,不使用encoding就在不同類型之間進行轉換是沒有意義的。所以Python依賴一個“默認編碼”,該編碼由sys.setdefaultencoding()指定。在大多數平台上,默認的是ASCII編碼。但對於所有轉換,使用這種編碼幾乎都是錯誤的。如果不手動指定編碼就調用str()或unicode(),或是函數以字符串作為參數,但傳遞的是其他類型的參數時,都會使用這個默認編碼。
走出這個unicode困境的一個解決辦法是,調用sys.setdefaultencoding()將默認的編碼設置為真正會用到的編碼。但這樣僅僅是將問題隱藏起來,雖然這樣剛開始能解決一些文本處理問題。但缺乏實際可行性,因為許多應用,特別是網絡應用,在不同的地方會使用不同的文本編碼。
正確的解決方法是修改代碼,以正確的方式處理文本。下面是一些應該做到的指導性意見:
-
所有文本字符串都應該是unicode類型,而不是str類型。如果處理的是文本,而變量類型是str,這就是bug了!
-
若要將字節串解碼成字符串,需要使用正確的解碼,即var.decode(encoding)(如,var.decode('utf-8'))。將文本字符串編碼成字節,使用var.encode(encoding)。
-
永遠不要對unicode字符串使用str(),也不要在不指定編碼的情況下就對字節串使用unicode()。
-
當應用從外部讀取數據時,應將其視為字節串,即str類型的,接着調用.decode()將其解釋成文本。同樣,在將文本發送到外部時,總是對文本調用.encode()。
-
如果代碼中使用字符串字面值來表示文本,總是應該含有’u'前綴。但實際上,永遠不要在代碼中定義原始的字符串字面值。不管怎樣,我自己是很討厭這一條,也許其他人也和我一樣吧。
順便說一句,Python 3修復了這些問題,可以正確的處理unicode和字符串,這樣Python就完全位於第四類中了,更多信息參見官方的更新說明中關於Unicode的部分。
希望這些內容能幫到你,如果對unicode到底是什么,如何處理unicode有疑惑的話,現在應該都清楚了。下次遇到UnicodeEncodeError或UnicodeDecodeError錯誤時,就應該完全知道問題出在哪,也知道如何去修復這些問題!