Unicode 指南?

發(fā)布版本

1.12

本文介紹了 Python 對(duì)表示文本數(shù)據(jù)的 Unicode 規(guī)范的支持,并對(duì)各種 Unicode 常見(jiàn)使用問(wèn)題做了解釋。

Unicode 概述?

定義?

如今的程序需要能夠處理各種各樣的字符。應(yīng)用程序通常做了國(guó)際化處理,用戶(hù)可以選擇不同的語(yǔ)言顯示信息和輸出數(shù)據(jù)。同一個(gè)程序可能需要以英語(yǔ)、法語(yǔ)、日語(yǔ)、希伯來(lái)語(yǔ)或俄語(yǔ)輸出錯(cuò)誤信息。網(wǎng)頁(yè)內(nèi)容可能由這些語(yǔ)言書(shū)寫(xiě),并且可能包含不同的表情符號(hào)。Python 的字符串類(lèi)型采用 Unicode 標(biāo)準(zhǔn)來(lái)表示字符,使得 Python 程序能夠正常處理所有這些不同的字符。

Unicode 規(guī)范(https://www.unicode.org/)旨在羅列人類(lèi)語(yǔ)言所用到的所有字符,并賦予每個(gè)字符唯一的編碼。該規(guī)范一直在進(jìn)行修訂和更新,不斷加入新的語(yǔ)種和符號(hào)。

一個(gè) 字符 是文本的最小組件?!瓵’、‘B’、‘C’ 等都是不同的字符?!ā?和 ‘í’ 也一樣。字符會(huì)隨著語(yǔ)言或者上下文的變化而變化。比如,‘Ⅰ’ 是一個(gè)表示 “羅馬數(shù)字 1” 的字符,它與大寫(xiě)字母 ‘I’ 不同。他們往往看起來(lái)相同,但這是兩個(gè)有著不同含義的字符。

Unicode 標(biāo)準(zhǔn)描述了字符是如何用 碼位(code point) 表示的。碼位的取值范圍是 0 到 0x10FFFF 的整數(shù)(大約 110 萬(wàn)個(gè)值,實(shí)際分配的數(shù)字 沒(méi)有那么多)。在 Unicode 標(biāo)準(zhǔn)和本文中,碼位采用 U+265E 的形式,表示值為 0x265e 的字符(十進(jìn)制為 9822)。

Unicode 標(biāo)準(zhǔn)中包含了許多表格,列出了很多字符及其對(duì)應(yīng)的碼位。

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
...

嚴(yán)格地說(shuō),上述定義暗示了以下說(shuō)法是沒(méi)有意義的:“這是字符 U+265E”。U+265E 只是一個(gè)碼位,代表某個(gè)特定的字符;這里它代表了字符 “國(guó)際象棋黑騎士” '?'。在非正式的上下文中,有時(shí)會(huì)忽略碼位和字符的區(qū)別。

一個(gè)字符在屏幕或紙上被表示為一組圖形元素,被稱(chēng)為 字形(glyph) 。比如,大寫(xiě)字母 A 的字形,是兩筆斜線和一筆橫線,而具體的細(xì)節(jié)取決于所使用的字體。大部分 Python 代碼不必?fù)?dān)心字形,找到正確的顯示字形通常是交給 GUI 工具包或終端的字體渲染程序來(lái)完成。

編碼?

上一段可以歸結(jié)為:一個(gè) Unicode 字符串是一系列碼位(從 0 到 0x10FFFF 或者說(shuō)十進(jìn)制的 1,114,111 的數(shù)字)組成的序列。這一序列在內(nèi)存中需被表示為一組 碼元(code unit) , 碼元 會(huì)映射成包含八個(gè)二進(jìn)制位的字節(jié)。將 Unicode 字符串翻譯成字節(jié)序列的規(guī)則稱(chēng)為 字符編碼 ,或者 編碼 。

