關於字元 位元組 python3

weixin_33816946發表於2012-01-24

http://woodpecker.org.cn/diveintopython3/strings.html

字串

I’m telling you this ’cause you’re one of my friends.
My alphabet starts where your alphabet ends!
— Dr. Seuss, On Beyond Zebra!

顯示目錄

在開始之前需要掌握的一些知識#

你是否知道 Bougainville 人有世界上最小的字母表?他們的 Rotokas 字母表只包含了12個字母: A, E, G, I, K, O, P, R, S, T, U, 和 V。另一方面,像漢語,日語和韓語這些語言,它們則有成千上萬個字元。當然啦,英語共有26個字母 — 如果把大寫和小寫分別計算的話,52個 — 外加少量的標點符號,比如!@#$%&

當人們說起“文字”,他們通常指顯示在螢幕上的字元或者其他的記號;但是計算機不能直接處理這些字元和標記;它們只認識位(bit)和位元組(byte)。實際上,從螢幕上的每一塊文字都是以某種字元編碼(character encoding)的方式儲存的。粗略地說就是,字元編碼提供一種對映,使螢幕上顯示的內容和記憶體、磁碟記憶體儲的內容對應起來。有許多種不同的字元編碼,有一些是為特定的語言,比如俄語、中文或者英語,設計、優化的,另外一些則可以用於多種語言的編碼。

在實際操作中則會比上邊描述的更復雜一些。許多字元在幾種編碼裡是共用的,但是在實際的記憶體或者磁碟上,不同的編碼方式可能會使用不同的字 節序列來儲存他們。所以,你可以把字元編碼當做一種解碼金鑰。當有人給你一個位元組序列 — 檔案,網頁,或者別的什麼 — 並且告訴你它們是“文字”時, 就需要知道他們使用了何種編碼方式,然後才能將這些位元組序列解碼成字元。如果他們給的是錯誤的“金鑰”或者根本沒有給你“金鑰”,那就得自己來破解這段編 碼,這可是一個艱難的任務。有可能你使用了錯誤的解碼方式,然後出現一些莫名其妙的結果。

你所瞭解的關於字串的知識都是錯的。

