Unicode之痛
原文地址: http://nedbatchelder.com/text...
譯者: yudun1989 校訂: sicklife
實用Unicode編程指南
這是我在 Pycon2012 所作的演講。你能夠閱讀本頁的幻燈片和文字,或者直接在瀏覽器中打開 演示 ,或者來看現場視頻。
同時,點擊文章的圖片將會進入所在幻燈片的的對應位置,圖片中使用了 Symbola 字體,可是若是想要顯示一些特殊符號的話,則需先將該字體下載下來。html
你們好,我是Ned Batchelder.我已經有十年的Python編程經驗,這意味着,不少不少的時候,我與其餘程序員同樣,犯過不少 Unicode 的編碼錯誤。程序員
若是你和其餘 Python 程序員同樣,那你確定也碰到過以下狀況:你編寫了一段很漂亮的代碼,事情看起來很順。而後某一天一個很奇怪的「方言字符」不知道從哪冒了出來,你的程序中就開始大量涌現 UnicodeErrors 。
你好像知道這種問題應該怎樣解決,因而呢,就去在錯誤出現的地方添加了 encode 和 decode ,可是 UnicodeError 又開始出如今其餘的地方。因而你又在另一個地方添加了 decode 抑或 encode 。在你玩過一段「編碼打地鼠」遊戲以後,問題彷佛被解決。
以後某一天,另外一種「方言字符」又在另一個地方出現了。而後你不得不又去玩這種「打地鼠」直到問題解決掉。編程
如今你的程序終於能夠運行。可是你既煩惱又不適,這個問題花費了太多時間,你知道這樣解決「正確」,因而開始憎恨本身。你對 Unicode 的主要了解就是你很討厭它。
你不想去了解怪異的字符集,你只想要寫一個你認爲不是很糟糕的程序。json
你沒必要去玩打地鼠遊戲. Unicode 會有些麻煩,可是它並不難。瞭解了相關知識而且加以練習,你也能夠方便的優雅的解決相關問題。
接下來我會教給你 five Facts of lie,而後給你一些專業建議來解決 Unicode 問題。下面的內容將會包含 Unicode 基本知識,如何在 Python 2 和 Python 3 中來實現。他們有必定差別,可是你使用的基本策略都是同樣的。
世界 & Unicode瀏覽器
咱們從 Unicode 基本知識開始。網絡
事實之一:計算機中的一切均爲 bytes(字節)。硬盤中的文件爲一系列的 byte 組成,網絡中傳輸的只有 byte。全部的信息,在你寫的程序中進進出出的,均由 byte 組成。
孤立的 byte 是毫無心義的,因此咱們來賦予它們含義。函數
爲了表示各類文字,咱們有大約 50 年的時間都在用 ASCII 碼。每個 byte 被賦予 95 種符號的一種,因此,當我給你發送 byte 值爲 65 的時候,你知道我想表達一個大寫的 A。測試
ISO Latin 1,或者 8859-1 對 ASCII 的 96 種字符進行了擴展。這也許是你用一個 byte 能夠作的最多的事情了。由於 byte 中沒有容量能夠存儲更多的符號了。字體
在 Windows 中增長了另外 27 種字符,這種叫作 CP1252 編碼。編碼
事實之二是,世界上的字符遠遠比256個要多。一個簡單的byte不可以表達世界範圍內的字符。在你玩」編碼打地鼠」的時候,你多麼的但願世界上全部的人都說英語,可是事實並非這樣,人們須要更多的符號來交流。
事實一和二共同形成了計算機設備結構與世界人類需求的一個衝突。
當時爲了解決衝突嘗試了多種途徑。經過一個 byte 來與符號或者字符進行對應的編碼,每一種解決途徑都沒有解決事實二中的實質問題。
當時有不少一個 byte 的編碼,都沒有可以解決問題。每個都只能解決人類語言的一部分。可是他們不能解決全部的文字問題。
人們開始創造兩個 byte 的字符集,可是仍然像碎片同樣,只可以服務於不一樣地域的一部分人。
當時產生了不一樣的標準,諷刺的是,他們都不足以知足全部的符號的需求。
Unicode 就是爲了解決以前的老的字符集問題。Unicode 分配整形,被成爲代碼點( UNICODE 的字符被成爲代碼點( CODE POINTS )用 U 後面加上 XXXX 來表現,其中, X 爲16進制的字符)來表示字符。它有 110 萬的代碼點,其中有十一萬被佔用,因此它能夠有不少不少的空間可供將來的增加使用。
Unicode 的目的是包含一切,它從 ASCII 開始,包含了數以千計的代碼,包含這著名的—-雪人??,包含了世界上全部的書寫系統,並且一直在被擴充。好比,最新的更新中,就有一大堆沒用的詞彙。
這裏有六個的異國 Unicode 字符。 Unicode 代碼點寫成 4- , 5- ,或者 6 位的十六進制編碼,同時有一個 U 的前綴。每個字符都有一個用 ASCII 字符規定的名稱。
因此說 Unicode 提供了全部咱們須要的字符的空間。可是咱們仍然須要處理事實一中所碰到的問題:計算機只能看懂 bytes 。咱們須要一種用 bytes 來表示 Unicode 的方法這樣才能夠存儲和傳播他們。
Unicode 標準定義了多種方法來用 bytes 來表示成代碼點,被成爲 encoding 。
UTF-8 是最流行的一種對 Unicode 進行傳播和存儲的編碼方式。它用不一樣的 bytes 來表示每個代碼點。ASCII 字符每一個只須要用一個 byte ,與 ASCII 的編碼是同樣的。因此說 ASCII 是 UTF-8 的一個子集。
這裏咱們展示了幾個怪異字符的 UTF8 的表示方法。 ASCII 字符 H 和 I 只用一個 byte 就能夠表示。其餘的根據代碼點的不一樣使用了兩個或者三個 bytes 。儘管有些並不經常使用,可是一些代碼點使用到四個 bytes。
Python 2
好,說完了這麼多理論知識,咱們來說一講 Python 2
在 Python2 中,有兩種字符串數據類型。一種純舊式的文字: 「str」 對象,存儲 bytes 。若是你使用一個 「u」 前綴,那麼你會有一個 「unicode」 對象,存儲的是 code points 。在一個 unicode 字符串中,你可使用反斜槓 u(u) 來插入任何的 unicode 代碼點。
你能夠注意到 「string」 這個詞是有問題的。無論是 「str」 仍是 「unicode」 都是一種 「string」 ,這會吸引叫它們都是 string ,可是爲了直接仍是將它們明確區分來。
若是想要在 unicode 和 bytes 間轉換的話,二者都有一個方法。 Unicode 字符串會有一個 .encode 方法來產生 bytes , bytes 串會有一個 .decode 方法來產生 unicode 。每一個方法中都有一個參數來代表你要操做的編碼類型。
咱們能夠定義一個 Unicode 字符串叫作 my_unicode ,而後看這九個字符,咱們使用 encode 方法來建立 my_unicode 的 bytes 串。會有 19 個 bytes ,想你所期待的那樣。將 bytes 串來 decode 將會獲得 utf-8 串。
不幸的是,若是指明的編碼名稱錯誤的話,那麼 encode 和 decode 會產生錯誤。如今嘗試 encode 咱們的幾個詭異的字符到 ascii ,會失敗。由於 ascii 只能表示 0-127個 字符中的一個。然而咱們的 Unicode 字符串早已經超出了範圍。
拋出的異常爲 UnicodeEncodeError ,它展示了你使用的編碼方式, 「codec」 即(coder/decoder),展示了致使問題的字符的位置。
解碼一樣會知道出一些問題。如今咱們去把一個 UTF-8 字符串解碼成 ASCII ,會獲得一個 UnicodeDecodeError ,緣由同樣, ASCII 只接受 127 內的值,咱們的 UTF-8字 符串超出了範圍。
儘管 UTF-8 不能解碼成任何的 bytes 串,咱們嘗試來 decode 一些垃圾信息。一樣也產生了 UnicodeDecodeError 錯誤。最終, UTF-8 的優點是,有效的 bytes 串,將會幫助咱們來建立高魯棒性的系統:若是數據無效的話,數據不會被接受。
當編碼或者解碼的時候,你能夠指明若是 codec 不可以處理數據的時候,會發生什麼狀況。 encode 或者 decode 時候的第二個參數指明瞭規則。默認的值是 「strict」 ,意味着像剛纔同樣,會拋出一個異常。
「replace」 值意味着,失敗時將會返回一個標準的替代字符。當編碼的時候,替代值是一個問號,因此任何不能被編碼的值將會產生一個 」?」。
一些其餘的 handler 很是有用。」xmlcharrefreplace」 將會產生一個徹底替代的 HTML/XML 字符,因此 u01B4 將會變成 「ƴ」 (由於十六進制的 01B4 是十進制的 436 )。若是你須要將返回的值來輸出到 html 文件中的話,將會很是有用。
注意要根據不一樣的錯誤緣由使用不一樣的錯誤處理方式。」replace」 是一個處理不能被解析的數據的自衛型方式,會丟失數據。」xmlcharrefreplace」 會保護全部的原始數據,在 XML 轉義符可使用的時候來輸出數據。
你也能夠指定在解碼時的錯誤處理方式。」ignore」 會直接將不能解碼的 bytes 丟掉。」replace」 將會直接添加 Unicode U+FFFD ,給有問題的 bytes 來直接替換成」替換字符」。注意由於解碼器不能解碼這些數據。它並不知道到底有多少 Unicode 字符。解碼咱們的 UTF-8 字符串成爲 ASCII 製造出了 16 個」替換字符」。每一個 byte 不能被解析都被替換掉了。然而這些 bytes 只想要表示 6 個 Unicode 字符。
Python 2 已經試圖在處理 unicode 和 byte 串的時候變得有用些。若是你係那個要把 Unicode 字符串串和 byte 字符串來組合起來的話, Python 2 將會自動的將 byte 串來解碼成 unicode 字符串。從而產生一個新的 Unicode 字符串。
好比,咱們想要鏈接 Unicode 串 「hello」 和一個 byte 字符串 「world」。結果是一個 Unicode 的 「hello world」。在咱們看來。Python 2 將 「world」 使用 ASCII codec 進行了解碼。此次在解碼中使用的字符集的值與 sys.getdefaultencoding() 的值相等。
這裏這個系統中的字符集爲 ASCII, 由於這是惟一合理的一種猜想: ASCII 被如此普遍接受,它是這麼多編碼的子集,不太會是錯誤的。
固然,這些隱藏的編碼轉換不能免疫於解碼錯誤。若是你想要鏈接 一個 byte 字符串和一個 unicode 字符串,而且 byte 字符串不能被解碼成 ASCII 的話,將會拋出一個 UnicodeDecodeError。
這就是那些可惡的 UnicodeError 的圓圈。你的代碼中包含了 unicode 和 byte 字符串,只要數據所有是 ASCII 的話,全部的轉換都是正確的,一旦一個非 ASCII 字符偷偷進入你的程序,那麼默認的解碼將會失敗,從而形成 UnicodeDecodeError 的錯誤。
Python 2 的哲學就是 Unicode 字符串和 byte 字符串是能夠混合的,它試圖去經過自動轉換來減輕你的負擔。就像在 int 和 float 之間的轉換同樣, int 到 float 的轉換不會失敗,byte 字符串到 unicode 字符串會失敗。
Python 2 悄悄掩蓋了 byte 到 unicode 的轉換,讓程序在處理 ASCII 的時候更加簡單。你付出的代價就是在處理非 ASCII 的時候將會失敗。
有不少方法來合併兩種字符串(一個 byte 字符串和一個 unicode 字符串),全部的方法都會先將 byte 轉換爲 unicode,因此處理它們的時候你必須多加當心。
首先咱們使用 ASCII 格式字符串,和 unicode 來結合。那麼最終的輸出將會變成 unicode。返回一個 unicode 字符串。
以後咱們將兩個交換一下:一個 unicode 格式的字符串和一個 byte 串再一次合併,生成了一個 unicode 字符串,由於 byte 串能夠被解碼成 ASCII。
簡單的去打印出一個 unicode 字符串將會調用隱式的編碼:輸出總會是 bytes, 因此在 unicode 被打印以前必須被編碼成 byte 串。
接下來的事情很是不可理解:咱們讓一個 byte 串編碼成 UTF-8,卻獲得一個錯誤說不能被解碼成 ASCII!這裏的問題是 byte 串不能被編碼,要記住編碼是你將 Unicode 變成了 byte 串。因此想要執行你的操做的話,Python2 須要的是一個 unicode 字符串,隱式的將你的字符串解碼成 ASCII。
最後,咱們將 ASCII 字符串編碼成 UTF-8。如今咱們進行相同的隱式編碼操做,由於字符串爲 ASCII,編碼成功。而且將它編碼成了 UTF-8 ,打印出了原始的 byte 字符串,由於 ASCII 是 UTF-8 的一個子集。
最重要的事實之三:byte 和 unicode 都很是重要,你必須將兩個都處理好。你不能假設全部的字符串都是 byte,或者全部的字符串都是 unicode,你必須適當地運用它們,必要時轉換它們。
Python 3
咱們看到了 Python 2 版本中有關 Unicode 之痛。如今咱們看一下 Python 3,在 Python 2 到 Python 3 中最重要的變化就是它們對 Unicode 的處理。
跟 Python 2 相似,Python 3 也有兩種類型,一個是 Unicode,一個是 byte 碼。可是它們有不一樣的命名。
如今你從普通文本轉換成 「str」 類型後存儲的是一個 unicode, 「bytes」 類型存儲的是 byte 串。你也能夠經過一個 b 前綴來製造 byte 串。
因此在 Python 2 中的 「str」 如今叫作 「bytes」,而 Python 2 中的 「unicode」 如今叫作 「str」。這比起Python 2中更容易理解,由於 Unicode 是你總想要存儲的內容。而 bytes 字符串只有你在想要處理 byte 的時候獲得。
Python 3 中對 Unicode 支持的最大變化就是沒有對 byte 字符串的自動解碼。若是你想要用一個 byte 字符串和一個 unicode 相鏈接的話,你會獲得一個錯誤,無論包含的內容是什麼。
全部這些在 Python 2 中都有隱式的處理,而在 Python 3 中你將會獲得一個錯誤。
另外若是一個 Unicode 字符串和 byte 字符串中包含的是相同的 ASCII 碼,Python 2 中將認爲兩個是相等的,而在 Python 3 中不會。這樣作的結果是 Unicode 中的鍵不能找到 byte 字符串中的值,反之亦然,然而在 Python 2 中是可行的。
這樣完全了改變了 Python 3 中的 Unicode 痛楚之源。在 Python 2 中,只要你使用 ASCII 數據,那麼混合 Unicode 和 byte 將會成功,而在 Python 3 會直接忽略數據而失敗。
這樣的話,在 Python 2 中所遇到的,你認爲你的程序是正確的可是最後發現因爲一些特殊字符而失敗的錯誤就會避免。
Python 3 中,你的程序立刻就會產生錯誤,因此即便你處理的是 ASCII 碼,那你也必須處理 bytes 和 Unicode 之間的關係。
Python 3 中對於 bytes 和 unicode 的處理很是嚴格,你被迫去處理這些事情。這曾經引發爭議。
這樣處理的緣由之一是對讀取文件的變化,Python 對於讀取文件有兩種方式,一種是二進制,一種是文本。在 Python 2 中,它只會影響到行尾符號,甚至在 Unix 系統上的時候,基本沒有區別。
在 Python 3中。這兩種模式將會返回不一樣的結果。當你用文本模式打開一個文件時無論你是用的 「r」 模式或者默認的模式,讀取成功的文件將會自動轉碼成 unicode ,你會獲得 str 對象。
若是你用二進制模式打開一個文件,在參數中輸入 「rb」 ,那麼從文件中讀取的數據會是 bytes,對它們沒有任何處理。
隱式的對 bytes 到 unicode 的處理使用的是 locale.getpreferedencoding() ,然而它有可能輸出你不想要的結果。好比,當你讀取 hi_utf8.txt 時,他被解碼成語言偏好中所設置的編碼方式,若是咱們這些例子在 Windows 中建立的話,那麼就是 「cp1252」 。像 ISO 8859-1, CP-1252 這些能夠獲得任意的 byte 值,因此不會拋出 UnicodeDecodeError ,固然也意味着它們會直接將數據解碼成 CP-1252,製造出咱們並不須要的垃圾信息。
爲了文件讀取正確的話,你應該指明想要的編碼。open 函數如今已經能夠經過參數來指明編碼。
減輕痛苦
好,那麼如何來減小這些痛苦?好消息是減輕痛苦的規則很是簡單,在Python 2和 Python 3中都比較適用。
正如咱們在事實一中所看到的,在你的程序中進進出出的只有 bytes, 可是在你的程序中你沒必要處理全部的 bytes。最好的策略是將輸入的 bytes 立刻解碼成 unicode。你在程序中均使用 unicode ,當在進行輸出的時候,儘早將之編碼成 bytes 。
製造一個 Unicode 三明治, bytes 在外, Unicode 在內。
要記着,有時候一些庫將會幫助你完成相似的事情。一些庫可能讓你輸入 unicode,輸出 unicode,它會幫你完成轉換的功能。好比 Django 在它的 json 模塊中提供 Unicode。
第二條規則是:你須要知道你如今處理的是哪一種類型的數據,在你的程序中任何一個位置,你須要知道你處理的是 byte 串仍是一個 unicode 串。它不能是一種猜想,而應該被設計好。
另外,若是你有一個 byte 串的話,若是你想對它進行處理。那麼你應該知道它是怎樣的編碼。
在對你的代碼進行 debug 的時候,不能僅僅將之打印出來來看它的類型。你應該查看它的 type ,或者查看它 repr 以後的值來查看你的數據究竟是什麼類型。
我曾經說過,你應該瞭解你的 byte 字符串的編碼類型。好這裏要我講事實四:你不能經過檢查它來判斷這個字符串編碼的類型。你應該經過其餘途徑來了解。好比不少協議中將會指明編碼類型。這裏咱們給出 HTTP, HTML, XML, Python 源文件中的例子。你也能夠經過預先的指定來了解編碼。好比數據源碼中可能會指明編碼。
有一些方式能夠來猜想一些 bytes 的編碼類型。可是僅僅是猜想。可以肯定的惟一方式是經過其餘方式。
這裏是給出一些怪異的字符的編碼猜想。咱們用UTF-8 便民店的一些字符,被不一樣的解碼方式解碼以後的輸出。你能夠看見。有時候用不正確的解碼方式解碼可能會輸出正確,可是會輸出錯誤的字符。你的程序不能告訴你這些解析錯誤了。只有當用戶察覺到的時候你纔會發現錯誤。
這是事實四的一個好例子:一樣的 bytes 流經過不一樣的解碼器是能夠解碼的。而 bytes 自己不能指明它本身用的哪一種編碼方式。
順便說一下,這些垃圾信息的顯示只遵循一個規則,那就是亂碼。
不幸的是,bytes 流會根據本身的來源不一樣而進行不一樣的編碼,有時候咱們指明的編碼方式多是錯誤的。好比你有可能將一個 HTML 從網上抓取下來,HTTP 頭中指明編碼方式是 8859-1, 然而實際上的編碼確是 UTF-8。
在一些狀況下編碼方式的不匹配可能會產生亂碼,而有些時候,則會產生 UnicodeError。
不用說。你應該測試你的 Unicode 支持。爲了這樣。你首先應該在你的代碼中首先去先把 Unicode 來提取出。若是你只會說英語,這可能會有些困難。由於有些 Unicode 數據會比較難以讀。幸運的是,大部分時候一些複雜結構的 Unicode 字符串仍是比較具備可讀性的。
這裏是一個例子。ASCII 文本中能夠讀的文本,和倒置的文本。這些文本的一些有時候是一些青年人會粘貼到社交網絡中。
根據你的程序,你有可能在 Unicode 的道路中越挖越深。還有不少不少的細節我這裏沒有解釋清楚。能夠被涉及到。咱們稱之爲事實五。由於你沒必要去對此瞭解太詳細。
複習一下,咱們有五個不可忽視的事實:
程序中全部的輸入和輸出均爲 byte
世界上的文本須要比 256 更多的符號來表現
你的程序必須處理 byte 和 unicode
byte 流中不會包含編碼信息
指明的編碼有多是錯誤的
這是你在編程中保持 Unicode 清潔的三個建議:
Unicode 三明治:儘量的讓你程序處理的文本都爲 Unicode 。
瞭解你的字符串。你應該知道你的程序中,哪些是 unicode, 哪些是 byte, 對於這些 byte 串,你應該知道,他們的編碼是什麼。
測試 Unicode 支持。使用一些奇怪的符號來測試你是否已經作到了以上幾點。
若是你遵循以上建議的話,你將會寫出對 Unicode 支持很好的代碼。無論 Unicode 中有多麼不規整的編碼你的程序也不會掛掉。
一些其餘你可能須要的資源Joel Spolsky 編寫的 The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!) 歸納了 Unicode 的工做方式和緣由。雖然沒有 Python 的內容,可是比我解釋的詳細多了!若是你須要處理一些語義上的 Unicode 字符問題。那麼 unicodedata module 也許會對你有些幫助。若是你但願找一些 Unicode 來測試的話,網上各類的 編碼文本計算器 會對你頗有幫助。