大家首先會(huì)想到的編碼可能是用 32 位的整數(shù)作為代碼位,然后采用 CPU 對(duì) 32 位整數(shù)的表示法。字符串 “Python” 用這種表示法可能會(huì)如下所示:

   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

這種表示法非常直白,但也存在 一些問(wèn)題。

  1. 不具可移植性;不同的處理器的字節(jié)序不同。

  2. 非常浪費(fèi)空間。 在大多數(shù)文本中,大部分碼位都小于 127 或 255,因此字節(jié) 0x00 占用了大量空間。相較于 ASCII 表示法所需的 6 個(gè)字節(jié),以上字符串需要占用 24 個(gè)字節(jié)。RAM 用量的增加沒(méi)那么要緊(臺(tái)式計(jì)算機(jī)有成 GB 的 RAM,而字符串通常不會(huì)有那么大),但要把磁盤(pán)和網(wǎng)絡(luò)帶寬的用量增加 4 倍是無(wú)法忍受的。

  3. 與現(xiàn)有的 C 函數(shù)(如 strlen() )不兼容,因此需要采用一套新的寬字符串函數(shù)。

因此這種編碼用得不多,人們轉(zhuǎn)而選擇其他更高效、更方便的編碼,比如 UTF-8。

UTF-8 是最常用的編碼之一,Python 往往默認(rèn)會(huì)采用它。UTF 代表“Unicode Transformation Format”,'8' 表示編碼采用 8 位數(shù)。(UTF-16 和 UTF-32 編碼也是存在的,但其使用頻率不如 UTF-8。)UTF-8 的規(guī)則如下:

  1. 如果碼位 < 128,則直接用對(duì)應(yīng)的字節(jié)值表示。

  2. 如果碼位 >= 128,則轉(zhuǎn)換為 2、3、4 個(gè)字節(jié)的序列,每個(gè)字節(jié)值都位于 128 和 255 之間。

UTF-8 有幾個(gè)很方便的特性:

  1. 可以處理任何 Unicode 碼位。

  2. Unicode 字符串被轉(zhuǎn)換為一個(gè)字節(jié)序列,僅在表示空(null )字符(U+0000)時(shí)才會(huì)包含零值的字節(jié)。這意味著 strcpy() 之類(lèi)的C 函數(shù)可以處理 UTF-8 字符串,而且用那些不能處理字符串結(jié)束符之外的零值字節(jié)的協(xié)議也能發(fā)送。

  3. ASCII 字符串也是也是也是合法的 UTF-8 文本。

  4. UTF-8 相當(dāng)緊湊;大多數(shù)常用字符均可用一兩個(gè)字節(jié)表示。

  5. 如果字節(jié)數(shù)據(jù)被損壞或丟失,則可以找出下一個(gè) UTF-8 碼點(diǎn)的開(kāi)始位置并重新開(kāi)始同步。隨機(jī)的 8 位數(shù)據(jù)也不太可能像是有效的 UTF-8 編碼。

  6. UTF-8 是一種面向字節(jié)的編碼。編碼規(guī)定了每個(gè)字符由一個(gè)或多個(gè)字節(jié)的序列表示。這避免了整數(shù)和雙字節(jié)編碼(如 UTF-16 和 UTF-32)可能出現(xiàn)的字節(jié)順序問(wèn)題,那時(shí)的字節(jié)序列會(huì)因執(zhí)行編碼的硬件而異。

參考文獻(xiàn)?

Unicode Consortium 站點(diǎn) 包含 Unicode 規(guī)范的字符圖表、詞匯表和 PDF 版本。請(qǐng)做好準(zhǔn)備,有些內(nèi)容讀起來(lái)有點(diǎn)難度。該網(wǎng)站上還提供了 Unicode 起源和發(fā)展的`年表 <https://www.unicode.org/history/>`_ 。