你肯定見過這樣的網頁,在撇號(')該出現的地方被奇怪的像問號的字元替代了。這種情況通常意味著頁面的作者沒有正確的宣告其使用的編碼方式,瀏覽器只能自己來猜測,結果就是一些正確的和意料之外的字元的混合體。如果原文是英語,那只是不方便閱讀而已;在其他的語言環境下,結果可能是完全不可讀的。

現有的字元編碼各類給世界上每種主要的語言都提供了編碼方案。由於每種語言的各不相同,而且在以前記憶體和硬碟都很昂貴,所以每種字元編碼都為特定的語言做了優化。上邊這句話的意思是,每種編碼都使用數字(0–255)來代表這種語言的字元。比如,你也許熟悉ASCII編碼,它將英語中的字元都當做從0–127的數字來儲存。(65表示大寫的“A”,97表示小寫的“a”,&c。)英語的字母表很簡單,所以它能用不到128個數字表達出來。如果你懂得2進位制計數的話,它只使用了一個位元組內的7位。

西歐的一些語言,比如法語,西班牙語和德語等,比英語有更多的字母。或者,更準確的說,這些語言含有與變音符號(diacritical marks)組合起來的字母,像西班牙語裡的ñ。這些語言最常用的編碼方式是CP-1252,又叫做“windows-1252”,因為它在微軟的視窗作業系統上被廣泛使用。CP-1252和ASCII在0–127這個範圍內的字元是一樣的,但是CP-1252為ñ(n-with-a-tilde-over-it, 241),Ü(u-with-two-dots-over-it, 252)這類字元而擴充套件到了128–255這個範圍。然而,它仍然是一種單位元組的編碼方式;可能的最大數字為255,這仍然可以用一個位元組來表示。

然而,像中文,日語和韓語等語言,他們的字元如此之多而不得不需要多位元組編碼的字符集。即,使用兩個位元組的數字(0–255)代表每個“字 符”。但是就跟不同的單位元組編碼方式一樣,多位元組編碼方式之間也有同樣的問題,即他們使用的數字是相同的,但是表達的內容卻不同。相對於單位元組編碼方式它 們只是使用的數字範圍更廣一些,因為有更多的字元需要表示。

在沒有網路的時代,“文字”由自己輸入,偶爾才會列印出來,大多數情況下使用以上的編碼方案是可行的。那時沒有太多的“純文字”。原始碼使用ASCII編碼,其他人也都使用字處理器,這些字處理器定義了他們自己的格式(非文字的),這些格式會連同字元編碼資訊和風格樣式一起記錄其中,&c。人們使用與原作者相同的字處理軟體讀取這些文件,所以或多或少地能夠使用。

現在,我們考慮一下像email和web這樣的全球網路的出現。大量的“純文字”檔案在全球範圍內流轉,它們在一臺電腦上被撰寫出來,通過 第二臺電腦進行傳輸,最後在另外一臺電腦上顯示。計算機只能識別數字,但是這些數字可能表達的是其他的東西。Oh no! 怎麼辦呢。。好吧,那麼系統必須被設計成在每一段“純文字”上都搭載編碼資訊。記住,編碼方式是將計算機可讀的數字對映成人類可讀的字元的解碼金鑰。失去 解碼金鑰則意味著混亂不清的,莫名其妙的資訊,或者更糟。

現在我們考慮嘗試把多段文字儲存在同一個地方,比如放置所有收到郵件的資料庫。這仍然需要對每段文字儲存其相關的字元編碼資訊,只有這樣才能正確地顯示它們。這很困難嗎?試試搜尋你的email資料庫,這意味著需要在執行時進行編碼之間的轉換。很有趣是吧…

現在我們來分析另外一種可能性,即多語言文件,同一篇文件裡來自幾種不同語言的字元混在一起。(提示:處理這樣文件的程式通常使用轉義符在 不同的“模式(modes)”之間切換。噗!現在是俄語 koi8-r 模式,所以241代表 Я;噗噗!現在到了Mac Greek模式,所以241代表 ώ。)當然,你也會想要搜尋這些文件。

現在,你就哭吧,因為以前所瞭解的關於字串的知識都是錯的,根本就沒有所謂的“純文字”。

Unicode#

Unicode入門。

Unicode編碼系統為表達任意語言的任意字元而設計。它使用4位元組的數字來表達每個字母、符號, 或者表意文字(ideograph)。每個數字代表唯一的至少在某種語言中使用的符號。(並不是所有的數字都用上了,但是總數已經超過了65535,所以 2個位元組的數字是不夠用的。)被幾種語言共用的字元通常使用相同的數字來編碼,除非存在一個在理的語源學(etymological)理由使不這樣做。不 考慮這種情況的話,每個字元對應一個數字,每個數字對應一個字元。即不存在二義性。不再需要記錄“模式”了。U+0041總是代表'A',即使這種語言沒有'A'這個字元。

初次面對這個創想,它看起來似乎很偉大。一種編碼方式即可解決所有問題。文件可包含多種語言。不再需要在各種編碼方式之間進行“模式轉換“。但是很快,一個明顯的問題跳到我們面前。4個位元組?只為了單獨一個字元 這似乎太浪費了,特別是對像英語和西語這樣的語言,他們只需要不到1個位元組即可以表達所需的字元。事實上,對於以象形為基礎的語言(比如中文)這種方法也有浪費,因為這些語言的字元也從來不需要超過2個位元組即可表達。

有一種Unicode編碼方式每1個字元使用4個位元組。它叫做UTF-82,因為32位 = 4位元組。UTF-32是一種直觀的編碼方式;它收錄每一個Unicode字元(4位元組數字)然後就以那個數字代表該字元。這種方法有其優點,最重要的一點 就是可以在常數時間內定位字串裡的第N個字元,因為第N個字元從第4×Nth個位元組開始。另外,它也有其缺點,最明顯的就是它使用4個“詭異”的位元組來儲存每個“詭異”的字元…

儘管有Unicode字元非常多,但是實際上大多數人不會用到超過前65535個以外的字元。因此,就有了另外一種Unicode編碼方 式,叫做UTF-16(因為16位 = 2位元組)。UTF-16將0–65535範圍內的字元編碼成2個位元組,如果真的需要表達那些很少使用的“星芒層(astral plane)” 內超過這65535範圍的Unicode字元,則需要使用一些詭異的技巧來實現。UTF-16編碼最明顯的優點是它在空間效率上比UTF-32高兩倍,因 為每個字元只需要2個位元組來儲存(除去65535範圍以外的),而不是UTF-32中的4個位元組。並且,如果我們假設某個字串不包含任何星芒層中的字 符,那麼我們依然可以在常數時間內找到其中的第N個字元,直到它不成立為止這總是一個不錯的推斷…

但是對於UTF-32和UTF-16編碼方式還有一些其他不明顯的缺點。不同的計算機系統會以不同的順序儲存位元組。這意味著字元U+4E2D在UTF-16編碼方式下可能被儲存為4E 2D或者2D 4E, 這取決於該系統使用的是大尾端(big-endian)還是小尾端(little-endian)。(對於UTF-32編碼方式,則有更多種可能的位元組排 列。)只要文件沒有離開你的計算機,它還是安全的 — 同一臺電腦上的不同程式使用相同的位元組順序(byte order)。但是當我們需要在系統之間傳輸這個文件的時候,也許在全球資訊網中,我們就需要一種方法來指示當前我們的位元組是怎樣儲存的。不然的話,接收文件 的計算機就無法知道這兩個位元組4E 2D表達的到底是U+4E2D還是U+2D4E

為了解決這個問題,多位元組的Unicode編碼方式定義了一個“位元組順序標記(Byte Order Mark)”,它是一個特殊的非列印字元,你可以把它包含在文件的開頭來指示你所使用的位元組順序。對於UTF-16,位元組順序標記是U+FEFF。如果收到一個以位元組FF FE開頭的UTF-16編碼的文件,你就能確定它的位元組順序是單向的(one way)的了;如果它以FE FF開頭,則可以確定位元組順序反向了。

不過,UTF-16還不夠完美,特別是要處理許多ASCII字元時。如果仔細想想的話,甚至一箇中文網頁也會包含許多的ASCII字元 — 所有包圍在可列印中文字元周圍的元素(element)和屬性(attribute)。能夠在常數時間內找到第Nth個字元當然非常好,但是依然存在著糾纏不休的星芒層字元的問題,這意味著你不能保證每個字元都是2個位元組長,所以,除非你維護著另外一個索引,不然就不能真正意義上的在常數時間內定位第N個字元。另外,朋友,世界上肯定還存在很多的ASCII文字…

另外一些人琢磨著這些問題,他們找到了一種解決方法:

UTF-8 The range of integers used to code the abstract characters is called the codespace. A particular integer in this set is called a code point. When an abstract character is mapped or assigned to a particular code point in the codespace, it is then referred to as an encoded character. <-->

UTF-8是一種為Unicode設計的變長(variable-length)編碼系統。即,不同的字元可使用不同數量的位元組編碼。對於ASCII字元(A-Z, &c.)UTF-8僅使用1個位元組來編碼。事實上,UTF-8中前128個字元(0–127)使用的是跟ASCII一樣的編碼方式。像ñ和ö這樣的“擴充套件拉丁字元(Extended Latin)”則使用2個位元組來編碼。(這裡的位元組並不是像UTF-16中那樣簡單的Unicode編碼點(unicode code point);它使用了一些位變換(bit-twiddling)。)中文字元比如“中”則佔用了3個位元組。很少使用的“星芒層字元”則佔用4個位元組。

缺點:因為每個字元使用不同數量的位元組編碼,所以尋找串中第N個字元是一個O(N)複雜度的操作 — 即,串越長,則需要更多的時間來定位特定的字元。同時,還需要位變換來把字元編碼成位元組,把位元組解碼成字元。

優點:在處理經常會用到的ASCII字元方面非常有效。在處理擴充套件的拉丁字符集方面也不比UTF-16差。對於中文字元來說,比UTF-32要好。同時,(在這一條上你得相信我,因為我不打算給你展示它的數學原理。)由位操作的天性使然,使用UTF-8不再存在位元組順序的問題了。一份以UTF-8編碼的文件在不同的計算機之間是一樣的位元流。

概述#

在Python 3,所有的字串都是使用Unicode編碼的字元序列。不再存在以UTF-8或者CP-1252編碼的情況。也就是說,“這個字串是以UTF-8編碼的嗎?不再是一個有效問題。”UTF-8是一種將字元編碼成位元組序列的方式。如果需要將字串轉換成特定編碼的位元組序列,Python 3可以為你做到。如果需要將一個位元組序列轉換成字串,Python 3也能為你做到。位元組即位元組,並非字元。字元在計算機內只是一種抽象。字串則是一種抽象的序列。

跳過該程式碼清單

為了建立一個字串,將其用引號包圍。Python字串可以通過單引號(')或者雙引號(")來定義。
內建函式len()可返回字串的長度,字元的個數。這與獲得列表,元組,集合或者字典的長度的函式是同一個。Python中,字串可以想像成由字元組成的元組。
Just like getting individual items out of a list, you can get individual characters out of a string using index notation.
與取得列表中的元素一樣,也可以通過下標記號取得字串中的某個字元。
類似列表,可以使用+操作符來連線(concatenate)字串。

格式化字串#

字串可以使用單引號或者雙引號來定義。

我們再來看一看humansize.py

跳過該程式碼清單

SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],        
           
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}

