Unicode 指南?
- 發(fā)布版本
1.12
本指南討論了 Python 對于表達文本數(shù)據(jù)的 Unicode 規(guī)范的支持,并且解釋了人們試圖使用 Unicode 時經(jīng)常遇到的問題。
Unicode 概述?
定義?
如今的程序需要具有處理許多不同類型字符的能力。應用程序常常需要國際化以便以用戶可選擇的不同語言顯示信息和輸出。同一個程序可能需要以英語、法語、日語、希伯來語或俄語輸出錯誤信息。網(wǎng)頁內(nèi)容可能由任何語言寫成,并且可能包含不同的表情符號。Python 的字符串類型使用 Unicode 標準來表示字符,這使 Python 程序能夠正常處理所有這些可能的字符。
Unicode 規(guī)范 (https://www.unicode.org/) 旨在列出人類語言中用到的每個字符,并賦予每個字符唯一的編碼。該規(guī)范持續(xù)進行修訂和更新以添加新的語言和符號。
一個**字符**是文本的最小可能部件。‘A’、‘B’、‘C’ 等都是不同的字符。‘è’ 和 ‘í’ 也一樣。字符會隨著語言或者上下文的變化而變化。比如,‘Ⅰ’ 是一個表示 “羅馬數(shù)字 1” 的字符,它與大寫字母 ‘I’ 不同。它們常常看起來相同,但這是兩個有著不同含義的不同字符。
Unicode 標準描述了**碼位**如何表示字符。一個碼位的值是在 0 到 0x10FFFF (大約 110 萬個值,目前有其中 11 萬個被指派)。在這一標準中并且在這一文檔中,一個碼位寫作 U+265E 來表示擁有值 0x265e 的字符(十進制下為 9,822)。
Unicode 標準包含了許多表格來列出字符和對應的碼位。
0061 'a'; LATIN SMALL LETTER A
0062 'b'; LATIN SMALL LETTER B
0063 'c'; LATIN SMALL LETTER C
...
007B '{'; LEFT CURLY BRACKET
...
2167 'Ⅷ'; ROMAN NUMERAL EIGHT
2168 'Ⅸ'; ROMAN NUMERAL NINE
...
265E '?'; BLACK CHESS KNIGHT
265F '?'; BLACK CHESS PAWN
...
1F600 '??'; GRINNING FACE
1F609 '??'; WINKING FACE
...
嚴格地說,這些定義暗示了這樣的說法是沒有意義的:“這是字符 U+265E”。U+265E 是一個碼位,其代表了某特定的字符 —— 在這一情形下,它代表了字符 “國際象棋黑方騎士(黑馬)” ‘?’。在非正式上下文中,碼位和字符的差異有時會被忽略。
一個字符在屏幕上或在紙上被表示為一組圖形元素,被稱為**字形**。比如,大寫字母 A 的字形,是斜向的兩筆和水平的一筆,而具體的細節(jié)取決于所使用的字體。大部分 Python 代碼不必擔心字形,找到應被顯示的正確字形一般來說是用戶圖形界面工具箱或者終端的字體渲染器的工作。
編碼?
上一段可以歸結(jié)為:一個 Unicode 字符串是一系列碼位(從 0 到 0x10FFFF 或者說十進制的 1,114,111 的數(shù)字)組成的序列。這一序列在內(nèi)存中需要被表示為一組**碼元**,然后**碼元** 會對應到包含八個二進制位的字節(jié)。將 Unicode 字符串翻譯成字節(jié)序列的規(guī)則被稱為**字符編碼**,或者**編碼**。
你可能會想到的第一種編碼是使用一個 32 位的整數(shù)來代表一個代碼位,然后使用 CPU 對 32 位整數(shù)的表達方式。在這一表達方式中,字符串 “Python” 可能看起來像是這樣:
P y t h o n
0x50 00 00 00 79 00 00 00 74 00 00 00 68 00 00 00 6f 00 00 00 6e 00 00 00
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
這個表達方式非常直接,但同時也存在 一些問題。
不夠方便;不同的處理器對字節(jié)的排序不同。
非常浪費空間。多數(shù)編碼都小于127,或者255,所以很多空間都是
0x00。上面的字符串takes 24 bytes compared to the 6 bytes needed for an ASCII representation. Increased RAM usage doesn't matter too much (desktop computers have gigabytes of RAM, and strings aren't usually that large), but expanding our usage of disk and network bandwidth by a factor of 4 is intolerable.與現(xiàn)有的 C 函數(shù)(如
strlen())不兼容,因此需要采用一套新的寬字符串函數(shù)。
因此這種編碼用得不多,人們轉(zhuǎn)而選擇其他更高效、更方便的編碼,比如 UTF-8。
UTF-8 是最常用的編碼之一,Python 往往默認會采用它。UTF 代表“Unicode Transformation Format”,'8' 表示編碼采用 8 位數(shù)。(UTF-16 和 UTF-32 編碼也是存在的,但其使用頻率不如 UTF-8。)UTF-8 的規(guī)則如下:
如果碼位 < 128,則直接用對應的字節(jié)值表示。
如果碼位 >= 128,則轉(zhuǎn)換為 2、3、4 個字節(jié)的序列,每個字節(jié)值都位于 128 和 255 之間。
UTF-8 有幾個很方便的特性:
可以處理任何 Unicode 碼位。
Unicode 字符串被轉(zhuǎn)換為一個字節(jié)序列,僅在表示空(null )字符(U+0000)時才會包含零值的字節(jié)。這意味著
strcpy()之類的C 函數(shù)可以處理 UTF-8 字符串,而且用那些不能處理字符串結(jié)束符之外的零值字節(jié)的協(xié)議也能發(fā)送。ASCII 字符串也是也是也是合法的 UTF-8 文本。
UTF-8 相當緊湊;大多數(shù)常用字符均可用一兩個字節(jié)表示。
如果字節(jié)數(shù)據(jù)被損壞或丟失,則可以找出下一個 UTF-8 碼點的開始位置并重新開始同步。隨機的 8 位數(shù)據(jù)也不太可能像是有效的 UTF-8 編碼。
UTF-8 是一種面向字節(jié)的編碼。編碼規(guī)定了每個字符由一個或多個字節(jié)的序列表示。這避免了整數(shù)和雙字節(jié)編碼(如 UTF-16 和 UTF-32)可能出現(xiàn)的字節(jié)順序問題,那時的字節(jié)序列會因執(zhí)行編碼的硬件而異。
參考文獻?
Unicode Consortium 站點 有 Unicode 規(guī)范的字符圖表、詞匯表和 PDF 版本。 為一些困難的閱讀做好準備。 Unicode 起源和發(fā)展的年表 也可在該站點上找到。
On the Computerphile Youtube channel, Tom Scott briefly discusses the history of Unicode and UTF-8 <https://www.youtube.com/watch?v=MijmeoH9LT4> (9 minutes 36 seconds).
為了幫助理解該標準,Jukka Korpela 編寫了閱讀 Unicode 字符表的`介紹性指南 <http://jkorpela.fi/unicode/guide.html>`_ 。
Joel Spolsky 撰寫了另一篇不錯的介紹性文章 <https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character- set-no-excuses/>`_ 。如果本文沒讓您弄清楚,那應在繼續(xù)之前先試著讀讀這篇文章。
Python對Unicode的支持?
現(xiàn)在您已經(jīng)了解了 Unicode 的基礎(chǔ)知識,可以看下 Python 的 Unicode 特性。
字符串類型?
從 Python 3.0 開始, str 類型包含了 Unicode 字符,這意味著用``"unicode rocks!"、'unicode rocks!'`` 或三重引號字符串語法創(chuàng)建的任何字符串都會存儲為 Unicode。
Python 源代碼的默認編碼是 UTF-8,因此可以直接在字符串中包含 Unicode 字符:
try:
with open('/tmp/input.txt', 'r') as f:
...
except OSError:
# 'File not found' error message.
print("Fichier non trouvé")
旁注:Python 3 還支持在標識符中使用 Unicode 字符:
répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
f.write("test\n")
如果無法在編輯器中輸入某個字符,或出于某種原因想只保留 ASCII 編碼的源代碼,則還可以在字符串中使用轉(zhuǎn)義序列。(根據(jù)系統(tǒng)的不同,可能會看到真的大寫 Delta 字體而不是 u 轉(zhuǎn)義符。):
>>> "\N{GREEK CAPITAL LETTER DELTA}" # Using the character name
'\u0394'
>>> "\u0394" # Using a 16-bit hex value
'\u0394'
>>> "\U00000394" # Using a 32-bit hex value
'\u0394'
此外,可以用 bytes 的 decode() 方法創(chuàng)建一個字符串。 該方法可以接受 encoding 參數(shù),比如可以為 UTF-8 ,以及可選的 errors 參數(shù)。
若無法根據(jù)編碼規(guī)則對輸入字符串進行編碼,errors 參數(shù)指定了響應策略。 該參數(shù)的合法值可以是 'strict' (觸發(fā) UnicodeDecodeError 異常)、'replace' (用 U+FFFD、REPLACEMENT CHARACTER)、'ignore' (只是將字符從 Unicode 結(jié)果中去掉),或 'backslashreplace' (插入一個 \xNN 轉(zhuǎn)義序列)。 以下示例演示了這些不同的參數(shù):
>>> b'\x80abc'.decode("utf-8", "strict")
Traceback (most recent call last):
...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x80 in position 0:
invalid start byte
>>> b'\x80abc'.decode("utf-8", "replace")
'\ufffdabc'
>>> b'\x80abc'.decode("utf-8", "backslashreplace")
'\\x80abc'
>>> b'\x80abc'.decode("utf-8", "ignore")
'abc'
編碼格式以包含編碼格式名稱的字符串來指明。 Python 有大約 100 種不同的編碼格式;清單詳見 Python 庫參考文檔 標準編碼。 一些編碼格式有多個名稱,比如 'latin-1'、'iso_8859_1' 和 '8859 都是指同一種編碼。
利用內(nèi)置函數(shù) chr() 還可以創(chuàng)建單字符的 Unicode 字符串,該函數(shù)可接受整數(shù)參數(shù),并返回包含對應碼位的長度為 1 的 Unicode 字符串。內(nèi)置函數(shù) ord() 是其逆操作,參數(shù)為單個字符的 Unicode 字符串,并返回碼位值:
>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344
轉(zhuǎn)換為字節(jié)?
bytes.decode() 的逆方法是 str.encode() ,它會返回 Unicode 字符串的 bytes 形式,已按要求的 encoding 進行了編碼。
參數(shù) errors 的意義與 decode() 方法相同,但支持更多可能的handler。除了 'strict' 、 'ignore' 和 'replace' (這時會插入問號替換掉無法編碼的字符),還有 'xmlcharrefreplace' (插入一個 XML 字符引用)、 backslashreplace (插入一個 \uNNNN 轉(zhuǎn)義序列)和 namereplace (插入一個 \N{...} 轉(zhuǎn)義序列 )。
以下例子演示了各種不同的結(jié)果:
>>> u = chr(40960) + 'abcd' + chr(1972)
>>> u.encode('utf-8')
b'\xea\x80\x80abcd\xde\xb4'
>>> u.encode('ascii')
Traceback (most recent call last):
...
UnicodeEncodeError: 'ascii' codec can't encode character '\ua000' in
position 0: ordinal not in range(128)
>>> u.encode('ascii', 'ignore')
b'abcd'
>>> u.encode('ascii', 'replace')
b'?abcd?'
>>> u.encode('ascii', 'xmlcharrefreplace')
b'ꀀabcd޴'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'
用于注冊和訪問可用編碼格式的底層函數(shù),位于 codecs 模塊中。 若要實現(xiàn)新的編碼格式,則還需要了解 codecs 模塊。 不過該模塊返回的編碼和解碼函數(shù)通常更為底層一些,不大好用,編寫新的編碼格式是一項專業(yè)的任務,因此本文不會涉及該模塊。
Python 源代碼中的 Unicode 文字?
在 Python 源代碼中,可以用 \u 轉(zhuǎn)義序列書寫特定的 Unicode 碼位,該序列后跟 4 個代表碼位的十六進制數(shù)字。\U 轉(zhuǎn)義序列用法類似,但要用8 個十六進制數(shù)字,而不是 4 個:
>>> s = "a\xac\u1234\u20ac\U00008000"
... # ^^^^ two-digit hex escape
... # ^^^^^^ four-digit Unicode escape
... # ^^^^^^^^^^ eight-digit Unicode escape
>>> [ord(c) for c in s]
[97, 172, 4660, 8364, 32768]
對大于 127 的碼位使用轉(zhuǎn)義序列,數(shù)量不多時沒什么問題,但如果要用到很多重音字符,這會變得很煩人,類似于程序中的信息是用法語或其他使用重音的語言寫的。也可以用內(nèi)置函數(shù) chr() 拼裝字符串,但會更加乏味。
理想情況下,都希望能用母語的編碼書寫文本。還能用喜好的編輯器編輯 Python 源代碼,編輯器要能自然地顯示重音符,并在運行時使用正確的字符。
默認情況下,Python 支持以 UTF-8 格式編寫源代碼,但如果聲明要用的編碼,則幾乎可以使用任何編碼。只要在源文件的第一行或第二行包含一個特殊注釋即可:
#!/usr/bin/env python
# -*- coding: latin-1 -*-
u = 'abcdé'
print(ord(u[-1]))
上述語法的靈感來自于 Emacs 用于指定文件局部變量的符號。Emacs 支持許多不同的變量,但 Python 僅支持“編碼”。 -*- 符號向 Emacs 標明該注釋是特殊的;這對 Python 沒有什么意義,只是一種約定。Python 在注釋中查找 coding: name 或 coding=name 。
如果沒有這種注釋,則默認編碼將會是前面提到的 UTF-8。更多信息請參閱 PEP 263 。
Unicode屬性?
Unicode 規(guī)范包含了一個碼位信息數(shù)據(jù)庫。對于定義的每一個碼位,都包含了字符的名稱、類別、數(shù)值(對于表示數(shù)字概念的字符,如羅馬數(shù)字、分數(shù)如三分之一和五分之四等)。還有有關(guān)顯示的屬性,比如如何在雙向文本中使用碼位。
以下程序顯示了幾個字符的信息,并打印一個字符的數(shù)值:
import unicodedata
u = chr(233) + chr(0x0bf2) + chr(3972) + chr(6000) + chr(13231)
for i, c in enumerate(u):
print(i, '%04x' % ord(c), unicodedata.category(c), end=" ")
print(unicodedata.name(c))
# Get numeric value of second character
print(unicodedata.numeric(u[1]))
當運行時,這將打印出:
0 00e9 Ll LATIN SMALL LETTER E WITH ACUTE
1 0bf2 No TAMIL NUMBER ONE THOUSAND
2 0f84 Mn TIBETAN MARK HALANTA
3 1770 Lo TAGBANWA LETTER SA
4 33af So SQUARE RAD OVER S SQUARED
1000.0
類別代碼是描述字符性質(zhì)的縮寫。 這些被分組為“字母”、“數(shù)字”、“標點符號”或“符號”等類別,而這些類別又分為子類別。 從上面的輸出中獲取代碼,'Ll' 表示“字母,小寫”,'No' 表示“數(shù)字,其他”,'Mn' 是“標記,非間距”,'So' 是“符號,其他”。 有關(guān)類別代碼列表,請參閱 Unicode 字符數(shù)據(jù)庫文檔 <http://www.unicode.org/reports/tr44/#General_Category_Values>`_ 的“通用類別值”部分。
字符串比較?
Unicode 讓字符串的比較變得復雜了一些,因為同一組字符可能由不同的碼位序列組成。例如,像“ê”這樣的字母可以表示為單碼位 U+00EA,或是 U+0065 U+0302,即“e”的碼位后跟“COMBINING CIRCUMFLEX ACCENT”的碼位。雖然在打印時會產(chǎn)生同樣的輸出,但一個是長度為 1 的字符串,另一個是長度為 2 的字符串。
一種不區(qū)分大小寫比較的工具是字符串方法 casefold() ,將按照 Unicode 標準描述的算法將字符串轉(zhuǎn)換為不區(qū)分大小寫的形式。該算法對諸如德語字母“?”(代碼點 U+00DF)之類的字符進行了特殊處理,變?yōu)橐粚π懽帜浮皊s”。
>>> street = 'Gürzenichstra?e'
>>> street.casefold()
'gürzenichstrasse'
第二個工具是 unicodedata 模塊的 normalize() 函數(shù),將字符串轉(zhuǎn)換為幾種常規(guī)形式之一,其中后跟組合字符的字母將被替換為單個字符。 normalize() 可用于執(zhí)行字符串比較,即便兩個字符串采用不同的字符組合,也不會錯誤地報告兩者不相等:
import unicodedata
def compare_strs(s1, s2):
def NFD(s):
return unicodedata.normalize('NFD', s)
return NFD(s1) == NFD(s2)
single_char = 'ê'
multiple_chars = '\N{LATIN SMALL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print('length of first string=', len(single_char))
print('length of second string=', len(multiple_chars))
print(compare_strs(single_char, multiple_chars))
當運行時,這將輸出:
$ python3 compare-strs.py
length of first string= 1
length of second string= 2
True
normalize() 函數(shù)的第一個參數(shù)是個字符串,給出所需的規(guī)范化形式,可以是“NFC”、“NFKC”、“NFD”和“NFKD”之一。
Unicode 標準還設(shè)定了如何進行不區(qū)分大小寫的比較:
import unicodedata
def compare_caseless(s1, s2):
def NFD(s):
return unicodedata.normalize('NFD', s)
return NFD(NFD(s1).casefold()) == NFD(NFD(s2).casefold())
# Example usage
single_char = 'ê'
multiple_chars = '\N{LATIN CAPITAL LETTER E}\N{COMBINING CIRCUMFLEX ACCENT}'
print(compare_caseless(single_char, multiple_chars))
這將打印 True 。(為什么 NFD() 會被調(diào)用兩次?因為有幾個字符讓 casefold() 返回非規(guī)范化的字符串,所以結(jié)果需要再次進行規(guī)范化。參見 Unicode 標準的 3.13 節(jié) 的一個討論和示例。)
Unicode 正則表達式?
re 模塊支持的正則表達式可以用字節(jié)串或字符串的形式提供。有一些特殊字符序列,比如 \d 和 \w 具有不同的含義,具體取決于匹配模式是以字節(jié)串還是字符串形式提供的。例如,\d 將匹配字節(jié)串中的字符 [0-9] ,但對于字符串將會匹配 'Nd' 類別中的任何字符。
上述示例中的字符串包含了泰語和阿拉伯數(shù)字書寫的數(shù)字 57:
import re
p = re.compile(r'\d+')
s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))
執(zhí)行時,\d+ 將匹配上泰語數(shù)字并打印出來。如果向 compile() 提供的是 re.ASCII 標志,\d+ 則會匹配子串 "57"。
類似地,\w 將匹配多種 Unicode 字符,但對于字節(jié)串則只會匹配 [a-zA-Z0-9_] ,如果指定 re.ASCII, \s `` 將匹配 Unicode 空白符或 ``[ \t\n\r\f\v]。
參考文獻?
關(guān)于 Python 的 Unicode 支持,其他還有一些很好的討論:
用 Python 3 處理文本文件 ,作者 Nick Coghlan。
實用的 Unicode,Ned Batchelder 在 PyCon 2012 上的演示。
str 類型在 Python 庫參考文檔 文本序列類型 --- str 中有介紹。
unicodedata 模塊的文檔
codecs 模塊的文檔
Marc-André Lemburg 在 EuroPython 2002 上做了一個題為“Python 和 Unicode”(PDF 幻燈片)<https://downloads.egenix.com/python/Unicode-EPC2002-Talk.pdf>`_ 的演示文稿。該幻燈片很好地概括了 Python 2 的 Unicode 功能設(shè)計(其中 Unicode 字符串類型稱為 unicode,文字以 u 開頭)。
Unicode 數(shù)據(jù)的讀寫?
既然處理 Unicode 數(shù)據(jù)的代碼寫好了,下一個問題就是輸入/輸出了。如何將 Unicode 字符串讀入程序,如何將 Unicode 轉(zhuǎn)換為適于存儲或傳輸?shù)男问侥兀?/p>
根據(jù)輸入源和輸出目標的不同,或許什么都不用干;請檢查一下應用程序用到的庫是否原生支持 Unicode。例如,XML 解析器往往會返回 Unicode 數(shù)據(jù)。許多關(guān)系數(shù)據(jù)庫的字段也支持 Unicode 值,并且 SQL 查詢也能返回 Unicode 值。
在寫入磁盤或通過套接字發(fā)送之前,Unicode 數(shù)據(jù)通常要轉(zhuǎn)換為特定的編碼。可以自己完成所有工作:打開一個文件,從中讀取一個 8 位字節(jié)對象,然后用 bytes.decode(encoding) 對字節(jié)串進行轉(zhuǎn)換。但是,不推薦采用這種全人工的方案。
編碼的多字節(jié)特性就是一個難題; 一個 Unicode 字符可以用幾個字節(jié)表示。 如果要以任意大小的塊(例如 1024 或 4096 字節(jié))讀取文件,那么在塊的末尾可能只讀到某個 Unicode 字符的部分字節(jié),這就需要編寫錯誤處理代碼。 有一種解決方案是將整個文件讀入內(nèi)存,然后進行解碼,但這樣就沒法處理很大的文件了;若要讀取 2 GB 的文件,就需要 2 GB 的 RAM。(其實需要的內(nèi)存會更多些,因為至少有一段時間需要在內(nèi)存中同時存放已編碼字符串及其 Unicode 版本。)
解決方案是利用底層解碼接口去捕獲編碼序列不完整的情況。這部分代碼已經(jīng)是現(xiàn)成的:內(nèi)置函數(shù) open() 可以返回一個文件類的對象,該對象認為文件的內(nèi)容采用指定的編碼,read() 和 write() 等方法接受 Unicode 參數(shù)。只要用 open() 的 encoding 和 errors 參數(shù)即可,參數(shù)釋義同 str.encode() 和 bytes.decode() 。
因此從文件讀取 Unicode 就比較簡單了:
with open('unicode.txt', encoding='utf-8') as f:
for line in f:
print(repr(line))
也可以在更新模式下打開文件,以便同時讀取和寫入:
with open('test', encoding='utf-8', mode='w+') as f:
f.write('\u4500 blah blah blah\n')
f.seek(0)
print(repr(f.readline()[:1]))
Unicode 字符 U+FEFF 用作字節(jié)順序標記(BOM),通常作為文件的第一個字符寫入,以幫助自動檢測文件的字節(jié)順序。某些編碼(例如 UTF-16)期望在文件開頭出現(xiàn) BOM;當采用這種編碼時,BOM 將自動作為第一個字符寫入,并在讀取文件時會靜默刪除。這些編碼有多種變體,例如用于 little-endian 和 big-endian 編碼的 “utf-16-le” 和 “utf-16-be”,會指定一種特定的字節(jié)順序并且不會忽略 BOM。
在某些地區(qū),習慣在 UTF-8 編碼文件的開頭用上“BOM”;此名稱具有誤導性,因為 UTF-8 與字節(jié)順序無關(guān)。此標記只是聲明該文件以 UTF-8 編碼。要讀取此類文件,請使用“utf-8-sig”編碼器自動忽略此標記。
Unicode 文件名?
當今大多數(shù)操作系統(tǒng)的文件名都支持包含任意 Unicode 字符。通常這是通過將 Unicode 字符串轉(zhuǎn)換為某系統(tǒng)的編碼來實現(xiàn)的。如今的 Python 趨向于采用 UTF-8 編碼:MacOS 上的 Python 已經(jīng)在多個版本中采用了 UTF-8,而 Python 3.6 在 Windows 上也改用了 UTF-8。在 Unix 系統(tǒng)中,只有設(shè)置了 LANG 或 LC_CTYPE 環(huán)境變量,才會采用文件系統(tǒng)編碼;否則默認編碼還是 UTF-8。
sys.getfilesystemencoding() 函數(shù)將返回要在當前系統(tǒng)采用的編碼,若想手動進行編碼時即可用到,但無需多慮。在打開文件進行讀寫時,通常只需提供 Unicode 字符串作為文件名,會自動轉(zhuǎn)換為合適的編碼格式:
filename = 'filename\u4500abc'
with open(filename, 'w') as f:
f.write('blah\n')
os 模塊中的函數(shù)也能接受 Unicode 文件名,如 os.stat() 。
os.listdir() 函數(shù)將返回文件名,這會引發(fā)一個難題:到底應返回文件名的 Unicode 版本,還是應返回已編碼的字節(jié)串呢?這兩種操作 os.listdir() 均可執(zhí)行,具體取決于給出的目錄路徑是字節(jié)串形式還是 Unicode 字符串。若傳入一個 Unicode 字符串作為路徑,文件名將采用文件系統(tǒng)的編碼進行解碼并返回一個 Unicode 字符串列表,而若傳入一個字節(jié)串路徑則會返回字節(jié)串格式的文件名。以下假定默認文件系統(tǒng)編碼為 UTF-8,運行以下程序:
fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()
import os
print(os.listdir(b'.'))
print(os.listdir('.'))
將產(chǎn)生以下輸出:
$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]
第一個列表包含 UTF-8 編碼的文件名,第二個列表則包含 Unicode 版本的。
請注意,大多時候應該堅持用這些 API 處理 Unicode。字節(jié)串 API 應該僅用于可能存在不可解碼文件名的系統(tǒng);現(xiàn)在幾乎僅剩 Unix 系統(tǒng)了。
識別 Unicode 的編程技巧?
本節(jié)提供了一些關(guān)于編寫 Unicode 處理軟件的建議。
最重要的技巧如下:
程序應只在內(nèi)部處理 Unicode 字符串,盡快對輸入數(shù)據(jù)進行解碼,并只在最后對輸出進行編碼。
如果嘗試編寫的處理函數(shù)對 Unicode 和字節(jié)串形式的字符串都能接受,就會發(fā)現(xiàn)組合使用兩種不同類型的字符串時,容易產(chǎn)生差錯。沒辦法做到自動編碼或解碼:如果執(zhí)行 str + bytes,則會觸發(fā) TypeError。
當要使用的數(shù)據(jù)來自 Web 瀏覽器或其他不受信來源時,常用技術(shù)是在用該字符串生成命令行之前,或要存入數(shù)據(jù)庫之前,先檢查字符串中是否包含非法字符。請仔細檢查解碼后的字符串,而不是編碼格式的字節(jié)串數(shù)據(jù);有些編碼可能具備一些有趣的特性,例如與 ASCII 不是一一對應或不完全兼容。如果輸入數(shù)據(jù)還指定了編碼格式,則尤其如此,因為攻擊者可以選擇一種巧妙的方式將惡意文本隱藏在經(jīng)過編碼的字節(jié)流中。
在文件編碼格式之間進行轉(zhuǎn)換?
StreamRecoder 類可以在兩種編碼之間透明地進行轉(zhuǎn)換,參數(shù)為編碼格式為 #1 的數(shù)據(jù)流,表現(xiàn)行為則是編碼格式為 #2 的數(shù)據(jù)流。
假設(shè)輸入文件 f 采用 Latin-1 編碼格式,即可用 StreamRecoder 包裝后返回 UTF-8 編碼的字節(jié)串:
new_f = codecs.StreamRecoder(f,
# en/decoder: used by read() to encode its results and
# by write() to decode its input.
codecs.getencoder('utf-8'), codecs.getdecoder('utf-8'),
# reader/writer: used to read and write to the stream.
codecs.getreader('latin-1'), codecs.getwriter('latin-1') )
編碼格式未知的文件?
若需對文件進行修改,但不知道文件的編碼,那該怎么辦呢?如果已知編碼格式與 ASCII 兼容,并且只想查看或修改 ASCII 部分,則可利用 surrogateescape 錯誤處理程序打開文件:
with open(fname, 'r', encoding="ascii", errors="surrogateescape") as f:
data = f.read()
# make changes to the string 'data'
with open(fname + '.new', 'w',
encoding="ascii", errors="surrogateescape") as f:
f.write(data)
surrogateescape 錯誤處理 handler 會把所有非 ASCII 字節(jié)解碼為 U+DC80 至 U+DCFF 這一特殊范圍的碼位。當 surrogateescape 錯誤處理 handler用于數(shù)據(jù)編碼并回寫時,這些碼位將轉(zhuǎn)換回去。
參考文獻?
David Beazley 在 PyCon 2010 上的演講 掌握 Python 3 輸入/輸出 中,有一節(jié)討論了文本和二進制數(shù)據(jù)的處理。
Marc-André Lemburg 演示的PDF 幻燈片“在 Python 中編寫支持 Unicode 的應用程序” ,討論了字符編碼問題以及如何國際化和本地化應用程序。這些幻燈片僅涵蓋 Python 2.x。
Python Unicode 實質(zhì) 是 Benjamin Peterson 在 PyCon 2013 上的演講,討論了 Unicode 在 Python 3.3 中的內(nèi)部表示。
致謝?
本文初稿由 Andrew Kuchling 撰寫。此后,Alexander Belopolsky、Georg Brandl、Andrew Kuchling 和 Ezio Melotti 作了進一步修訂。
感謝以下各位指出本文錯誤或提出建議:éric Araujo、Nicholas Bastin、Nick Coghlan、Marius Gedminas、Kent Johnson、Ken Krugler、Marc-André Lemburg、Martin von L?wis、Terry J. Reedy、Serhiy Storchaka , Eryk Sun, Chad Whitacre, Graham Wideman。