在 Computerphile 的 Youtube 頻道上,Tom Scott 簡(jiǎn)要地`討論了 Unicode 和 UTF-8 <https://www.youtube.com/watch?v=MijmeoH9LT4>`_(9 分 36 秒)的歷史。

為了幫助理解該標(biāo)準(zhǔn),Jukka Korpela 編寫(xiě)了閱讀 Unicode 字符表的`介紹性指南 <http://jkorpela.fi/unicode/guide.html>`_ 。

Joel Spolsky 撰寫(xiě)了另一篇不錯(cuò)的介紹性文章 <https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character- set-no-excuses/>`_ 。如果本文沒(méi)讓您弄清楚,那應(yīng)在繼續(xù)之前先試著讀讀這篇文章。

Wikipedia 條目通常也有幫助;請(qǐng)參閱“字符編碼”和 UTF-8 的條目,例如:

Python對(duì)Unicode的支持?

現(xiàn)在您已經(jīng)了解了 Unicode 的基礎(chǔ)知識(shí),可以看下 Python 的 Unicode 特性。

字符串類(lèi)型?

從 Python 3.0 開(kāi)始, str 類(lèi)型包含了 Unicode 字符,這意味著用``"unicode rocks!"、'unicode rocks!'`` 或三重引號(hào)字符串語(yǔ)法創(chuàng)建的任何字符串都會(huì)存儲(chǔ)為 Unicode。

Python 源代碼的默認(rèn)編碼是 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 還支持在標(biāo)識(shí)符中使用 Unicode 字符:

répertoire = "/tmp/records.log"
with open(répertoire, "w") as f:
    f.write("test\n")

如果無(wú)法在編輯器中輸入某個(gè)字符,或出于某種原因想只保留 ASCII 編碼的源代碼,則還可以在字符串中使用轉(zhuǎn)義序列。(根據(jù)系統(tǒng)的不同,可能會(huì)看到真的大寫(xiě) 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'

此外,可以用 bytesdecode() 方法創(chuàng)建一個(gè)字符串。 該方法可以接受 encoding 參數(shù),比如可以為 UTF-8 ,以及可選的 errors 參數(shù)。

若無(wú)法根據(jù)編碼規(guī)則對(duì)輸入字符串進(jìn)行編碼,errors 參數(shù)指定了響應(yīng)策略。 該參數(shù)的合法值可以是 'strict' (觸發(fā) UnicodeDecodeError 異常)、'replace' (用 U+FFFDREPLACEMENT CHARACTER)、'ignore' (只是將字符從 Unicode 結(jié)果中去掉),或 'backslashreplace' (插入一個(gè) \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'

編碼格式以包含編碼格式名稱(chēng)的字符串來(lái)指明。 Python 有大約 100 種不同的編碼格式;清單詳見(jiàn) Python 庫(kù)參考文檔 標(biāo)準(zhǔn)編碼。 一些編碼格式有多個(gè)名稱(chēng),比如 'latin-1'、'iso_8859_1''8859 都是指同一種編碼。

利用內(nèi)置函數(shù) chr() 還可以創(chuàng)建單字符的 Unicode 字符串,該函數(shù)可接受整數(shù)參數(shù),并返回包含對(duì)應(yīng)碼位的長(zhǎng)度為 1 的 Unicode 字符串。內(nèi)置函數(shù) ord() 是其逆操作,參數(shù)為單個(gè)字符的 Unicode 字符串,并返回碼位值:

>>>
>>> chr(57344)
'\ue000'
>>> ord('\ue000')
57344

轉(zhuǎn)換為字節(jié)?

bytes.decode() 的逆方法是 str.encode() ,它會(huì)返回 Unicode 字符串的 bytes 形式,已按要求的 encoding 進(jìn)行了編碼。

參數(shù) errors 的意義與 decode() 方法相同,但支持更多可能的handler。除了 'strict' 、 'ignore''replace' (這時(shí)會(huì)插入問(wèn)號(hào)替換掉無(wú)法編碼的字符),還有 'xmlcharrefreplace' (插入一個(gè) XML 字符引用)、 backslashreplace (插入一個(gè) \uNNNN 轉(zhuǎn)義序列)和 namereplace (插入一個(gè) \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'&#40960;abcd&#1972;'
>>> u.encode('ascii', 'backslashreplace')
b'\\ua000abcd\\u07b4'
>>> u.encode('ascii', 'namereplace')
b'\\N{YI SYLLABLE IT}abcd\\u07b4'

用于注冊(cè)和訪問(wèn)可用編碼格式的底層函數(shù),位于 codecs 模塊中。 若要實(shí)現(xiàn)新的編碼格式,則還需要了解 codecs 模塊。 不過(guò)該模塊返回的編碼和解碼函數(shù)通常更為底層一些,不大好用,編寫(xiě)新的編碼格式是一項(xiàng)專(zhuān)業(yè)的任務(wù),因此本文不會(huì)涉及該模塊。

Python 源代碼中的 Unicode 文字?

在 Python 源代碼中,可以用 \u 轉(zhuǎn)義序列書(shū)寫(xiě)特定的 Unicode 碼位,該序列后跟 4 個(gè)代表碼位的十六進(jìn)制數(shù)字。\U 轉(zhuǎn)義序列用法類(lèi)似,但要用8 個(gè)十六進(jìn)制數(shù)字,而不是 4 個(gè):

>>>
>>> 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]

對(duì)大于 127 的碼位使用轉(zhuǎn)義序列,數(shù)量不多時(shí)沒(méi)什么問(wèn)題,但如果要用到很多重音字符,這會(huì)變得很煩人,類(lèi)似于程序中的信息是用法語(yǔ)或其他使用重音的語(yǔ)言寫(xiě)的。也可以用內(nèi)置函數(shù) chr() 拼裝字符串,但會(huì)更加乏味。

理想情況下,都希望能用母語(yǔ)的編碼書(shū)寫(xiě)文本。還能用喜好的編輯器編輯 Python 源代碼,編輯器要能自然地顯示重音符,并在運(yùn)行時(shí)使用正確的字符。

默認(rèn)情況下,Python 支持以 UTF-8 格式編寫(xiě)源代碼,但如果聲明要用的編碼,則幾乎可以使用任何編碼。只要在源文件的第一行或第二行包含一個(gè)特殊注釋即可:

#!/usr/bin/env python
# -*- coding: latin-1 -*-

u = 'abcdé'
print(ord(u[-1]))

上述語(yǔ)法的靈感來(lái)自于 Emacs 用于指定文件局部變量的符號(hào)。Emacs 支持許多不同的變量,但 Python 僅支持“編碼”。 -*- 符號(hào)向 Emacs 標(biāo)明該注釋是特殊的;這對(duì) Python 沒(méi)有什么意義,只是一種約定。Python 會(huì)在注釋中查找 coding: namecoding=name 。

如果沒(méi)有這種注釋?zhuān)瑒t默認(rèn)編碼將會(huì)是前面提到的 UTF-8。更多信息請(qǐng)參閱 PEP 263 。

Unicode屬性?

Unicode 規(guī)范包含了一個(gè)碼位信息數(shù)據(jù)庫(kù)。對(duì)于定義的每一個(gè)碼位,都包含了字符的名稱(chēng)、類(lèi)別、數(shù)值(對(duì)于表示數(shù)字概念的字符,如羅馬數(shù)字、分?jǐn)?shù)如三分之一和五分之四等)。還有有關(guān)顯示的屬性,比如如何在雙向文本中使用碼位。

以下程序顯示了幾個(gè)字符的信息,并打印一個(gè)字符的數(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]))

當(dāng)運(yùn)行時(shí),這將打印出:

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

類(lèi)別代碼是描述字符性質(zhì)的一個(gè)縮寫(xiě)。分為“字母”、“數(shù)字”、“標(biāo)點(diǎn)符號(hào)”或“符號(hào)”等類(lèi)別,而這些類(lèi)別又分為子類(lèi)別。就以上輸出的代碼而言,'Ll' 表示“字母,小寫(xiě)”,'No' 表示“數(shù)字,其他”,'Mn' 表示“標(biāo)記,非空白符” , 'So' 是“符號(hào),其他”。有關(guān)類(lèi)別代碼的清單,請(qǐng)參閱 Unicode 字符庫(kù)文檔 <https://www.unicode.org/reports/tr44/#General_Category_Values>`_ 的“通用類(lèi)別值”部分。