def approximate_size(size, a_kilobyte_is_1024_bytes=True):
    '''Convert a file size to human-readable form.                          

    Keyword arguments:
    size -- file size in bytes
    a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
                                if False, use multiples of 1000

    Returns: string

    '''                                                                    
   
if size < 0:
        raise ValueError('number must be non-negative')                    

    multiple
= 1024 if a_kilobyte_is_1024_bytes else 1000
   
for suffix in SUFFIXES[multiple]:
        size
/= multiple
       
if size < multiple:
            return '{0:.1f} {1}'.format(size, suffix)                      

   
raise ValueError('number too large')
'KB', 'MB', 'GB'… 這些是字串。
函式的文件字串(docstring)也是字串。當前的文件字串佔用了多行,所以它使用了相鄰的3個引號來標記字串的起始和終止。
這3個引號代表該文件字串的終止。
這是另外一個字串,作為一個可讀的提示資訊傳遞給異常。
瓦哦…那是什麼?

Python 3支援把值格式化(format)成字串。可以有非常複雜的表示式,最基本的用法是使用單個佔位符(placeholder)將一個值插入字串。

跳過該程式碼清單

不,PapayaWhip真的不是我的密碼。
這裡包含了很多知識。首先,這裡使用了一個字串字面值的方法呼叫。字串也是物件,物件則有其方法。其次,整個表示式返回一個字串。最後,{0}{1} 叫做替換欄位(replacement field),他們會被傳遞給format()方法的引數替換。

