在Python中正確使用Unicode

Daetalus發表於2015-01-19

正確處理文字,特別是正確處理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有疑惑的話,現在應該都清楚了。下次遇到UnicodeEncodeErrorUnicodeDecodeError錯誤時,就應該完全知道問題出在哪,也知道如何去修復這些問題!

打賞支援我翻譯更多好文章,謝謝!

打賞譯者

打賞支援我翻譯更多好文章,謝謝!

任選一種支付方式

在Python中正確使用Unicode 在Python中正確使用Unicode

相關文章