Unicode HOWTO¶
- 版本:
1.12
本 HOWTO 討論了 Python 對 Unicode 規範的支援,用於表示文字資料,並解釋了人們在嘗試使用 Unicode 時經常遇到的各種問題。
Unicode 簡介¶
定義¶
今天的程式需要能夠處理各種各樣的字元。應用程式通常會進行國際化,以各種使用者可選擇的語言顯示訊息和輸出;同一個程式可能需要用英語、法語、日語、希伯來語或俄語輸出錯誤訊息。Web 內容可以用上述任何一種語言編寫,並且還可以包含各種表情符號。Python 的字串型別使用 Unicode 標準來表示字元,這使得 Python 程式可以使用所有這些不同的可能字元。
Unicode (https://www.unicode.org/) 是一種旨在列出人類語言使用的每個字元併為每個字元提供其唯一的程式碼的規範。Unicode 規範不斷修訂和更新,以新增新的語言和符號。
一個 字元 是文字的最小組成部分。“A”、“B”、“C”等等都是不同的字元。“È”和“Í”也是。字元會因你所談論的語言或上下文而異。例如,有一個表示“羅馬數字一”的字元 ‘Ⅰ’,它與大寫字母 ‘I’ 是分開的。它們通常看起來相同,但這是兩個具有不同含義的不同字元。
Unicode 標準描述了字元如何由 程式碼點 表示。程式碼點值是一個介於 0 到 0x10FFFF(大約 110 萬個值,實際分配的數量 小於該值)之間的整數。在標準和本文件中,程式碼點使用 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 的字形是兩條對角線和一條水平線,儘管確切的細節取決於所使用的字型。大多數 Python 程式碼不需要擔心字形;確定要顯示的正確字形通常是 GUI 工具包或終端字型渲染器的工作。
編碼¶
總結上一節:Unicode 字串是程式碼點的序列,程式碼點是 0 到 0x10FFFF
(十進位制 1,114,111) 的數字。這個程式碼點序列需要在記憶體中表示為一組 程式碼單元,然後將 程式碼單元 對映到 8 位位元組。將 Unicode 字串轉換為位元組序列的規則稱為 字元編碼,或者簡稱 編碼。
您可能想到的第一個編碼是使用 32 位整數作為程式碼單元,然後使用 CPU 的 32 位整數表示形式。在這種表示形式中,字串 “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
這種表示形式很簡單,但是使用它會帶來許多問題。
它不具備可移植性;不同的處理器對位元組的排序方式不同。
它非常浪費空間。在大多數文字中,大多數程式碼點都小於 127 或小於 255,因此大量空間被
0x00
位元組佔用。上面的字串需要 24 個位元組,而 ASCII 表示只需要 6 個位元組。增加的 RAM 使用量並不是太重要(臺式計算機有千兆位元組的 RAM,並且字串通常不是那麼大),但是將我們對磁碟和網路頻寬的使用量擴大 4 倍是無法容忍的。它與現有的 C 函式(例如
strlen()
)不相容,因此需要使用新的寬字串函式系列。
因此,這種編碼的使用並不多,人們而是選擇其他更高效和方便的編碼,例如 UTF-8。
UTF-8 是最常用的編碼之一,Python 通常預設使用它。UTF 代表“Unicode 轉換格式”,而 ‘8’ 表示編碼中使用 8 位值。(還有 UTF-16 和 UTF-32 編碼,但它們的使用頻率低於 UTF-8。)UTF-8 使用以下規則
如果程式碼點 < 128,則用相應的位元組值表示。
如果程式碼點 >= 128,則將其轉換為兩、三或四個位元組的序列,其中序列的每個位元組都在 128 和 255 之間。
UTF-8 有幾個方便的屬性
它可以處理任何 Unicode 程式碼點。
Unicode 字串轉換為位元組序列,該序列僅在表示空字元 (U+0000) 的位置包含嵌入的零位元組。這意味著 UTF-8 字串可以由 C 函式(例如
strcpy()
)處理,並透過無法處理零位元組作為字串結尾標記的協議傳送。ASCII 文字字串也是有效的 UTF-8 文字。
UTF-8 相當緊湊;大多數常用的字元可以用一個或兩個位元組表示。
如果位元組損壞或丟失,則可以確定下一個 UTF-8 編碼的程式碼點的開頭並重新同步。隨機的 8 位資料也不太可能看起來像有效的 UTF-8。
UTF-8 是面向位元組的編碼。該編碼指定每個字元由一個或多個位元組的特定序列表示。這避免了整數和麵向字的編碼(如 UTF-16 和 UTF-32)中可能發生的位元組順序問題,在這些編碼中,位元組序列會因字串編碼所在的硬體而異。
參考資料¶
Unicode 聯盟網站 包含字元圖表、詞彙表和 Unicode 規範的 PDF 版本。請做好閱讀一些困難內容的準備。Unicode 的起源和發展時間表 也可在該網站上找到。
在 Computerphile Youtube 頻道上,Tom Scott 簡要討論了 Unicode 和 UTF-8 的歷史(9 分 36 秒)。
為了幫助理解該標準,Jukka Korpela 撰寫了 關於閱讀 Unicode 字元表的入門指南。
另一篇 不錯的入門文章 是由 Joel Spolsky 撰寫的。如果本介紹沒有讓您理解清楚,您應該在繼續之前嘗試閱讀這篇替代文章。
Python 的 Unicode 支援¶
現在您已經瞭解了 Unicode 的基本原理,我們可以看看 Python 的 Unicode 功能。
字串型別¶
自 Python 3.0 以來,該語言的 str
型別包含 Unicode 字元,這意味著使用 "unicode rocks!"
、'unicode rocks!'
或三引號字串語法建立的任何字串都儲存為 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,您還可以在字串字面量中使用轉義序列。(根據您的系統,您可能會看到實際的大寫 delta 字形而不是 u 轉義。)
>>> "\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'
此外,可以使用 decode()
方法的 bytes
建立字串。此方法接受一個 encoding 引數,例如 UTF-8
,以及可選的 errors 引數。
當輸入字串無法按照編碼規則轉換時,errors 引數指定響應。此引數的合法值是 'strict'
(引發 UnicodeDecodeError
異常)、'replace'
(使用 U+FFFD
,REPLACEMENT CHARACTER
)、'ignore'
(只是將字元從 Unicode 結果中刪除) 或 'backslashreplace'
(插入 \xNN
轉義序列)。以下示例顯示了差異
>>> 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'
都是同一編碼的同義詞。
也可以使用內建函式 chr()
建立單字元 Unicode 字串,該函式接受整數並返回一個長度為 1 的 Unicode 字串,其中包含相應的程式碼點。反向操作是內建函式 ord()
,它接受一個單字元 Unicode 字串並返回程式碼點值。
>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344
轉換為位元組¶
bytes.decode()
的相反方法是 str.encode()
,它返回 Unicode 字串的 bytes
表示形式,並使用請求的編碼進行編碼。
errors 引數與 decode()
方法的引數相同,但支援更多可能的處理程式。除了 'strict'
、 'ignore'
和 'replace'
(在這種情況下,它會插入一個問號來代替無法編碼的字元)之外,還有 'xmlcharrefreplace'
(插入 XML 字元引用)、backslashreplace
(插入 \uNNNN
轉義序列)和 namereplace
(插入 \N{...}
轉義序列)。
以下示例顯示了不同的結果
>>> 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'
用於註冊和訪問可用編碼的底層例程在 codecs
模組中。實現新的編碼也需要理解 codecs
模組。但是,此模組返回的編碼和解碼函式通常比舒適的程度更底層,並且編寫新的編碼是一項專門的任務,因此本 HOWTO 中不會介紹該模組。
Python 原始碼中的 Unicode 字面量¶
在 Python 原始碼中,可以使用 \u
轉義序列編寫特定的 Unicode 程式碼點,後跟四個十六進位制數字,給出程式碼點。\U
轉義序列類似,但需要八個十六進位制數字,而不是四個。
>>> 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 的情況,使用轉義序列在少量情況下是可以的,但是如果您使用許多重音字元,就會變得很煩人,就像在法語或其他一些使用重音的語言的訊息程式中一樣。您還可以使用內建函式 chr()
來組裝字串,但這更加繁瑣。
理想情況下,您應該能夠以您語言的自然編碼編寫字面量。然後,您可以使用您喜歡的編輯器編輯 Python 原始碼,該編輯器會自然地顯示重音字元,並在執行時使用正確的字元。
Python 預設支援使用 UTF-8 編寫原始碼,但如果您宣告使用的編碼,則可以使用幾乎任何編碼。這可以透過在原始檔的第一行或第二行包含一個特殊的註釋來完成
#!/usr/bin/env python
# -*- coding: latin-1 -*-
u = 'abcdé'
print(ord(u[-1]))
此語法的靈感來自 Emacs 用於指定檔案本地變數的表示法。Emacs 支援許多不同的變數,但 Python 僅支援“coding”。-*-
符號向 Emacs 指示該註釋是特殊的;它們對 Python 沒有意義,而是一種約定。Python 在註釋中查詢 coding: name
或 coding=name
。
如果您不包含此類註釋,則預設使用的編碼將為 UTF-8,如前所述。另請參閱 PEP 263 獲取更多資訊。
Unicode 屬性¶
Unicode 規範包括有關程式碼點資訊的資料庫。對於每個定義的程式碼點,資訊包括字元的名稱、其類別、適用的數字值(對於表示數字概念的字元,例如羅馬數字、分數,如三分之一和五分之四等)。還有與顯示相關的屬性,例如如何在雙向文字中使用程式碼點。
以下程式顯示有關幾個字元的一些資訊,並列印一個特定字元的數值
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
類別程式碼是描述字元性質的縮寫。這些被分組為“字母”、“數字”、“標點符號”或“符號”等類別,這些類別又被細分為子類別。以從上述輸出中獲取的程式碼為例,'Ll'
表示“字母,小寫”,'No'
表示“數字,其他”,'Mn'
表示“標記,非間距”,而 'So'
表示“符號,其他”。有關類別程式碼的列表,請參閱 Unicode 字元資料庫文件的“常規類別值”部分。
比較字串¶
Unicode 為比較字串增加了一些複雜性,因為同一組字元可以用不同的程式碼點序列表示。例如,像 “ê” 這樣的字母可以表示為單個程式碼點 U+00EA,或表示為 U+0065 U+0302,它是 “e” 的程式碼點,後跟一個 “COMBINING CIRCUMFLEX ACCENT” 的程式碼點。列印時,它們會產生相同的輸出,但一個是長度為 1 的字串,另一個是長度為 2 的字串。
用於不區分大小寫比較的一種工具是 casefold()
字串方法,該方法根據 Unicode 標準描述的演算法將字串轉換為不區分大小寫的形式。此演算法對諸如德語字母 “ß”(程式碼點 U+00DF)之類的字元進行特殊處理,該字元會變成一對小寫字母 “ss”。
>>> street = 'Gürzenichstraße'
>>> street.casefold()
'gürzenichstrasse'
第二個工具是 unicodedata
模組的 normalize()
函式,該函式將字串轉換為幾種規範化形式之一,其中後跟組合字元的字母將替換為單個字元。normalize()
可用於執行字串比較,如果兩個字串以不同的方式使用組合字元,則不會錯誤地報告不相等
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))
執行時,它會輸出
$ python compare-strs.py
length of first string= 1
length of second string= 2
True
normalize()
函式的第一個引數是一個字串,給出所需的規範化形式,可以是“NFC”、“NFKC”、“NFD”和“NFKD”之一。
Unicode 標準還規定了如何進行不區分大小寫的比較
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()
兩次?因為有一些字元會使 casefold()
返回一個未規範化的字串,因此需要再次規範化結果。有關討論和示例,請參見 Unicode 標準的 3.13 節。)
Unicode 正則表示式¶
re
模組支援的正則表示式可以作為位元組或字串提供。諸如 \d
和 \w
之類的一些特殊字元序列根據模式是以位元組還是字串形式提供而具有不同的含義。例如,\d
將匹配位元組中的字元 [0-9]
,但在字串中將匹配任何在 'Nd'
類別中的字元。
此示例中的字串使用泰語和阿拉伯數字寫入數字 57
import re
p = re.compile(r'\d+')
s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))
當執行時,\d+
將匹配泰語數字並將其打印出來。如果您向 compile()
提供 re.ASCII
標誌,則 \d+
將匹配子字串 “57” 。
類似地,\w
匹配各種 Unicode 字元,但在位元組中或如果提供 re.ASCII
時,僅匹配 [a-zA-Z0-9_]
,而 \s
將匹配 Unicode 空白字元或 [ \t\n\r\f\v]
。
參考資料¶
以下是一些關於 Python 的 Unicode 支援的優秀討論:
在 Python 3 中處理文字檔案,作者:Nick Coghlan。
實用的 Unicode,Ned Batchelder 在 2012 年 PyCon 上的演講。
str
型別在 Python 庫參考中的 文字序列型別 — str 中描述。
unicodedata
模組的文件。
codecs
模組的文件。
Marc-André Lemburg 在 EuroPython 2002 上做了題為 “Python 和 Unicode” 的演講(PDF 幻燈片)。這些幻燈片很好地概述了 Python 2 的 Unicode 功能的設計(其中 Unicode 字串型別稱為 unicode
,字面量以 u
開頭)。
讀取和寫入 Unicode 資料¶
一旦您編寫了一些處理 Unicode 資料的程式碼,下一個問題就是輸入/輸出。您如何將 Unicode 字串輸入到您的程式中,以及如何將 Unicode 轉換為適合儲存或傳輸的形式?
您可能不需要做任何事情,這取決於您的輸入源和輸出目的地;您應該檢查您的應用程式中使用的庫是否原生支援 Unicode。例如,XML 解析器通常會返回 Unicode 資料。許多關係資料庫也支援 Unicode 值列,並且可以從 SQL 查詢中返回 Unicode 值。
Unicode 資料通常在寫入磁碟或透過套接字傳送之前被轉換為特定的編碼。您可以自己完成所有工作:開啟一個檔案,從中讀取一個 8 位位元組物件,並使用 bytes.decode(encoding)
轉換這些位元組。但是,不建議使用手動方法。
一個問題是編碼的多位元組性質;一個 Unicode 字元可以用多個位元組表示。如果您想以任意大小的塊(例如 1024 或 4096 位元組)讀取檔案,則需要編寫錯誤處理程式碼來捕獲在塊末尾僅讀取單個 Unicode 字元的部分位元組編碼的情況。一種解決方案是將整個檔案讀取到記憶體中,然後執行解碼,但這會阻止您處理非常大的檔案;如果您需要讀取一個 2 GiB 的檔案,則需要 2 GiB 的 RAM。(實際上更多,因為至少在片刻,您需要將編碼的字串及其 Unicode 版本都保留在記憶體中。)
解決方案是使用低階解碼介面來捕獲部分編碼序列的情況。實現此功能的工作已經為您完成:內建的 open()
函式可以返回一個類似檔案的物件,該物件假定檔案的內容是指定的編碼,並接受諸如 read()
和 write()
等方法的 Unicode 引數。這透過 open()
的 encoding 和 errors 引數來實現,這些引數的解釋方式與 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
用作位元組順序標記(BOM),通常作為檔案的第一個字元寫入,以便幫助自動檢測檔案的位元組順序。一些編碼(例如 UTF-16)期望檔案開頭存在 BOM;當使用此類編碼時,BOM 將自動作為第一個字元寫入,並且在讀取檔案時將被靜默刪除。這些編碼的變體(例如 'utf-16-le' 和 'utf-16-be',分別用於小端和大端編碼)指定一種特定的位元組順序,並且不跳過 BOM。
在某些領域,也習慣在 UTF-8 編碼檔案的開頭使用“BOM”;這個名稱具有誤導性,因為 UTF-8 不依賴於位元組順序。該標記只是宣告檔案以 UTF-8 編碼。要讀取此類檔案,請使用 'utf-8-sig' 編解碼器來自動跳過該標記(如果存在)。
Unicode 檔名¶
當今常用的作業系統大多數都支援包含任意 Unicode 字元的檔名。通常,這是透過將 Unicode 字串轉換為一些隨系統而異的編碼來實現的。目前,Python 正在趨向於使用 UTF-8:MacOS 上的 Python 已經使用 UTF-8 幾個版本了,Python 3.6 也切換到在 Windows 上使用 UTF-8。在 Unix 系統上,只有在設定了 LANG
或 LC_CTYPE
環境變數時,才會有 檔案系統編碼;如果沒有設定,則預設編碼仍然是 UTF-8。
sys.getfilesystemencoding()
函式返回當前系統上要使用的編碼,以防您想手動進行編碼,但沒有太多必要這樣做。當開啟檔案進行讀取或寫入時,您通常只需提供 Unicode 字串作為檔名,它將自動轉換為適合您的正確編碼
filename = 'filename\u4500abc'
with open(filename, 'w') as f:
f.write('blah\n')
os
模組中的函式(例如 os.stat()
)也將接受 Unicode 檔名。
os.listdir()
函式返回檔名,這會引發一個問題:它應該返回檔名的 Unicode 版本,還是應該返回包含編碼版本的位元組?os.listdir()
可以兩者都做到,具體取決於您是否將目錄路徑作為位元組或 Unicode 字串提供。如果您傳遞一個 Unicode 字串作為路徑,則檔名將使用檔案系統的編碼進行解碼,並返回一個 Unicode 字串列表,而傳遞一個位元組路徑將返回位元組形式的檔名。例如,假設預設的 檔案系統編碼 是 UTF-8,執行以下程式
fn = 'filename\u4500abc'
f = open(fn, 'w')
f.close()
import os
print(os.listdir(b'.'))
print(os.listdir('.'))
將產生以下輸出
$ python listdir-test.py
[b'filename\xe4\x94\x80abc', ...]
['filename\u4500abc', ...]
第一個列表包含 UTF-8 編碼的檔名,第二個列表包含 Unicode 版本。
請注意,在大多數情況下,您應該堅持使用這些 API 的 Unicode。位元組 API 僅應在可能存在無法解碼的檔名的系統上使用;現在幾乎只有 Unix 系統。
編寫 Unicode 感知程式的提示¶
本節提供了一些關於編寫處理 Unicode 的軟體的建議。
最重要的提示是
軟體內部應僅使用 Unicode 字串,儘快解碼輸入資料,並僅在最後編碼輸出。
如果您嘗試編寫同時接受 Unicode 和位元組字串的處理函式,您會發現您的程式在任何組合兩種不同型別的字串的地方都容易出現錯誤。沒有自動編碼或解碼:如果您執行例如 str + bytes
,將引發 TypeError
。
當使用來自 Web 瀏覽器或其他不受信任來源的資料時,一種常見的技術是在生成的命令列中使用字串或將其儲存在資料庫中之前檢查字串中的非法字元。如果您正在這樣做,請小心檢查解碼後的字串,而不是編碼後的位元組資料;某些編碼可能具有有趣的屬性,例如不是雙射的或不是完全 ASCII 相容的。如果輸入資料也指定了編碼,則尤其如此,因為攻擊者可以選擇一種巧妙的方式將惡意文字隱藏在編碼的位元組流中。
檔案編碼之間的轉換¶
StreamRecoder
類可以在編碼之間透明地轉換,接受一個以編碼 #1 返回資料的流,並表現得像一個以編碼 #2 返回資料的流。
例如,如果您有一個 Latin-1 格式的輸入檔案 f,您可以使用 StreamRecoder
將其包裝起來,以返回 UTF-8 編碼的位元組
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
錯誤處理程式會將任何非 ASCII 位元組解碼為 U+DC80 到 U+DCFF 特殊範圍內的程式碼點。當使用 surrogateescape
錯誤處理程式對資料進行編碼並將其寫回時,這些程式碼點將轉換回相同的位元組。
參考資料¶
精通 Python 3 輸入/輸出(David Beazley 在 PyCon 2010 上的演講)的其中一部分討論了文字處理和二進位制資料處理。
Marc-André Lemburg 的演講“用 Python 開發 Unicode 感知應用程式”的 PDF 幻燈片討論了字元編碼問題,以及如何將應用程式國際化和本地化。這些幻燈片僅涵蓋 Python 2.x。
Python 中 Unicode 的內部結構是 Benjamin Peterson 在 PyCon 2013 上的演講,討論了 Python 3.3 中的內部 Unicode 表示。
致謝¶
本文件的初稿由 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。