複合欄位名#

在前一個例子中,替換欄位只是簡單的整數,這是最簡單的用法。整型替換欄位被當做傳給format()方法的引數列表的位置索引。即,{0}會被第一個引數替換(在此例中即username),{1}被第二個引數替換(password),&c。可以有跟引數一樣多的替換欄位,同時你也可以使用任意多個引數來呼叫format()。但是替換欄位遠比這個強大。

跳過該程式碼清單

>>> import humansize >>> si_suffixes = humansize.SUFFIXES[1000] >>> si_suffixes ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] >>> '1000{0[0]} = 1{0[1]}'.format(si_suffixes) '1000KB = 1MB'
不需要呼叫humansize模組定義的任何函式我們就可以抓取到其所定義的資料結構:國際單位制(SI, 來自法語Système International)的字尾列表(以1000為進位制)。
這一句看上去有些複雜,其實不是這樣的。{0}代表傳遞給format()方法的第一個引數,即si_suffixes。注意si_suffixes是一個列表。所以{0[0]}指代si_suffixes的第一個元素,即'KB'。同時,{0[1]}指代該列表的第二個元素,即:'MB'。大括號以外的內容 — 包括1000,等號,還有空格等 — 則按原樣輸出。語句最後返回字串為'1000KB = 1MB'

