Python 中文處理系列之源代碼與文件IO
無論是做網路爬蟲、讀寫文件、製作圖形用戶界面,都免不了和中文打交道。由於歷史以及所處平台本身的問題(微軟!說的就是你),Python的中文處理比較複雜。這個系列文章加以總結。=======================================================================
1. 中文字元編碼概要
為了存儲、處理和表達中文字元,計算機系統需要維護一個中文字符集合到數字集合的一一對應關係。編解碼方案(簡稱編碼方案)就是這樣一種對應關係。
從漢字到數字序列的過程叫編碼,從數字序列到漢字的過程叫解碼。
在實際的計算機系統中,首先劃定一定數量的字元(中文、英文、標點乃至其他語言的文字)組成字符集(Character Set);每個字符集的字元賦予唯一的碼位(Code Point),然後再對碼位使用某種編碼方法進行編碼(encoding)從而得編碼後的序列。解碼的過程與之相反。
目前,與簡體中文有關的字符集有:
- GB-18030,GB-2312:中國國家標準。前者是後者的擴展
- GBK:中國全國信息技術標準化技術委員會發布的文件,是GB-2312的擴展
- Unicode:國際標準。規定於 ISO/IEC 10646
- GB-13000:中國國家標準,為Unicode的子集
與之對應的編碼方案:
- GB-18030,GBK,GB-2312:事實上採用EUC-CN。
- Unicode,GB-13000:採用多種編碼方案,常用的有UTF-8 ,UTF-16等。這其中,UTF-8由於對ASCII良好的兼容性,廣泛用於程序代碼、網頁設計等中英混合的場合。
由於 GB-18030,GBK,GB-2312 只使用一種編碼方案,所以,GB-18030,GBK,GB-2312不僅指代字符集的名字,也指代它唯一的編碼方案的名字。
無論是GBK還是Unicode,這些都是字元標準。為了能夠方便的讓程序識別採用Unicode字符集的編碼方案編碼的文件,Unicode字符集設定了一個BOM(Byte order mark)字元U+FEFF。帶有BOM的Unicode編碼文件總是以BOM開頭。如果文件用UTF-8編碼,那麼BOM就是對應的編碼字元序列0xEF 0xBB 0xBF ,如果文件用UTF-16LE編碼,那麼BOM的序列就是0xFF 0xFE,以此類推。但是,Unicode的標準並不建議使用BOM。
國際字元的編碼,是每一個現代操作系統都要解決的問題。當然會影響到應用程序和各種編程語言。具體對於「編程」這件事來說,會牽扯到「代碼編輯器」這個軟體對國際字元編碼的處理。 每一種代碼編輯器,對讀寫文件的編碼設置都不相同;尤其需要注意同一款軟體在不同操作系統、不同操作系統的設置下表現也不盡相同。提議在編寫涉及國際字元編碼的程序之前,先了解你所使用的代碼編輯器對代碼文本本身的編碼方式。
2. Windows 中的錯誤
由於歷史和其他一些不清楚的原因,Windows在字符集、編碼問題上犯了一些錯誤。主要體現在兩方面:
- 聲明採用了某個標準,但不嚴格遵循這個標準
- 對概念進行混淆
Windows NT(Windows 2000,XP,Vista,7,8,10以及所有Windows Server)的內核使用Unicode,而在用戶界面上,使用了所謂「代碼頁」管理多種編碼方案。不同語言版本的Windows,其默認設置的代碼頁是不同的。
具體來說,所有涉及字元編碼的API函數均有兩個版本。「A」(ANSI)版本使用當前代碼頁所設定的字元編碼方式,中文Windows的默認設置下,這個編碼方式即為GBK;「W」版本稱為「寬字元」,以對Unicode進行支持。為什麼會用ANSI這個詞是歷史原因,但這個歷史遺留問題造成了概念混淆。
Windows 字元編碼的雙軌制影響到了所有在Windows上運行的軟體。微軟建議新編寫軟體均採用寬字元的方案。
目前,Windows 將代碼頁CP936分配給了GBK,而不再對GB-2312提供支持。Windows將代碼頁CP54936分配給了GB-18030。中文Windows的默認代碼頁設置是CP936。
Windows的GBK不符合中國全國信息技術標準化技術委員會發布的文件,存在標準GBK未設定的字元,也缺失了標準中規定的若干字元。
Windows的記事本程序對保存文件的編碼混淆了概念: 當你使用Windows記事本時,保存選項有個「編碼」,並有4種選擇:ANSI、Unicode、Unicode big endian和UTF-8。其中,ANSI指「使用當前代碼頁」,中文Windows下即為GBK。Unicode指UTF-16LE,Unicode big endian指UTF-16BE。記事本會自作主張地在文件頭部增加BOM序列。
3.Python 中文處理:源代碼
下面的敘述中,不再使用字符集的名字而是使用具體的編碼方案。考慮到廣泛性,本文只考慮GB-2312(Windows下用GBK取代,Mac OS nX下用GB18030取代)和UTF-8。此外,Python 2中還有一個類的名字叫unicode,不要與字符集Unicode混淆。
假設你已經掌握了你所使用的代碼編輯器的源文件保存方式。
在Python中,源代碼中的字元串字面量(string literal)以單引號或者雙引號包裹。而Python還支持帶有前綴的字元串字面量。Python 2與Python 3對前綴字元串字面量的解釋不同:
- Python 2:b測試 的類型為str,測試 的類型為 str,u測試 的類型為 unicode
- Python 3:b測試 的類型為bytes,測試 的類型為 str,u測試 的類型為 str
Python 2有類型 unicode 處理國際字元編碼,Python 3的str類型內建了對unicode的支持,並提供bytes類型處理原始碼流。
但無論是unicode類型還是str類型,它們只是負責處理編解碼。而字元串字面量的「原始編碼」卻是由代碼編輯器提供的。具體來說,你在某個代碼編輯器里輸入了一些漢字,此時這些漢字的數據位於內存之中,由代碼編輯器與操作系統共同維護;你一旦將代碼保存在硬碟之中,代碼編輯器就需要根據自己內置的設置或者用戶選定的設置,使用某種編碼方案對中文進行編碼。因此,在Python源代碼中引號間的「數據」,是由代碼編輯器提供的。而在Python開始運行的時候,字元串字面量將會被Python讀取並解碼,隨後放入Python的內存中做進一步的解析和處理。
我們知道國際字元的編碼方式不唯一,例如在廣泛使用的Windows系統當中,GBK和UTF-8依然處於共存的狀態。 在解碼源文件時,Python使用兩種方法來判斷字元串字面量的編碼方式:
- BOM
- PEP 263 -- Defining Python Source Code Encodings
其中BOM是代碼編輯器保存代碼時附著於源文件的頭部的,大部分代碼編輯器都無法直接編輯BOM。而PEP 263是在Python源文件的頭部增加特殊的注釋,對於程序員是可見的。
下面寫一個小程序,對Python中文處理進行實測。代碼如下:
# -*- coding: utf-8 -*-ncn_str = u你好nnif type(cn_str) == type(u):n barray = cn_str.encode(utf-8)nelse:n barray = cn_strnprint(cn_str)nprint(len(cn_str))ntry:n print([hex(ord(c)) for c in barray])nexcept:n print([hex(c) for c in barray])n
第一行是PEP 263的編碼定義約定,如果有這一行,就告訴Python「本源代碼中所有中文字面量是採用UTF-8編碼的,Python要用UTF-8對源文件解碼」。第二行是待測的字元串。接下來的若干行是輸出待測字元串的所有「原始數據」。
待測字元串為「你好」,它的GBK編碼是 C4 E3 BA C3,每兩個位元組代表一個漢字;它的UTF-8編碼是E4 BD A0 E5 A5 BD,每三個位元組代表一個漢字。
下面我們對這個源代碼加以修改,並進行排列組合。控制變數有三組:
- 源代碼編碼:ANSI(即GBK),UTF-8 無BOM,UTF-8 有BOM
- PEP 263編碼定義約定:有和無
- 字元串字面量:使用 和 使用 u
這樣的話會形成一個三維表。
Python 2.7.10 (Windows)的運行結果如下表。一些測試會產生編譯期錯誤。沒有產生錯誤的測試,表格中列出了在Windows命令提示符下實際輸出的結果。
- 在沒有使用PEP 263,以及沒有BOM的情況下,Python按照7bit ASCII序列理解字元串字面量。這時,如果字元串字面量中出現了大於0x80的值,會直接產生編譯期錯誤。
- 在使用了PEP 263或者存在BOM的情況下,Python容許字元串字面量出現大於0x80的值。Python會將 包裹的字元串字面量理解為單個位元組組成的序列。此時,對這個字元串求長度會得到位元組的數量,而若列印到控制台的過程中,存在一個從Python內部編碼到當前代碼頁的轉換。所以「你好」的UTF-8編碼會按照GBK解碼,得到「浣犲ソ」這三個字元。
- 在使用了PEP 263或者存在BOM的情況下,對於u而言,只有以UTF-8編碼的漢字字元得到了正確的解碼。此時得到的是unicode類型。print一個unicode類型時,Python內部會以當前代碼頁解碼,使得命令提示符下能夠看到正確的字元。
下面是 Python 3.4.3 (Windows)的運行結果:
- 在沒有PEP 263的情況下,Python 3總默認以UTF-8解碼源代碼。
- Python 3的 和 u 是一碼事,都是帶有unicode支持的str類型。
綜上,提供幾點建議:
- 源文件在保存時使用UTF-8編碼。理由:跨版本,跨平台。
- 不要假設源文件有BOM。理由:看不見。
- 使用PEP 263聲明源文件的編碼方式為UTF-8。理由:跨版本。
- 凡國際字元串必加前綴u。理由:跨版本。
- 如果想得到其他編碼方式的字元串序列,使用unicode.encode方法(Python 2)或 str.encode方法(Python 3)
以上示例均在中文Windows默認設置(代碼頁CP936)之下。對於Mac OS X,大部分軟體(文本編輯器、終端)的默認設置為UTF-8。上述幾點建議依然有效,在使用默認設置的情況下這些建議是自動被採納的。
如果需要升級老代碼,無法將每一個替換成u的話,可以在源文件的所有代碼之前加上
from __future__ import unicode_literalsn
此時會被認為是u。然而,PEP 0236規定自己必須出現在第一行或者第二行,shebang必須在第一行,所以,Python文件的頭部必須這樣寫:
#/usr/bin/pythonn# -*- coding: utf-8 -*-nfrom __future__ import unicode_literalsn
4.Python 中文處理:寫文件
本節討論使用Python內建文件IO進行文件的寫操作。
寫文件涉及兩個問題:文件名本身的編碼和文件內容的編碼。
測試的小程序如下:
# -*- coding: utf-8 -*-nfilename = u你好nwith open(filename,w) as f:n f.write(filename)n
對這個源代碼加以修改,並進行排列組合。控制變數有三組:
- 源代碼編碼:ANSI(即GBK),UTF-8 無BOM,UTF-8 有BOM
- PEP 263編碼定義約定:有和無
- 字元串字面量:使用 和 使用 u
如果這個代碼能夠正常運行,會創建一個文件,並把文件名當作文件內容寫入。
Python 2.7.10 (Windows)的運行結果如下表。一些測試會產生與上節相同的編譯期錯誤,以N/A標記。沒有產生錯誤的測試,表格中列出資源管理器顯示的文件名和文件的長度。文件長度若是4,則說明編碼是GBK,如果是6,則說明編碼是UTF-8。
結論如下:- open函數能夠接受str和unicode類型。對於str類型的文件名,最終會依據系統默認代碼頁(如GBK)產生文件名。對於unicode類型的文件名,最終會解釋為編碼時採用的文字。
- file.write函數不能夠接受unicode類型。
對於第二個結論有補救辦法:
- 將unicode類型編碼,此時可以任意選擇需要的編碼:
# -*- coding: utf-8 -*-nfilename = u你好nwith open(filename,"w") as f:n f.write(filename.encode(gbk))n
- 不使用Python默認的file object,而使用codecs讀寫文件:
# -*- coding: utf-8 -*-nnimport codecsnfilename = u你好nwith codecs.open(filename,encoding=utf-8, mode=w+) as f:n f.write(filename)n
下面是 Python 3.4.3 (Windows)的運行結果:
可見,在Python 3中,寫入文件的編碼是默認的GBK。如果想產生其他的編碼,在Python內部仍然可以使用codecs。而Python 3的open函數也帶有了encoding的參數,然而,這樣的代碼不具備跨語言特性。
結論:
- 打開文件所使用的文件名最好使用unicode類型的字元串字面量。即u。
- 以跨語言、跨平台的考慮,保存帶有國際字元的文件應該使用codecs。
5.Python 中文處理:讀文件
讀文件的操作與Python代碼本身的編碼方式無關。首先我們準備一個二進位的文件,有6個位元組E4 BD A0 E5 A5 BD。文件名為datafile。
這6個位元組,如果用UTF-8解碼,就是「你好」,如果用GBK解碼,那就是「浣犲ソ」 ,所以:在讀文件之前,必須要知道文件的編碼
在讀文件之前,必須要知道文件的編碼在讀文件之前,必須要知道文件的編碼
例如,剛才提到的PEP 236就能夠讓Python「知道」源文件的編碼方式;BOM也是一種提示方法,對於XML、HTML而言,文件頭部有meta data供瀏覽器知曉整個文件的編碼方式。
如果實在不知道,就得用第三方的工具猜測文件的編碼。比如有Windows API IsTextUnicode function (Windows) (這個函數不見得好用,例如記事本ANSI保存「聯通」二字會無法讀出)。
我們分情況討論Python 2和Python 3的處理方法。本節討論與操作系統無關。
Python 2.7.10
若以以下方法讀入文件datafile,會得到6個字元的str類型變數。
with open(datafile) as f:n cn_str = f.read()n
如果想得到中文加以處理,可以用str.decode方法解碼為unicode類型。
cn_str.decode(utf-8) #你好ncn_str.decode(gbk) #浣犲ソn
當然也可以用codecs讀取datafile,解碼方式仍然不可少:
import codecsnnwith codecs.open(datafile, encoding=utf-8) as f:n cn_str = f.read()n
此時得到的cn_str是unicode類型。
Python 3.4.3
首先,codecs仍然可以使用,表現方式與Python 2相同,但得到的str類型。剛才已經說過,Python 3的str類型支持unicode。
其次,Python 3升級了內建的open函數:
with open(datafile, encoding=utf-8) as f:n cn_str = f.read()n
不過,這樣的代碼顯然是不能跨語言的。
本文總結
- Python源代碼用UTF-8編碼,並使用PEP 236的注釋聲明源代碼的編碼為UTF-8
- 中文字元串用u前綴。
- 讀取含有中文的文件,使用codecs可以讓你的代碼同時適用Python 2和Python 3。
推薦閱讀:
TAG:蛇之魅惑 | UTF-8 | UTF-16 | Byteordermark | PEP263--DefiningPythonSourceCodeEncodings | IsTextUnicodefunctionWindows |