關於你不想知道的所有Python3 unicode特性

賤聖OMG發表於2014-07-24

我的讀者知道我是一個喜歡痛罵Python3 unicode的人。這次也不例外。我將會告訴你用unicode有多痛苦和為什麼我不能閉嘴。我花了兩週時間研究Python3,我需要發洩我的失望。在這些責罵中,仍然有有用的資訊,因為它教我們如何來處理Python3。如果沒有被我煩到,就讀一讀吧。

這次吐槽的內容會不一樣。不會關聯到WSGI或者HTTP及與其相關的東西。通常,我被告知我應該停止抱怨Python3 Unicode系統,因為我不寫別人經常寫的程式碼(HTTP庫之類的東西),所以我這次準備寫點別的東西:一個命令列應用程式。我寫了一個很方便的庫叫click來讓編寫它更加簡單。

注意,我做的是每一個新手Python程式設計師做的事情:寫一個命令列應用程式。Hello World程式。但是不同以往,我想要確保應用程式是穩定的並且對於Python2和Python3的Unicode都是支援的,還能夠進行單元測試。所以接下來的就是如何來實現它。

我們想做什麼

在Python3我們作為開發者需要好好使用Unicode。顯然,我覺得這意味著所有的文字資料都是Unicode,所有非文字資料都是位元組。在這麼美妙的世界裡所有的東西只有黑與白,Hello World的例子非常直截了當。所以讓我們來寫一些shell工具吧。

這是用Python2形式實現的應用程式:

顯然,命令在處理任何命令列選項的時候也不是特別好,不過至少能夠用。所以我們開始碼程式碼吧。

UNIX裡的UNICODE

上面的程式碼在Python2是不行的,因為你暗中處理位元組。命令列引數是位元組,檔名是位元組,檔案內容也是位元組。語言衛道士會指出這是不對的,這樣會引發問題,但如果你開始更多考慮它,你會發現這是個不固定的問題。

UNIX是位元組,已經被定義成了這樣,並且一直會是這樣。為了理解為什麼你需要觀察資料傳輸的不同場景。

  • 終端
  • 命令列引數
  • 作業系統輸入輸出層
  • 檔案系統驅動

順便提一下,這不是資料可能通過的唯一東西,但是我們來了解一下,在多少場景下我們能瞭解一個編碼。答案是一個也沒有。至少我們需要理解一個編碼是終端輸出區域資訊。這個資訊可以用來展現轉換,也能夠理解文字資訊所擁有的編碼。

舉個例子,如果LC_CTYPE的值為en_US.utf-8告訴應用程式系統使用US English,並且大部分文字資料是utf-8編碼。實際上還有很多別的變數,不過我們假定這是我們唯一需要看的。注意LC_CTYPE並不代表所有的資料都是utf-8編碼的。它代替通知應用程式如何分類文字特性並且什麼時候需要應用轉換。

這很重要,原因是因為c locale。c locale是POSIX唯一指定的現場,它說所有ASCII編碼和來自命令列工具的回覆會按照POSIX spec裡定義的來對待。

在我們上面的cat工具裡,如果它是位元,沒有別的方法來對待這些資料。原因是shell裡沒有指定這資料是什麼。例如你呼叫cat hello.txt,終端會在對應用程式編碼的時候對hello.txt進行編碼。

但是現在想想這個例子echo *。Shell會把目前目錄的所有檔名傳遞給你的應用程式。那它們是什麼編碼?檔名沒有編碼!

UNICODE瘋狂

現在一個用Windows的人看到這裡會說:弄UNIX的人在搞什麼呢。但這還不算悲慘。產生這些工作的原因是一些聰明的人設計得這個系統能夠向後相容。不像Windows把每個API都定義兩次,在POSIX上,最好的處理方法是為了顯示的目的將其假定為位元組,用預設的編碼方式來編碼。

用上面的cat命令來舉例。比如有一個關於檔案無法開啟的錯誤資訊,原始是因為它們不存在或者它們是受保護的,或者其他任何的原因。我們假定檔案是用latin1編碼的,因為它是來自1995年外部驅動。終端會獲取標準輸出,它將會試著把它用utf-8編碼,因為這是它認為的編碼。因為字串是latin1編碼的,因為它無法順利得解碼。但是不怕,不會有什麼崩潰,因為你的終端在無法處理它的時候會無視它。

它在圖形介面上怎樣?每種有兩個版本。在一個像Nautilus 這樣的圖形介面上列出所有的檔案。它把檔名和圖示關聯起來,能夠雙擊並且試著使檔名能夠顯示出來,因而把它解碼。例如它會嘗試用utf-8解碼,錯誤的地方用問題記號來替代。你的檔名可能不是完全可讀的但那是你仍能開啟檔案。

UNIX上的unicode只在你強制所有東西用它的時候會很瘋狂。但那不是unicode在UNIX上工作的方式。UNIX沒有區別unicode和位元組的API。它們是相同的,使其更容易處理。

C Locale

C Locale在這裡出現的次數非常多。C Locale是避免POSIX的規格被強行應用到任何地方的一種手段。POSIX服從作業系統需要支援設定LC_CTYPE,來讓一切使用ASCII編碼。

這個locale是在不同的情況下挑選的。你主要發現這個locale為所有從cron啟動的程式,你的初始化程式和子程式提供一個空的環境。C Locale在環境裡復原了一個健全的ASCII地帶,否則你無法信任任何東西。

但是ASCII這個詞指出它是7bit編碼。這不是問題,因為作業系統是能處理位元組的!任何基於8bit的內容能正常處理,但你與作業系統遵循約定,那麼字元處理會限制在前7bit。任何你的工具生成的資訊它會用ASCII編碼並且使用英語。