{0}會被format()的第1個引數替換,{1}則被其第2個引數替換。

這個例子說明格式說明符可以通過利用(類似)Python的語法訪問到物件的元素或屬性。這就叫做複合欄位名(compound field names)。以下複合欄位名都是“有效的”。

  • 使用列表作為引數,並且通過下標索引來訪問其元素(跟上一例類似)
  • 使用字典作為引數,並且通過鍵來訪問其值
  • 使用模組作為引數,並且通過名字來訪問其變數及函式
  • 使用類的例項作為引數,並且通過名字來訪問其方法和屬性
  • 以上方法的任意組合

為了使你確信的確如此,下面這個樣例就組合使用了上面所有方法:

>>> import humansize
>>> import sys
>>> '1MB = 1000{0.modules[humansize].SUFFIXES[1000][0]}'.format(sys)
'1MB = 1000KB'

下面是描述它如何工作的:

  • sys模組儲存了當前正在執行的Python例項的資訊。由於已經匯入了這個模組,因此可以將其作為format()方法的引數。所以替換域{0}指代sys模組。
  • sys.modules is a dictionary of all the modules that have been imported in this Python instance. The keys are the module names as strings; the values are the module objects themselves. So the replacement field {0.modules} refers to the dictionary of imported modules. sys.modules是一個儲存當前Python例項中所有已經匯入模組的字典。模組的名字作為字典的鍵;模組自身則是鍵所對應的值。所以{0.modules}指代儲存當前己被匯入模組的字典。
  • sys.modules['humansize']即剛才匯入的humansize模組。所以替換域{0.modules[humansize]}指代humansize模組。請注意以上兩句在語法上輕微的不同。在實際的Python程式碼中,字典sys.modules的鍵是字串型別的;為了引用它們,我們需要在模組名周圍放上引號(比如 'humansize')。但是在使用替換域的時候,我們在省略了字典的鍵名周圍的引號(比如 humansize)。在此,我們引用PEP 3101:字串格式化高階用法,“解析鍵名的規則非常簡單。如果名字以數字開頭,則它被當作數字使用,其他情況則被認為是字串。”
  • sys.modules['humansize'].SUFFIXES是在humansize模組的開頭定義的一個字典物件。 {0.modules[humansize].SUFFIXES}即指向該字典。
  • sys.modules['humansize'].SUFFIXES[1000]是一個SI(國際單位制)字尾列表:['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']。所以替換域{0.modules[humansize].SUFFIXES[1000]}指向該列表。
  • sys.modules['humansize'].SUFFIXES[1000][0]SI字尾列表的第一個元素:'KB'。因此,整個替換域{0.modules[humansize].SUFFIXES[1000][0]}最後都被兩個字元KB替換。

格式說明符#

但是,還有一些問題我們沒有講到!再來看一看humansize.py中那一行奇怪的程式碼:

if size < multiple:
   
return '{0:.1f} {1}'.format(size, suffix)

{1}會被傳遞給format()方法的第二個引數替換,即suffix。但是{0:.1f}是什麼意思呢?它其實包含了兩方面的內容:{0}你已經能理解,:.1f則不一定了。第二部分(包括冒號及其後邊的部分)即格式說明符(format specifier),它進一步定義了被替換的變數應該如何被格式化。

格式說明符的允許你使用各種各種實用的方法來修飾被替換的文字,就像C語言中的printf()函式一樣。我們可以新增使用零填充(zero-padding),襯距(space-padding),對齊字串(align strings),控制10進位制數輸出精度,甚至將數字轉換成16進位制數輸出。

在替換域中,冒號(:)標示格式說明符的開始。“.1”的意思是四捨五入到保留一們小數點。“f”的意思是定點數(與指數標記法或者其他10進位制數表示方法相對應)。因此,如果給定size為698.24suffix'GB',那麼格式化後的字串將是'698.2 GB',因為698.24被四捨五入到一位小數表示,然後字尾'GB'再被追加到這個串最後。

>>> '{0:.1f} {1}'.format(698.24, 'GB')
'698.2 GB'

想了解格式說明符的複雜細節,請參閱Python官方文件關於格式化規範的迷你語言

其他常用字串方法#

除了格式化,關於字串還有許多其他實用的使用技巧。

跳過該程式碼清單

>>> s = '''Finished files are the re- ... sult of years of scientif- ... ic study combined with the ... experience of years.''' >>> s.splitlines() ['Finished files are the re-',
 
'sult of years of scientif-',
 
'ic study combined with the',
 
'experience of years.']
>>> print(s.lower()) finished files are the re- sult of years of scientif- ic study combined with the experience of years. >>> s.lower().count('f') 6
我們可以在Python的互動式shell裡輸入多行(multiline)字串。一旦我們以三個引號標記多行字串的開始,按ENTER鍵,Python shell會提示你繼續這個字串的輸入。連續輸入三個結束引號以終止該字串的輸入,再敲ENTER鍵則會執行該條命令(在當前例子中,把這個字串賦給變數s)。
splitlines()方法以多行字串作為輸入,返回一個由字串組成的列表,列表的元素即原來的單行字串。請注意,每行行末的回車符沒有被包括進去。
lower()方法把整個字串轉換成小寫的。(類似地,upper()方法執行大寫化轉換操作。)
count()方法對串中的指定的子串進行計數。是的,在那一句中確實出現了6個字母“f”。

還有一種經常會遇到的情況。比如有如下形式的鍵-值對列表 key1=value1&key2=value2,我們需要將其分離然後產生一個這樣形式的字典{key1: value1, key2: value2}

跳過該程式碼清單

>>> query = 'user=pilgrim&database=master&password=PapayaWhip' >>> a_list = query.split('&') >>> a_list ['user=pilgrim', 'database=master', 'password=PapayaWhip'] >>> a_list_of_lists = [v.split('=', 1) for v in a_list] >>> a_list_of_lists [['user', 'pilgrim'], ['database', 'master'], ['password', 'PapayaWhip']] >>> a_dict = dict(a_list_of_lists) >>> a_dict {'password': 'PapayaWhip', 'user': 'pilgrim', 'database': 'master'}
split()方法使用一個引數,即指定的分隔符,然後根據這個分隔符將串分離成一個字串列表。此處,分隔符即字元“&”,它還可以是其他的內容。
現在我們有了一個字串列表,其中的每個串由三部分組成:鍵,等號和值。我們可以使用列表解析來遍歷整個列表,然後利用第一個等號標記將每個字串再分離成兩個子串。(理論上,值也可以包含等號標記,如果執行'key=value=foo'.split('='),那麼我們會得到一個三元素列表['key', 'value', 'foo']。)
最後,通過呼叫dict()函式Python會把那個包含列表的列表(list-of-lists)轉換成字典物件。