字符串比較?

Unicode 讓字符串的比較變得復(fù)雜了一些,因?yàn)橥唤M字符可能由不同的碼位序列組成。例如,像“ê”這樣的字母可以表示為單碼位 U+00EA,或是 U+0065 U+0302,即“e”的碼位后跟“COMBINING CIRCUMFLEX ACCENT”的碼位。雖然在打印時(shí)會(huì)產(chǎn)生同樣的輸出,但一個(gè)是長(zhǎng)度為 1 的字符串,另一個(gè)是長(zhǎng)度為 2 的字符串。

一種不區(qū)分大小寫(xiě)比較的工具是字符串方法 casefold() ,將按照 Unicode 標(biāo)準(zhǔn)描述的算法將字符串轉(zhuǎn)換為不區(qū)分大小寫(xiě)的形式。該算法對(duì)諸如德語(yǔ)字母“?”(代碼點(diǎn) U+00DF)之類(lèi)的字符進(jìn)行了特殊處理,變?yōu)橐粚?duì)小寫(xiě)字母“ss”。

>>>
>>> street = 'Gürzenichstra?e'
>>> street.casefold()
'gürzenichstrasse'

第二個(gè)工具是 unicodedata 模塊的 normalize() 函數(shù),將字符串轉(zhuǎn)換為幾種規(guī)范化形式之一,其中后跟組合字符的字母將被替換為單個(gè)字符。 normalize() 可用于執(zhí)行字符串比較,即便兩個(gè)字符串采用不同的字符組合,也不會(huì)錯(cuò)誤地報(bào)告兩者不相等:

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))

