代碼頁即地獄

jopen 10年前發布 | 19K 次閱讀 代碼

        最近,我在把一個 Python 2 的視頻下載工具 youku-lixian 改寫成 Python 3,并添加了自己需要的 油Tube 支持。

        在 Linux 下,事情進行得很順利:所有的東西都用 UTF-8進 行編碼。Python 3 里的 str 類型從 2.x 版本的 ASCII 字符串變成了 Unicode 字符串;我移除了原來代碼里關于本地編碼類型的判斷處理部分。程序從抓取的頁面上解析出視頻標題部分的 Unicode 字符串,直接 print ()顯示到標準輸出,一切看起來很和諧。

        假定我抓取的這個視頻標題是中文,叫做“你好,世界”。眾所周知,得益于 Python 良好的 Unicode 支持,輸出它只需要簡單的一句:

print ('你好,世界')

        在天殺的 Windows 7下測試這個程序時,麻煩就出現了。如果你想知道我為什么這么說,請繼續看下去。

        我所不了解的 Windows

        去年從學校拿到這臺 Dell 筆記本時,Windows 7 自然是預裝在上面的。

        系統語言已經設置成了英語。很快,我對瑞典語鍵盤的布局感到極其不適應:它的標點符號位置與英語鍵盤布局有很大區別,分號、冒號、單引號雙引 號、斜杠反斜杠這些程序員司空見慣的符號,和美式英語鍵盤完全不同。于是,我把鍵盤布局換回了習慣的英語鍵盤,順便把控制面板的“區域”選項也一概從瑞典 換到了英語/美國。

        在很長一段時間里,除了界面是英文以外,它看起來和以往用的中文系統沒什么區別:有默認的中文字體,輸入法可以添加中文的。我平時用它做的,只有:上網,掛迅雷,拿 IE 登網銀,玩 Mirror's Edge,幾件事而已。

        文件系統是 Unicode 編碼的,Web 瀏覽器是支持 Unicode 的,偶爾用的文本編輯器也是一律設置成 UTF-8 的。而且我們知道,從 Windows 2000 起,Windows 的內碼實現是使用 UTF-16LE 的。幾乎讓人快要忘了還有代碼頁這么一回事。

        可是,如果要在英文 Windows 系統的命令提示符里執行這個簡單的輸出 Unicode 文本的程序:

#!/usr/bin/env python
# -*- coding: utf-8 -*- if __name__ == '__main__':
    print ('你好,世界')

        Python 就會跳出來一段錯誤:

File "c:\Python32\lib\encodings\cp437.py", line 19, in encode
    return codecs.charmap_encode (input,self.errors,encoding_map)[0]