上一個例子跟解析URL的請求引數(query parameters)很相似,但是真實的URL解析實際上比這個複雜得多。如果需要處理URL請求引數,我們最好使用urllib.parse.parse_qs()函式,它可以處理一些不常見的邊緣情況。

字串的分片#

定義一個字串以後,我們可以擷取其中的任意部分形成新串。這種操作被稱作字串的分片(slice)。字串分片跟列表的分片(slicing lists)原理是一樣的,從直觀上也說得通,因為字串本身就是一些字元序列。

跳過該程式碼清單

>>> a_string = 'My alphabet starts where your alphabet ends.' >>> a_string[3:11] 'alphabet' >>> a_string[3:-3] 'alphabet starts where your alphabet en' >>> a_string[0:2] 'My' >>> a_string[:18] 'My alphabet starts' >>> a_string[18:] ' where your alphabet ends.'
我們可以通過指定兩個索引值來獲得原字串的一個“slice”。該操作的返回值是一個新串,依次包含了從原串中第一個索引位置開始,直到但是不包含第二個索引位置之間的所有字元。
就像給列表做分片一樣,我們也可以使用負的索引值來分片字串。
字串的下標索引是從0開始的,所以a_string[0:2]會返回原字串的前兩個元素,從a_string[0]開始,直到但不包括a_string[2]
如果省略了第一個索引值,Python會預設它的值為0。所以a_string[:18]a_string[0:18]的效果是一樣的,因為從0開始是被Python預設的。
同樣地,如果第2個索引值是原字串的長度,那麼我們也可以省略它。所以,在此處a_string[18:]a_string[18:44]的結果是一樣的,因為這個串的剛好有44個字元。這種規則存在某種有趣的對稱性。在這個由44個字元組成的串中,a_string[:18]會返回前18個字元,而a_string[18:]則會返回除了前18個字元以外字串的剩餘部分。事實上a_string[:n]總是會返回串的前n個字元,而a_string[n:]則會返回其餘的部分,這與串的長度無關。

String vs. Bytes#

位元組即位元組;字元是一種抽象。一個不可變(immutable)的Unicode編碼的字元序列叫做string。一串由0到255之間的數字組成的序列叫做bytes物件。

跳過該程式碼清單

>>> by = b'abcd\x65' >>> by b'abcde' >>> type(by) <class 'bytes'> >>> len(by) 5 >>> by += b'\xff' >>> by b'abcde\xff' >>> len(by) 6 >>> by[0] 97 >>> by[0] = 102 Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'bytes' object does not support item assignment
使用“byte字面值”語法b''來定義bytes物件。byte字面值裡的每個位元組可以是ASCII字元或者是從\x00\xff編碼了的16進位制數。
bytes物件的型別是bytes
跟列表和字串一樣,我們可以通過內建函式len()來獲得bytes物件的長度。
使用+操作符可以連線bytes物件。操作的結果是一個新的bytes物件。
連線5個位元組的和1個位元組的bytes物件會返回一個6位元組的bytes物件。
一如列表和字串,可以使用下標記號來獲取bytes物件中的單個位元組。對字串做這種操作獲得的元素仍為字串,而對bytes物件做這種操作的返回值則為整數。確切地說,是0–255之間的整數。
bytes物件是不可變的;我們不可以給單個位元組賦上新值。如果需要改變某個位元組,可以組合使用字串的切片和連線操作(效果跟字串是一樣的),或者我們也可以將bytes物件轉換為bytearray物件。

跳過該程式碼清單

>>> by = b'abcd\x65' >>> barr = bytearray(by) >>> barr bytearray(b'abcde') >>> len(barr) 5 >>> barr[0] = 102 >>> barr bytearray(b'fbcde')
使用內建函式bytearray()來完成從bytes物件到可變的bytearray物件的轉換。
所有對bytes物件的操作也可以用在bytearray物件上。
有一點不同的就是,我們可以使用下標標記給bytearray物件的某個位元組賦值。並且,這個值必須是0–255之間的一個整數。