當(dāng)運(yùn)行時(shí),這將輸出:

$ python3 compare-strs.py
length of first string= 1
length of second string= 2
True

normalize() 函數(shù)的第一個(gè)參數(shù)是個(gè)字符串,給出所需的規(guī)范化形式,可以是“NFC”、“NFKC”、“NFD”和“NFKD”之一。

Unicode 標(biāo)準(zhǔn)還設(shè)定了如何進(jìn)行不區(qū)分大小寫(xiě)的比較:

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() 會(huì)被調(diào)用兩次?因?yàn)橛袔讉€(gè)字符讓 casefold() 返回非規(guī)范化的字符串,所以結(jié)果需要再次進(jìn)行規(guī)范化。參見(jiàn) Unicode 標(biāo)準(zhǔn)的 3.13 節(jié) 的一個(gè)討論和示例。)

Unicode 正則表達(dá)式?

re 模塊支持的正則表達(dá)式可以用字節(jié)串或字符串的形式提供。有一些特殊字符序列,比如 \d\w 具有不同的含義,具體取決于匹配模式是以字節(jié)串還是字符串形式提供的。例如,\d 將匹配字節(jié)串中的字符 [0-9] ,但對(duì)于字符串將會(huì)匹配 'Nd' 類(lèi)別中的任何字符。

上述示例中的字符串包含了泰語(yǔ)和阿拉伯?dāng)?shù)字書(shū)寫(xiě)的數(shù)字 57:

import re
p = re.compile(r'\d+')

s = "Over \u0e55\u0e57 57 flavours"
m = p.search(s)
print(repr(m.group()))

