代碼頁即地獄
最近,我在把一個 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: cp65001This 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 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!