我們決不應該這樣混用bytes和strings。

跳過該程式碼清單

>>> by = b'd' >>> s = 'abcde' >>> by + s Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: can't concat bytes to str >>> s.count(by) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: Can't convert 'bytes' object to str implicitly >>> s.count(by.decode('ascii')) 1
不能連線bytes物件和字串。他們兩種不同的資料型別。
也不允許針對字串中bytes物件的出現次數進行計數,因為串裡面根本沒有bytes。字串是一系列的字元序列。也許你是想要先把這些位元組序列通過某種編碼方式進行解碼獲得字串,然後對該字串進行計數?可以,但是需要顯式地指明它。Python 3不會隱含地將bytes轉換成字串,或者進行相反的操作。
好巧啊…這一行程式碼剛好給我們演示了使用特定編碼方式將bytes物件轉換成字串後該串的出現次數。

所以,這就是字串與位元組陣列之間的聯絡了:bytes物件有一個decode()方法,它使用某種字元編碼作為引數,然後依照這種編碼方式將bytes物件轉換為字串,對應地,字串有一個encode()方法,它也使用某種字元編碼作為引數,然後依照它將串轉換為bytes物件。在上一個例子中,解碼的過程相對直觀一些 — 使用ASCII編碼將一個位元組序列轉換為字串。同樣的過程對其他的編碼方式依然有效 — 傳統的(非Unicode)編碼方式也可以,只要它們能夠編碼串中的所有字元。

跳過該程式碼清單

>>> a_string = '深入 Python' >>> len(a_string) 9 >>> by = a_string.encode('utf-8') >>> by b'\xe6\xb7\xb1\xe5\x85\xa5 Python' >>> len(by) 13 >>> by = a_string.encode('gb18030') >>> by b'\xc9\xee\xc8\xeb Python' >>> len(by) 11 >>> by = a_string.encode('big5') >>> by b'\xb2`\xa4J Python' >>> len(by) 11 >>> roundtrip = by.decode('big5') >>> roundtrip '深入 Python' >>> a_string == roundtrip True
a_string是一個字串。它有9個字元。
by是一個bytes物件。它有13個位元組。它是通過a_string使用UTF-8編碼而得到的一串位元組序列。
by還是一個bytes物件。它有11個位元組。它是通過a_string使用GB18030編碼而得到的一串位元組序列。
此時的by仍舊是一個bytes物件,由11個位元組組成。它又是一種完全不同的位元組序列,我們通過對a_string使用Big5編碼得到。
roundtrip是一個字串,共有9個字元。它是通過對by使用Big5解碼演算法得到的一個字元序列。並且,從執行結果可以看出,roundtripa_string是完全一樣的。

補充內容:Python原始碼的編碼方式#

Python 3會假定我們的原始碼 — .py檔案 — 使用的是UTF-8編碼方式。

Python 2裡,.py檔案預設的編碼方式為ASCII。Python 3的原始碼的預設編碼方式為UTF-8

如果想使用一種不同的編碼方式來儲存Python程式碼,我們可以在每個檔案的第一行放置編碼宣告(encoding declaration)。以下宣告定義.py檔案使用windows-1252編碼方式:

# -*- coding: windows-1252 -*-

從技術上說,字元編碼的過載宣告也可以放在第二行,如果第一行被類UNIX系統中的hash-bang命令佔用了。

#!/usr/bin/python3
# -*- coding: windows-1252 -*-

瞭解更多資訊,請參閱PEP 263: 指定Python原始碼的編碼方式

進一步閱讀#

關於Python中的Unicode:

關於Unicode本身:

關於其他的編碼方式:

關於字串及其格式化:

相關文章