注意POSIX規範沒有說你的應用程式應當死於火焰。

Python3死於火焰

Python3在unicode上選擇了與UNIX不同的立場。Python3說:任何東西是Unicode(預設情況下,除非是在某些情況下,除非我們傳送重複編碼的資料,可即使如此,有時候它仍然是Unicode,雖然是錯誤的Unicode)。檔名是Unicode,終端是Unicode,stdin和stdout是Unicode,有如此多的Unicode。因為UNIX不是Unicode,Python3現在的立場是它是對的UNIX是錯的,人們也應該修改POSIX的定義來新增Unicode。那麼這樣的話,檔名就是Unicode了,終端也是Unicode了,這樣也就不會看到一些由於位元組導致的錯誤了。

不是僅僅我這樣說。這些是Python關於Unicode的腦殘想法導致的bug:

如果你Google一下,你就能發現如此多的吐槽。看看有多少人安裝pip模組失敗,原因是changelog裡的一些字元,或者是因為home資料夾的原因又,或者是因為SSH session是用ASCII的,或者是因為他們是使用Putty連線的。

Python3 cat

現在開始為Python3修復cat。我們如何做?首先,我們需要處理位元組,因為有些東西可能會顯示一些不符合shell編碼的東西。所以無論如何,檔案內容需要是位元組。但我們也需要開啟基本輸出來讓它支援位元組,而它預設是不支援的。我們也需要分別處理一些情況比如Unicode API失敗,因為編碼是C。那麼這就是,Python3特性的cat。

這不是最差的版本。不是因為我想讓事情更加複雜,它現在就是有這麼複雜。例如在例子裡沒有做的是在讀取一個二進位制的東西是強制清理文字stdout。在這個例子裡沒有必要,是因為這裡的print呼叫去了stderr而不是stdout,但如果你想列印一些stdout,你就必須清理。為什麼?因為stdout是別的緩衝區之上的緩衝區,如果你不強制清理它,你的輸出順序可能會出錯。

不僅僅是我,例如看:twisted’s compat module ,會發現相同的麻煩。

跳起編碼舞蹈

為了理解shell裡的命令列引數,順便說一些Python3裡最糟糕的情況:

  1. shell把檔名以位元組傳給指令碼
  2. 位元組在命中你的程式碼前被Python以預期的解碼方式解碼。因為這是有損好的過程,Python3使用一個特別的錯誤處理器來處理解碼錯誤。
  3. Python程式碼處理一個沒有錯誤的檔案,並且需要格式化一個錯誤資訊。因為我們寫文字流的時候如果它不是非法的unicode,是不會寫替代的。
  4. 將包含替代的unicode串編碼為utf-8,然後告訴它處理替代轉義。
  5. 然後我們從utf-8解碼並告訴他忽略錯誤
  6. 結果字串回到只有文字的流裡
  7. 之後終端會解碼我們的字串來進行顯示

以下是Python2裡的情況:

  1. shell把檔名作為位元組傳給指令碼
  2. shell解碼字串來進行顯示

因為Python2版本里的字串處理只是在出錯的時候進行糾正,因為shell在顯示檔名時能做得更好。

注意這沒有讓指令碼更不對。如果你需要對輸入資料進行實際的字串處理,你就要在2.x和3.x裡面切換到unicode處理。但在那種情況,你也想讓你的指令碼支援一個—charset引數,那麼在2.x和3.x上做的工作差不多。只是在3.x上會更加糟糕,你需要構建在2.x上不需要的二進位制標準輸出。

但你是錯誤的

很顯然我錯了,我被人告訴這些:

  • 我感到痛苦是因為我不像初學者那樣思考,新的unicode系統會對初學者更友好
  • 我不考慮windows使用者和新的文字模型對windows使用者是多麼大的改進
  • 問題不在於Python,問題在POSIX規範
  • Linux發行版需要開始支援C.UTF-8,因為它們被過去一直阻礙著
  • 問題是SSH傳送了錯誤的編碼。SSH需要修復這個問題。
  • Python3裡一大堆unicode錯誤的真正問題是人們不傳遞明確的編碼而假設Python3作出了正確的決定。
  • 我與分解程式碼工作,顯然這在Python3裡會更難。
  • 我應該去改進Python3而不是在twitter和部落格上抱怨
  • 你在沒有問題的地方製造問題。讓每個人修復他們的環境和對任何東西進行編碼就很好。這是使用者的問題。
  • Java有這個問題好多年了,這對開發者來說沒問題。

你知道嗎?我在做HTTP方面的工作的時候就停止了抱怨,因為我接受了這個主意,就是HTTP/WSGI的一大堆問題對人們來說很平常。但你知道什麼?在Hello World這樣的情況下也有相同的問題。可能我應該放棄獲得一個高質量的unicode支援的庫,就這麼將就了。

我可以對以上觀點進行反駁,但最終也沒關係了。如果Python3是我唯一使用的Python語言,我會解決所有的問題並且使用它開發。有一個完美的另一個語言叫Python2,它有更大的使用者基礎,並且使用者基礎是很牢固的。這時我是非常沮喪的。

Python3可能足夠強大,會開始讓UNIX走Windows走過的路:在很多地方使用unicode,但我很懷疑這樣的做法。

更可能的事情是人們仍舊使用Python2,並且用Python3做一些很爛的東西。或者他們會用Go。這門語言使用了與Python2很相似的模型:任何東西都是位元組串。並且假設其編碼是UTF-8。到此結束。

相關文章