執(zhí)行時(shí),\d+ 將匹配上泰語(yǔ)數(shù)字并打印出來(lái)。如果向 compile() 提供的是 re.ASCII 標(biāo)志,\d+ 則會(huì)匹配子串 "57"。

類(lèi)似地,\w 將匹配多種 Unicode 字符,但對(duì)于字節(jié)串則只會(huì)匹配 [a-zA-Z0-9_] ,如果指定 re.ASCII, \s `` 將匹配 Unicode 空白符或 ``[ \t\n\r\f\v]。

參考文獻(xiàn)?

關(guān)于 Python 的 Unicode 支持,其他還有一些很好的討論:

str 類(lèi)型在 Python 庫(kù)參考文檔 文本序列類(lèi)型 --- str 中有介紹。

unicodedata 模塊的文檔

codecs 模塊的文檔

Marc-André Lemburg 在 EuroPython 2002 上做了一個(gè)題為“Python 和 Unicode”(PDF 幻燈片)<https://downloads.egenix.com/python/Unicode-EPC2002-Talk.pdf>`_ 的演示文稿。該幻燈片很好地概括了 Python 2 的 Unicode 功能設(shè)計(jì)(其中 Unicode 字符串類(lèi)型稱(chēng)為 unicode,文字以 u 開(kāi)頭)。

Unicode 數(shù)據(jù)的讀寫(xiě)?

既然處理 Unicode 數(shù)據(jù)的代碼寫(xiě)好了,下一個(gè)問(wèn)題就是輸入/輸出了。如何將 Unicode 字符串讀入程序,如何將 Unicode 轉(zhuǎn)換為適于存儲(chǔ)或傳輸?shù)男问侥兀?/p>

根據(jù)輸入源和輸出目標(biāo)的不同,或許什么都不用干;請(qǐng)檢查一下應(yīng)用程序用到的庫(kù)是否原生支持 Unicode。例如,XML 解析器往往會(huì)返回 Unicode 數(shù)據(jù)。許多關(guān)系數(shù)據(jù)庫(kù)的字段也支持 Unicode 值,并且 SQL 查詢(xún)也能返回 Unicode 值。

在寫(xiě)入磁盤(pán)或通過(guò)套接字發(fā)送之前,Unicode 數(shù)據(jù)通常要轉(zhuǎn)換為特定的編碼??梢宰约和瓿伤泄ぷ鳎捍蜷_(kāi)一個(gè)文件,從中讀取一個(gè) 8 位字節(jié)對(duì)象,然后用 bytes.decode(encoding) 對(duì)字節(jié)串進(jìn)行轉(zhuǎn)換。但是,不推薦采用這種全人工的方案。

編碼的多字節(jié)特性就是一個(gè)難題; 一個(gè) Unicode 字符可以用幾個(gè)字節(jié)表示。 如果要以任意大小的塊(例如 1024 或 4096 字節(jié))讀取文件,那么在塊的末尾可能只讀到某個(gè) Unicode 字符的部分字節(jié),這就需要編寫(xiě)錯(cuò)誤處理代碼。 有一種解決方案是將整個(gè)文件讀入內(nèi)存,然后進(jìn)行解碼,但這樣就沒(méi)法處理很大的文件了;若要讀取 2 GB 的文件,就需要 2 GB 的 RAM。(其實(shí)需要的內(nèi)存會(huì)更多些,因?yàn)橹辽儆幸欢螘r(shí)間需要在內(nèi)存中同時(shí)存放已編碼字符串及其 Unicode 版本。)

解決方案是利用底層解碼接口去捕獲編碼序列不完整的情況。這部分代碼已經(jīng)是現(xiàn)成的:內(nèi)置函數(shù) open() 可以返回一個(gè)文件類(lèi)的對(duì)象,該對(duì)象認(rèn)為文件的內(nèi)容采用指定的編碼,read()write() 等方法接受 Unicode 參數(shù)。只要用 open()encodingerrors 參數(shù)即可,參數(shù)釋義同 str.encode()bytes.decode() 。