UnicodeEncodeError: 'charmap' 
codec can't encode characters in position 0-1: character maps to 

        難道 Python 3 不是支持 Unicode 的嗎?難道它不是跨平臺的嗎?

        第一個問題,基本上是對的,Python 3 確實支持 Unicode,這種支持體現在它把所有的 str 字符串都作為 Unicode 處理這件事情上。

        第二個問題,不完全,跨平臺的可移植性是有條件的。Python 本身是支持 Unicode,但是如果遇上了非 Unicode 的古董環境,那就一點辦法也沒有。

        什么叫“非 Unicode 的古董環境”呢……不,我說的不是 DOS。這個東西,竟然就是 Windows 上的cmd.exe,每個人或多或少都用過的命令行環境。

        cmd.exe,從 MinGW 到 Python,基本上每個 Windows 下需要接觸命令行的開發人員都躲不過去的東西,微軟怎么就不能把它做好些?窗口大小不能隨心所欲改也就算了,不能全屏顯示也就算了,字體大小屏幕緩沖設置 各種限制也就算了,鼠標拖拽不方便也就算了,命令行補全補不全也就算了,你好歹能把默認編碼改成用 Unicode 吧?一個破窗口從二十年前的 3.x 時代沿用到今天的 Windows 7,從依賴 DOS 的command.com到獨立的cmd.exe,尼瑪這么多年了,也沒見功能上有什么實質的改進,是不是在微軟眼里所有的程序員都在拿個白花花的 IDE“做你的 code”、不需要命令行了?

        (在 Windows 已經完全使用 UTF-16 作為內碼實現的今天,cmd.exe仍然在使用系統默認的代碼頁,我所能想到的唯一理由就是為了保持和以前的 non-Unicode 程序兼容——不過這理由也太弱了吧)

        微軟有功夫把 Windows 8 的界面做得花里胡哨,不過看樣子他們是壓根不打算把cmd.exe這個東西做得更好用些。不繼續噴下去了,說處理問題的經過:

        前面 Python 的錯誤信息里提到了個文件cp437.py。既然是 cp437 什么的,那就一定是 Python 在試圖把 Unicode 字符串轉換成用于輸出的 437 代碼頁(英語/美國)時出了錯。

        為什么 Python 要把一個好端端的 Unicode 字符串轉換成 cp437 呢?這很容易想通,因為程序是在cmd.exe這個終端環境下執行的。在我的英文系統上,它的活動代碼頁是 437(英語/美國)。從代碼中的 Unicode 字符串到輸出 cp437 的這一步轉換,是由 Python 解釋器來實現的,所以會由 Python 拋出一個錯誤,而不是直接在控制臺輸出一堆亂碼。

        首先想到的解決方案,自然是改變當前cmd.exe的活動代碼頁到 UTF-8 Unicode:

chcp.com 65001

        不幸的是,這導致 Python 解釋器直接崩潰了:

Fatal Python error: Py_Initialize: can't initialize sys standard streams LookupError: unknown encoding: cp65001

This application has requested the Runtime to terminate it in an unusual way. Please contact the application's support team for more information.
LookupError: unknown encoding: cp65001</pre>

        搜了一下才發現,Python 3.2 目前并不支持 Windows 上面的 cp65001。話說 65001 代碼頁不就是 UTF-8 嘛(囧囧囧囧囧)

        與其說是不支持,不如說是 bug 更合適些。因為執行之后 Windows 就跳出一個警告框說“python.exe已經停止響應”了……

        于是,試著改變代碼頁到 GBK:

chcp.com 936

        結果卻是:

Invalid code page

        Windows 聲稱這是一個無效的代碼頁。為什么?

        編碼是什么

        好了,暫且忘記cmd.exe諸如此類令人不愉快的東西,在 IDLE 上試一試。

        我不知道有多少 Linux 程序員寫 Python 的時候會用到 IDLE。對于這些習慣了終端+文本編輯器的用戶來說,IDLE 看起來是個無關緊要的附屬品,也許它的定位只是用來幫助初學者入門的一個開發環境?

        不過,容易被人們忽略的一點是:IDLE 本身是個跨平臺的環境,這意味著它可以無條件支持 Unicode(只要系統上有相應的字體),用它來解釋執行程序不必受制于特定終端環境的拘束。這一點在 Windows 上很重要,因為cmd.exe這玩意實在是太差勁了,所以估計很多人在 Windows 下交互執行 Python 的時候還是會選擇 IDLE 的。

        進入 IDLE。我們可能要關心一下這個 Windows 系統下面的默認編碼方式是什么,Python 3 里面有兩個函數:

>>> sys.stdout.encoding; locale.getpreferredencoding ()'cp1252' 'cp1252'

        第一個sys.stdout.encoding是指標準輸出的編碼,第二個locale.getpreferredencoding則是系統本地化設置的編碼。兩者是有區別的。現在我們看到,它們在當前環境下是相同的,都是默認的cp1252,也就是傳說中的“ANSI”代碼頁。

        恩,我們已經知道 IDLE 是一個完全跨平臺的環境,所以在 IDLE 上輸出 Unicode 字符可以得到和 Linux 環境下同樣和諧的結果:

>>> print ('你好,世界')你好,世界

        順便看看“你好,世界”的 UTF-8 編碼和 GBK 是什么,如果強制用其他編碼方式來解碼又會得到怎樣的結果(后面也許會用到)。可以看到,5 個全角字符在 UTF-8 編碼下是 15 個字節,每個字符占 3 bytes;在 GBK 編碼下是 10 個字節,每個字符占 2 bytes。

        雖然沒有什么實際的意義,不過還是可以注意到:UTF-8 編碼的字符是無法用 GBK 解碼的,哪怕是亂碼有時候也不行,因為可能會出現奇數字節長度,這在 GBK 下不合法;反之 GBK 編碼字符亦無法用 UTF-8 解碼,因為有無效字符值的存在。

代碼頁即地獄

        借助 IDLE 看到了“你好,世界”各種編碼的詳細情況。現在我們可以回到cmd.exe里面看一看下面這段程序的運行結果了:

#!/usr/bin/env python # -*- coding: utf-8 -*- import sys, localeif __name__ == '__main__':
    print(sys.stdout.encoding, locale.getpreferredencoding ())

    try:
        print('你好,世界')
    except Exception as err:
        print(str (err))

        首先通過chcp確認,cmd.exe的當前活動代碼頁是 437(英語/美國),而非 IDLE 里的 1252(ANSI)。這是由于我的 Windows 里對 non-Unicode 程序的區域設置是“英語/美國”的緣故。

        程序運行的結果是:

cp437 cp1252'charmap' codec can't encode characters in position 0-4:  character maps to 

        可以看到sys.stdout.encoding實際上就是當前環境下活動代碼頁的值。locale.getpreferredencoding ()沒變,仍然是系統默認的 cp1252。

        之后拋出的異常是在我們預料之中的,正如此前一樣,Python 嘗試把 Unicode 字符串轉換成 cmd 終端下的 cp437 代碼頁編碼。而中文字符本來就是沒有對應的 cp437 編碼的,所以 Python 報錯。

        Google 一下'charmap' codec can't encode characters in position 0-4: character maps to 這個錯誤。在 Stack Overflow 上,有人提到了解決的方法:設置一個叫做PYTHONIOENCODING的環境變量。

        PYTHONIOENCODING 環境變量

        所謂的PYTHONIOENCODING,既可以作為環境變量存在,也可以作為 Python 的命令行參數傳遞。它用于指定 Python 程序標準輸入輸出(stdin/stdout/stderr)的編碼。(注意這個編碼不是指源代碼的編碼,和 Python 程序開頭常見的# -*- coding: utf-8 -*-是兩碼事)

        在沒有這個環境變量時

        如前面所述,Python 會試圖把內部 Unicode 編碼的字符串轉化成當前執行程序的終端環境下所使用的編碼方式(sys.stdout.encoding)后輸出。對于當前代碼頁 437 的cmd.exe來說,把只含有英文數字的字符串轉成 cp437 編碼沒有任何問題;但是一旦遇上了中文字符,英語/美國的 437 代碼頁里必然是找不到對應的編碼的,于是 Python 就會報錯。

        如果當前代碼頁設成 65001,Python 3.2 會崩潰,這是本身實現上的問題。在最新的 Python 3.3 beta 中已經增加了對 cp65001 的支持

        在設置了這個環境變量時

        通過

set PYTHONIOENCODING=utf-8

        或(PowerShell 下)

$env:PYTHONIOENCODING = "utf-8"

        PYTHONIOENCODING指定的編碼方式會覆蓋原來的sys.stdout.encoding。如果將PYTHONIOENCODING設置為 utf-8,那么 Python 在輸出 Unicode 字符串的時候就會以 UTF-8 輸出,相當于什么也不轉換。

        再次執行該 Python 程序,這一次 Python 不再嘗試自動轉換 Unicode 的中文字符到 cp437 中的對應字符,程序成功運行,sys.stdout.encoding變成了 utf-8,字符串輸出則是亂碼:

utf-8 cp1252Σ╜áσ?╜∩╝?Σ╕?τò?

        這與我們之前在 IDLE 中將 UTF-8 編碼的文本強制用 cp437 解碼得到的結果是完全相同的:

>>> print(bytes ('你好,世界', 'utf-8') .decode ('cp437'))
Σ╜áσ?╜∩╝?Σ╕?τò?

        Python 直接把 UTF-8 編碼的字符串輸出到了 cp437 代碼頁的終端,相當于強制用 cp437 來解碼 UTF-8 文本,產生了無意義的亂碼。

代碼頁即地獄

        用文本編輯器寫一個內容是“你好,世界”的文件,以 UTF-8 編碼保存。在cmd.exe下通過 type 顯示,結果和上面是相同的。

代碼頁即地獄

        cmd.exe 和 PowerShell ISE 的微妙之處對比

        在當前區域設置(英語/美國)下,兩者執行chcp.com顯示的當前活動代碼頁都是 437。

        只有 cmd 下 Python 的sys.stdout.encoding默認是 cp437(與活動代碼頁相同);PowerShell ISE 下sys.stdout.encoding則是 cp1252(ANSI)。

        locale.getpreferredencoding永遠是系統本身默認的 cp1252,這是一個系統全局值。

        cmd 無法輸入中文,不能正確顯示文件系統中的中文文件名;PowerShell ISE 能夠輸入中文,能顯示中文文件名。

        在缺少 936 代碼頁的情況下,兩者都不能夠通過執行腳本或 type 文件內容正確顯示中文字符(無論是 GBK 還是 UTF-8),會產生亂碼。

代碼頁即地獄

        為什么 Windows 會缺少 GBK 代碼頁?

        回到最初的那個問題上來,為什么執行chcp.com 936不能切換到 GBK 代碼頁?為什么cmd.exe和 PowerShell 里不能正常顯示中文?

        這個問題讓我百思不得其解。花了幾個小時找到了原因,簡而言之:因為 Windows 的“區域和語言”設置不對。

        “Language for non-Unicode programs”這個選項不是簡體中文,所以就不能用 GBK,手動chcp.com也會告訴你該代碼頁無效。所以必須要在控制面板里設置成簡體中文,重啟后才能生效。

代碼頁即地獄

        好吧,問題來了,為什么這里只能單選?如果我既想使用 936(GBK)編碼的應用程序,又想使用 932(日語)編碼的應用程序,難道每次都要在這里改完后再重啟嗎?為什么他們不能給一個詳細的代碼頁列表讓用戶多選、需要時可以動態加載?

        Windows 設計的齷齪之處就在這里。如果你不去設置 system locale 為中文并重啟,所有 non-Unicode 程序里的中文字符集都是不會出現的,只能顯示成一個方框,比如cmd.exe里:

代碼頁即地獄

        還有 Vim 里(set fileencodings=utf-8,gbk),GBK 編碼的文本和 UTF-8 編碼的文本都一樣無法顯示。(按理說 Vim 應該不能算 non-Unicode 程序吧……誰知道呢?!)

代碼頁即地獄

        改過"Language for non-Unicode programs”為中文并且重啟系統之后,Vim 立即顯示正常:

代碼頁即地獄

        再進cmd.exe,默認活動代碼頁 936。這段 Python 程序終于也能正確輸出了:

代碼頁即地獄

        也許 Windows 這種蛋疼的設計是因為考慮到英文用戶一般不會需要多余的 Unicode 和代碼頁字符集,這么做可以節省系統啟動時間?誰知道呢,Windows 用戶不是最喜歡拿所謂的“啟動時間快”作為衡量系統性能的指標了嗎……

        切換到 cp65001(UTF-8 Unicode),PYTHONIOENCODING設置成 utf-8,按理來說這種方式不應該出問題,但是這輸出怎么看都不像是正常(如下圖所示)。不想深究到底為什么了,總之 Windows 下面東西的復雜程度以我這種智商是永遠都不能夠理解的……

代碼頁即地獄

        Python 除了標準輸入輸出,還有……

        文件名

open ('文件名測試', 'w')

        Python 中對文件系統的操作基本上是不受默認編碼影響的,只要sys.getfilesystemencoding ()的結果是 utf-8(現代 Linux)或者 mbcs(現代 Windows NT 系統上)。兩者本質上都是 Unicode 編碼。

        文件輸入輸出

        文件讀寫不屬于標準I/O,因此和環境變量PYTHONIOENCODING無關。

for c in ['utf-8', 'gbk']:
    with open ('test_%s.txt' % c, 'w', encoding=c) as output:
        try:
            output.write ('你好,世界\n')
        except Exception as err:
            print('\nWriting to file using %s:\n' % c, str (err))

        由于在 open ()中顯式指定了中文編碼方式(encoding='utf-8'encoding='gbk'),輸出“你好,世界”這樣的中文文本在任何平臺上都應該能夠得到正確的結果。

        然而對于:

with open ('test_default.txt', 'w') as output:
    try:
        output.write ('你好,世界\n')
    except Exception as err:
        print('\nWriting to file using default encoding:\n', str (err))

        由于沒有指定編碼方式,Python 會自動使用系統默認的編碼方式來進行輸出。如果系統默認編碼是 cp437 或 cp1252,由于中文字符在這些代碼頁中顯然不存在對應值,Python 會拋出一個熟悉的錯誤:

File "c:\Python32\lib\encodings\cp437.py", line 19, in encode
    return codecs.charmap_encode (input,self.errors,encoding_map)[0]
UnicodeEncodeError: 'charmap' 
codec can't encode characters in position 0-1: character maps to 

        當然,當系統默認編碼為 cp936(GBK)時,無論

output.write ('你好,世界')

        還是

print ('你好,世界')

        都可以正常工作。因為“你好,世界”這個 Unicode 字符串是可以被完全轉換成 GBK 中的對應編碼的。

        一些總結和思考

        雖然 Python 3 使用 Unicode 編碼的字符串,但是在跨平臺的程序中依然要取得系統的默認編碼用于后續處理,因為并不是所有的終端環境都支持全部的 Unicode 字符集:

if sys.stdout.isatty ()
    default_encoding = sys.stdout.encodingelse     
    default_encoding = locale.getpreferredencoding ()

         無論何時,不要隨心所欲地用 print ()向 stdout 輸出 Unicode 字符串。如果某個要輸出的 Unicode 字符(比如,中文字)在系統默認編碼的字符集(比如,代碼頁 437)上沒有,Python 這時就會拋出一個錯誤。這其實在大部分時候并不是我們想看到的局面,我們總希望即使有時會輸出一些無意義的亂碼,程序整體上也能正確運行。拿視頻下載工具 的例子來說,即使由于終端的關系有時無法正確顯示視頻名稱,這問題并不太嚴重,因為程序總是可以把抓取的視頻內容寫入正確的文件的。

        在程序中獲取了系統默認的default_encoding,我們就可以強制用它來對 Unicode 字符串進行編碼,至少避免了 Python 在自動轉碼過程中可能會拋出的錯誤——雖然結果可能只是得到一堆亂碼。另外一種處理方式是對于這樣的字符串,我們決定根本不去輸出它們。

        我們比較愿意看到的情況是:如果程序會輸出且只會輸出中文,而你假想中的 Windows 用戶群所使用的代碼頁是 936(GBK)——盡管在程序中使用 Unicode 字符串吧,這樣做不會帶來任何問題。

        但是,如果不能確定要處理的 Unicode 文本會限定在哪個代碼頁字符集的范圍當中:中文?梵文?希伯來文?阿拉伯文?還是……火星文?這個時候就必須考慮到世界上還有“編碼方式差異”這回事了。 當然,最好的解決方式也許是:告訴用戶,去他的代碼頁,去他的什么 437、500、936、1252……這堆詭異的數字,去他的 Bush hid the facts,扔掉設計上如此糟糕、編碼方式如此混亂和不一致的 Windows,轉投一個讓生活更簡單的操作系統吧。

來自: www.soimort.org

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!