因此從文件讀取 Unicode 就比較簡(jiǎn)單了:

with open('unicode.txt', encoding='utf-8') as f:
    for line in f:
        print(repr(line))

也可以在更新模式下打開(kāi)文件,以便同時(shí)讀取和寫(xiě)入:

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é)順序標(biāo)記(BOM),通常作為文件的第一個(gè)字符寫(xiě)入,以幫助自動(dòng)檢測(cè)文件的字節(jié)順序。某些編碼(例如 UTF-16)期望在文件開(kāi)頭出現(xiàn) BOM;當(dāng)采用這種編碼時(shí),BOM 將自動(dòng)作為第一個(gè)字符寫(xiě)入,并在讀取文件時(shí)會(huì)靜默刪除。這些編碼有多種變體,例如用于 little-endian 和 big-endian 編碼的 “utf-16-le” 和 “utf-16-be”,會(huì)指定一種特定的字節(jié)順序并且不會(huì)忽略 BOM。

在某些地區(qū),習(xí)慣在 UTF-8 編碼文件的開(kāi)頭用上“BOM”;此名稱(chēng)具有誤導(dǎo)性,因?yàn)?UTF-8 與字節(jié)順序無(wú)關(guān)。此標(biāo)記只是聲明該文件以 UTF-8 編碼。要讀取此類(lèi)文件,請(qǐng)使用“utf-8-sig”編解碼器自動(dòng)忽略此標(biāo)記。

Unicode 文件名?

當(dāng)今大多數(shù)操作系統(tǒng)都支持包含任意 Unicode 字符的文件名。 通常這是通過(guò)將 Unicode 字符串轉(zhuǎn)換為某種根據(jù)具體系統(tǒng)而定的編碼格式來(lái)實(shí)現(xiàn)的。 如今的 Python 傾向于使用 UTF-8:MacOS 上的 Python 已經(jīng)在多個(gè)版本中使用了 UTF-8,而 Python 3.6 也已在 Windows 上改用了 UTF-8。 在 Unix 系統(tǒng)中,將只有一個(gè) 文件系統(tǒng)編碼格式。 如果你已設(shè)置了 LANGLC_CTYPE 環(huán)境變量的話;如果未設(shè)置,則默認(rèn)編碼格式還是 UTF-8。

sys.getfilesystemencoding() 函數(shù)將返回要在當(dāng)前系統(tǒng)采用的編碼,若想手動(dòng)進(jìn)行編碼時(shí)即可用到,但無(wú)需多慮。在打開(kāi)文件進(jìn)行讀寫(xiě)時(shí),通常只需提供 Unicode 字符串作為文件名,會(huì)自動(dòng)轉(zhuǎn)換為合適的編碼格式:

filename = 'filename\u4500abc'
with open(filename, 'w') as f:
    f.write('blah\n')

os 模塊中的函數(shù)也能接受 Unicode 文件名,如 os.stat()

os.listdir() 函數(shù)返回文件名,這引發(fā)了一個(gè)問(wèn)題:它應(yīng)該返回文件名的 Unicode 版本,還是應(yīng)該返回包含已編碼版本的字節(jié)串? 這兩者 os.listdir() 都能做到,具體取決于你給出的目錄路徑是字節(jié)串還是 Unicode 字符串形式的。 如果你傳入一個(gè) Unicode 字符串作為路徑,文件名將使用文件系統(tǒng)的編碼格式進(jìn)行解碼并返回一個(gè) Unicode 字符串列表,而傳入一個(gè)字節(jié)串形式的路徑則將返回字節(jié)串形式的文件名。 例如,假定默認(rèn) 文件系統(tǒng)編碼 為 UTF-8,運(yùn)行以下程序:

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', ...]

第一個(gè)列表包含 UTF-8 編碼的文件名,第二個(gè)列表則包含 Unicode 版本的。

請(qǐng)注意,大多時(shí)候應(yīng)該堅(jiān)持用這些 API 處理 Unicode。字節(jié)串 API 應(yīng)該僅用于可能存在不可解碼文件名的系統(tǒng);現(xiàn)在幾乎僅剩 Unix 系統(tǒng)了。

識(shí)別 Unicode 的編程技巧?

本節(jié)提供了一些關(guān)于編寫(xiě) Unicode 處理軟件的建議。

最重要的技巧如下:

程序應(yīng)只在內(nèi)部處理 Unicode 字符串,盡快對(duì)輸入數(shù)據(jù)進(jìn)行解碼,并只在最后對(duì)輸出進(jìn)行編碼。

如果嘗試編寫(xiě)的處理函數(shù)對(duì) Unicode 和字節(jié)串形式的字符串都能接受,就會(huì)發(fā)現(xiàn)組合使用兩種不同類(lèi)型的字符串時(shí),容易產(chǎn)生差錯(cuò)。沒(méi)辦法做到自動(dòng)編碼或解碼:如果執(zhí)行 str + bytes,則會(huì)觸發(fā) TypeError。

當(dāng)要使用的數(shù)據(jù)來(lái)自 Web 瀏覽器或其他不受信來(lái)源時(shí),常用技術(shù)是在用該字符串生成命令行之前,或要存入數(shù)據(jù)庫(kù)之前,先檢查字符串中是否包含非法字符。請(qǐng)仔細(xì)檢查解碼后的字符串,而不是編碼格式的字節(jié)串?dāng)?shù)據(jù);有些編碼可能具備一些有趣的特性,例如與 ASCII 不是一一對(duì)應(yīng)或不完全兼容。如果輸入數(shù)據(jù)還指定了編碼格式,則尤其如此,因?yàn)楣粽呖梢赃x擇一種巧妙的方式將惡意文本隱藏在經(jīng)過(guò)編碼的字節(jié)流中。

在文件編碼格式之間進(jìn)行轉(zhuǎn)換?

StreamRecoder 類(lèi)可以在兩種編碼之間透明地進(jìn)行轉(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') )

編碼格式未知的文件?

若需對(duì)文件進(jìn)行修改,但不知道文件的編碼,那該怎么辦呢?如果已知編碼格式與 ASCII 兼容,并且只想查看或修改 ASCII 部分,則可利用 surrogateescape 錯(cuò)誤處理 handler 打開(kāi)文件:

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 錯(cuò)誤處理 handler 會(huì)把所有非 ASCII 字節(jié)解碼為 U+DC80 至 U+DCFF 這一特殊范圍的碼位。當(dāng) surrogateescape 錯(cuò)誤處理 handler用于數(shù)據(jù)編碼并回寫(xiě)時(shí),這些碼位將轉(zhuǎn)換回原樣。

參考文獻(xiàn)?

David Beazley 在 PyCon 2010 上的演講 掌握 Python 3 輸入/輸出 中,有一節(jié)討論了文本和二進(jìn)制數(shù)據(jù)的處理。

Marc-André Lemburg 演示的PDF 幻燈片“在 Python 中編寫(xiě)支持 Unicode 的應(yīng)用程序” ,討論了字符編碼問(wèn)題以及如何國(guó)際化和本地化應(yīng)用程序。這些幻燈片僅涵蓋 Python 2.x。

Python Unicode 實(shí)質(zhì) 是 Benjamin Peterson 在 PyCon 2013 上的演講,討論了 Unicode 在 Python 3.3 中的內(nèi)部表示。

致謝?

本文初稿由 Andrew Kuchling 撰寫(xiě)。此后,Alexander Belopolsky、Georg Brandl、Andrew Kuchling 和 Ezio Melotti 作了進(jìn)一步修訂。

感謝以下各位指出本文錯(cuò)誤或提出建議:é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。