[TOC]
在我們參與更高級別的設計模式之前,讓我們深入研究一下Python最常見的對象之一:字符串。我們會看到更多,包括在字符串中搜索模式和序列化數據以便存儲或傳輸。
</b>
特別是,我們將學習:
* 字符串、字節和字節數組的復雜性
* 字符串格式化的來龍去脈
* 序列化數據的幾種方法
* 神秘的正則表達式
## 字符串
字符串是Python中一個的基本主類型;到目前為止,我們已經討論的每個例子中,幾乎都使用了字符串。它們只是代表一個不可變的字符序列。然而,盡管你以前可能沒有考慮過,“字符”有點像歧義詞;Python字符串能代表不同國家字符序列嗎?漢字?希臘語、西里爾語或波斯語呢?
</b>
在Python 3中,答案是肯定的。Python字符串使用Unicode表示字符定義標準,Unicode幾乎可以代表地球上的任何語言(以及一些虛構的語言和隨機字符)。這在很大程度上是無縫完成的。所以,讓我們想想Python3字符串作為不可變的Unicode字符序列。那么我們能用這個不變序列做些什么呢?在前面的例子中,我們已經觸及了操作字符串的許多可能方式,但是讓我們在一個地方快速地學習它:字符串理論速成班!
### 字符串操作
如你所知,字符串可以在Python中通過單引號或雙引號包裹一串字符來創建。使用三個引號可以很容易地創建多行字符串。多個硬編碼字符串可以通過排排坐將它們連接起來。以下是一些例子:
```
a = "hello"
b = 'world'
c = '''a multiple
line string'''
d = """More
multiple"""
e = ("Three " "Strings "
"Together")
```
解釋器會自動將最后一個字符串組成一個字符串。也可以使用+運算符連接字符串(例如"hello " + "world")。當然,字符串不必硬編碼實現。它們也可以來自各種外部來源,如文本文件、用戶輸入或網絡編碼。
> 相鄰字符串的自動連接可能因為缺少逗號而出現一些滑稽錯誤。然而,當需要在函數調用中放置長字符串時,這是很有用的,特別是Python樣式指南中有單行不得超過79個字符的限制。
其他序列一樣,字符串可以迭代(逐個字符),索引,切片或連接。語法與列表相同。
</b>
`str`類上有許多方法,使得操作字符串更加容易。Python解釋器中的`dir`和`help`命令可以告訴我們如何使用這些方法;我們將直接考慮一些最常見的方法。
</b>
幾種布爾方法幫助我們識別字符是否匹配特定模式字符串。以下是這些方法的概述。其中最常見的,如`isalpha`、`isupper`、`islower`、`startwith`、`endwith
`,有顯而易見的意義。`isspace`方法也相當明顯,但是請記住它考慮所有空白字符(包括制表符、換行符),而不僅僅是空格字符。
</b>
如果每個單詞的第一個字符是大寫的,其他字符都是小寫,那么`istitle`方法將返回True。請注意,它并沒有嚴格執行標題格式的英語語法定義。例如,利·亨特的詩“The Glove and the Lions”應該是一個有效的標題,盡管不是所有的單詞都是大寫。羅伯特·服務的“The Cremation of Sam McGee”也應該是有效的標題,即使在最后一個單詞的中間有一個大寫字母。
</b>
小心`isdigit`、`isdecimal`和`isnumeric`方法,它們比你想象的更微妙。許多Unicode字符被認為是除了我們習慣的十位數之外的數字。更糟糕的是,我們用于從字符串構造浮點型數字的日期符號不被認為是一個小數點符號,所以`'45.2'
.isdecimal()`返回False。真正的小數點符號被表示為一個Unicode值0660,如45.2,(或45\u06602)。此外,這些方法不能驗證字符串是有效的數字;對于這三種方法,“127.0.0.1”返回`True`。我們可能認為我們應該用小數點符號而不是句點來表示所有數字,但是將該字符傳遞給`float()`或`int()`構造函數會將小數點符號轉換為零:
```
>>> float('45\u06602')
4502.0
```
對模式匹配有用的其他方法不返回布爾值。`count`方法告訴我們給定子字符串在字符串中出現的次數,而`find`、`index`、`rfind`和`rindex`告訴我們給定子字符串在原始字符串的位置。兩個“r”(代表“right”或“reverse”)方法從字符串的結尾開始搜索。如果找不到子字符串,`find`方法返回-1,而在這種情況下,`index`會拋出`ValueError`。看看這些方法的一些例子:
```
>>> s = "hello world"
>>> s.count('l')
3
>>> s.find('l')
2
>>> s.rindex('m')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: substring not found
```
剩余的大多數字符串方法主要用于字符串轉換。`upper`、 `lower` `capitalize`和`title
`方法使用所有字母創建一個新的給定格式的字符串。`translate`方法使用字典映射任意輸入字符到指定的輸出字符。
</b>
對于所有這些方法,注意輸入字符串保持不變;而是返回全新的字符串實例。如果我們需要操作結果字符串,我們應該將其分配給一個新變量,如`new _ value = value.capitalize()`。通常,一旦我們完成了轉換,我們就不再需要舊值了,所以一個常見的習慣用法是將其賦給同一個變量,如`value = value.title()`。
</b>
最后,一些字符串方法返回列表或對列表進行操作。`split`方法接受子字符串作為分隔標記,在子字符串出現的地方,將該字符串拆分為字符串列表。你可以傳遞一個數字作為第二參數來限制分割次數。如果不限制的數量,`rsplit`的行為與`split`是相同的,但是如果你提供了一個限制,它會從字符串的末尾開始拆分。`partition`和`rpartition`方法只在第一個或最后一個出現分隔字符串的位置拆分字符串,并返回三個值的元組:分隔字符串前面的字符串、分隔字符串本身以及分隔字符串后面的字符串。
</b>
與`split`相反,`join`方法接受一個字符串列表,將原始字符串放在它們之間,返回組合在一起的所有這些字符串。`replace`方法接受兩個參數,并返回一個字符串,其中第一個參數所在的每個實例都將被第二個參數所取代。這里有一些例子:
```
>>> s = "hello world, how are you"
>>> s2 = s.split(' ')
>>> s2
['hello', 'world,', 'how', 'are', 'you']
>>> '#'.join(s2)
'hello#world,#how#are#you'
>>> s.replace(' ', '**')
'hello**world,**how**are**you'
>>> s.partition(' ')
('hello', ' ', 'world, how are you')
```
給你,這就是`str`類最常見方法的旋風之旅!現在,讓我們來看看一些Python3的方法,它們用于組合字符串和變量,創建新字符串。
### 字符串格式化
Python 3有強大的字符串格式化和模板機制,允許我們構造由硬編碼文本和散置變量組成的字符串。我們已經在以前的許多例子中使用過它,但是它提供了比我們使用的簡單格式指定器更豐富的功能。
</b>
任何字符串都可以通過調用`format()`方法轉換成格式字符串。此方法返回一個新字符串,其中輸入字符串中的特定字符被替換為傳進方法中的參數值和鍵參數值。`format`方法不需要一組固定的參數;在內部,它使用了我們在第7章“Python面向對象快捷方式”中討論過的`*args`和`**kwargs`語法。
</b>
格式化字符串中有兩個替換的特殊字符,左右括號字符:{ }。我們可以在一個字符串中插入成對的左右括號,它們會按順序被替換成傳遞給`str.format`方法的位置參數。
```
template = "Hello {}, you are currently {}."
print(template.format('Dusty', 'writing'))
```
如果我們運行這些語句,它會用變量替換大括號,順序如下:
```
Hello Dusty, you are currently writing.
```
如果我們想在一個字符串中重用變量,或者決定在不同的位置使用它們,這種基本語法就不那么有用了。我們可以在花括號內放置零起始索引整數,告訴格式化程序在指定的字符串位置插入哪一個位置變量。讓我們在這個程序重復插入`name`:
```
template = "Hello {0}, you are {1}. Your name is {0}."
print(template.format('Dusty', 'writing'))
```
如果我們使用這些整數索引,我們必須在所有變量中使用它們。我們不能將空括號與位置索引混合使用。例如,下面的代碼將拋出某種`ValueError`異常:
```
template = "Hello {}, you are {}. Your name is {0}."
print(template.format('Dusty', 'writing'))
```
#### 轉義括號(譯注:第一版翻譯成“避免花括號”,第二版改過來了!)
除了格式之外,大括號字符在字符串中還有很多其他用途。我們需要一種方法,在我們希望他們表現為自己的情況下,不要被替代。這可以通過加兩層花括號來實現。例如,我們可以使用Python格式化一個基本的Java程序:
```
template = """
public class {0} {{
public static void main(String[] args) {{
System.out.println("{1}");
}}
}}"""
print(template.format("MyClass", "print('hello world')"));
```
只要我們在模板中看到{{ }}序列,即用括號包裝Java類和方法定義,我們知道`format`方法會使用單個大括號取代它們,而不是替換成傳遞給`format`方法的一些參數。輸出如下:
```
public class MyClass {
public static void main(String[] args) {
System.out.println("print('hello world')");
}
}
```
類名和輸出的內容已被兩個參數替換,而雙大括號被單大括號取代,這給了我們一個有效的Java文件。事實證明,這是打印最簡單Java程序、可以打印最簡單Python程序的Python程序!(說這么繞干嘛呢!)
#### 鍵參數
如果我們格式化復雜的字符串,記住順序可能會變得很乏味,或者如果我們插入新的參數,則需要更新模板。因此,`format`方法允許我們在大括號內指定名稱而不是數字。然后,命名變量作為鍵參數傳遞給`format`方法:
```
template = """
From: <{from_email}>
To: <{to_email}>
Subject: {subject}
{message}"""
print(template.format(
from_email = "a@example.com",
to_email = "b@example.com",
message = "Here's some mail for you. "
" Hope you enjoy the message!",
subject = "You have mail!"
))
```
我們還可以混合索引和關鍵字參數(就像所有的 Python 函數調用一樣,鍵參數必須跟在位置參數之后)。我們甚至可以混合鍵參數和未標記位置的空括號:
```
print("{} {label} {}".format("x", "y", label="z"))
```
正如預期的那樣,該代碼輸出:
```
x z y
```
#### 容器查找
我們不局限于將簡單的字符串變量傳遞給`format`方法。任何主數據類型,如整數或浮點數,都可以被打印。更有趣的是,復雜對象,包括列表、元組、字典和任意對象,我們可以在`format`字符串中訪問這些對象的索引和變量(但不能訪問方法)。
</b>
例如,如果我們的電子郵件已經將發件人和收件人電子郵件地址放入元組中,并出于某種原因將主題和消息放入字典中(也許因為這是我們想要使用的現有`send_mail`函數所需的輸入),我們可以這樣格式化它:
```
emails = ("a@example.com", "b@example.com")
message = {
'subject': "You Have Mail!",
'message': "Here's some mail for you!"
}
template = """
From: <{0[0]}>
To: <{0[1]}>
Subject: {message[subject]}
{message[message]}"""
print(template.format(emails, message=message))
```
模板字符串中括號內的變量看起來有點奇怪,讓我們看看它們在做什么。我們傳遞了一個位置參數和一個鍵參數。兩個電子郵件地址由`0[x]`查找,其中`x`是`0`或`1`。與其他基于位置的參數一樣,初始零表示傳遞給`format`的位置參數的第一個元素(即`email`元組的第一個元素)。
</b>
內部帶有數字的方括號與我們查找索引的方式相同,因此`0[0]`映射到`email`元組中的`emails[0]`。索引語法適用于任何可索引的對象,因此當我們訪問`message[subject]`時,我們看到類似的行為,除了這次我們查找的是字典中的字符串鍵。請注意,與Python代碼不同的是,我們不需要在字典查找中的字符串周圍加上引號。
</b>
如果我們有嵌套的數據結構,我們甚至可以進行多層查找。我建議不要經常這樣做,因為模板字符串很快變得很難讀懂。如果我們有一個包含元組的字典,我們可以這樣做:
```
emails = ("a@example.com", "b@example.com")
message = {
'emails': emails,
'subject': "You Have Mail!",
'message': "Here's some mail for you!"
}
template = """
From: <{0[emails][0]}>
To: <{0[emails][1]}>
Subject: {0[subject]}
{0[message]}"""
print(template.format(message))
```
#### 對象查找
索引使`format`查找功能強大,但是我們還沒有完成!我們也可以通過任意對象作為參數,并使用點符號查找這些對象的屬性
。讓我們再次更改我們的電子郵件信息數據,這次改為一個類:
```
class EMail:
def __init__(self, from_addr, to_addr, subject, message):
self.from_addr = from_addr
self.to_addr = to_addr
self.subject = subject
self.message = message
email = EMail("a@example.com", "b@example.com",
"You Have Mail!",
"Here's some mail for you!")
template = """
From: <{0.from_addr}>
To: <{0.to_addr}>
Subject: {0.subject}
{0.message}"""
print(template.format(email))
```
這個例子中的模板可能比前面的例子更可讀,但是創建電子郵件類的開銷增加了Python代碼的復雜性。為了展示目的將對象包含在模板是不明智的。通常,如果我們試圖格式化的對象已經存在,我們才這么做。所有的例子都是如此;如果我們有元組、列表或字典,我們將把它直接傳遞到模板中。否則,我們只需要創建一組簡單的包含位置參數和鍵參數的集合。
#### 至少看起來要正確
能夠在模板字符串中包含變量很好,但是有時需要一點強制來使它們在輸出中看起來正確。例如,如果我們用貨幣進行計算,我們可能會得到一個長十進制數,然而,我們不想它們出現在模板中:
```
subtotal = 12.32
tax = subtotal * 0.07
total = subtotal + tax
print("Sub: ${0} Tax: ${1} Total: ${total}".format(
subtotal, tax, total=total))
```
如果我們運行這個格式化代碼,輸出看起來不太像正確的貨幣:
```
Sub: $12.32 Tax: $0.8624 Total: $13.182400000000001
```
> 從技術上講,**我們永遠不應該在貨幣計算中使用浮點數字**;我們應該構造`decimal.Decimal()`對象。浮點數字是危險的,因為它們的計算存在固有的超出特定精度水平的不準確。但是我們希望的是字符串,而不是浮點數字,貨幣是格式化的一個很好的例子!
我們修改一下這個例子的格式字符串,我們可以在花括號內包含一些附加信息用于調整參數的格式。有很多我們可以定制的東西,但是大括號內的基本語法是一樣的;首先,我們仍然使用早期布局(位置參數、鍵參數、索引參數、屬性訪問),用于指定我們要放置在模板字符串中的變量。接下來我們加上冒號,然后是格式的特定語法。這里有一個改進版本:
```
print("Sub: ${0:0.2f} Tax: ${1:0.2f} "
"Total: ${total:0.2f}".format(
subtotal, tax, total=total))
```
冒號后的`0.2f`格式指定符基本上從左到右表示:對于小于1的值,確保小數點左側顯示零;小數點后顯示兩位;將輸入值格式化為浮點型數字。
</b>
我們還可以指定每個數字在屏幕上應該占用特定數量的字符,通過在精度中的句點前放置一個值。這對于輸出表格數據非常有用,例如:
```
orders = [('burger', 2, 5),
('fries', 3.5, 1),
('cola', 1.75, 3)]
print("PRODUCT QUANTITY PRICE SUBTOTAL")
for product, price, quantity in orders:
#留意這里的用法!
subtotal = price * quantity
print("{0:10s}{1: ^9d} ${2: <8.2f}${3: >7.2f}".format(
product, quantity, price, subtotal))
```
好吧,這是一個看起來很可怕的格式字符串,讓我們看看它是如何工作的。我們把它分成可以理解的部分:
```
PRODUCT QUANTITY PRICE SUBTOTAL
burger 5 $2.00 $ 10.00
fries 1 $3.50 $ 3.50
cola 3 $1.75 $ 5.25
```
漂亮!那么,這到底是怎么發生的呢?我們正在`for`循環的每一行中格式化四個變量。第一個變量是字符串,格式為`{0:10s}`。`s`表示它是一個字符串變量,10表示它應該占用10個字符。默認情況下,對于字符串,如果字符串短于指定的數字對于字符,它會在字符串的右側添加空格,使其足夠長(但是,要小心:如果原始字符串太長,它不會被截斷!)。我們可以更改此行為(使用其他字符填充或更改格式字符串的對齊方式),正如我們對下一個變量`quantity`所做的那樣。
</b>
`quantity`的格式化形式是`{1: ^9d}`。`d`代表整數值。9告訴我們該值應該包含9個字符。但是對于整數而言,默認情況下,空格中的多余字符為零。這看起來有點奇怪。因此我們明確指定一個空格(緊接在冒號后面)作為填充字符。插入字符`^`告訴我們,數字應該在中間對齊;這使得列欄看起來更專業了。格式化形式必須按正確的順序排列,盡管所有都是可選的:先寫填充字符、然后對齊格式,然后是對齊尺寸,最后是類型。
</b>
我們對價格和小計的格式化做類似的事情。對于價格,我們使用{2: <8.2f},對于小計,我們使用{3: >7.2f}。在這兩種情況下,我們都指定了一個空格作為填充字符,但是我們分別使用<和>符號,分別表示這些數字應該在最小的七個或八個字符串內向左或向右對齊。此外,每個浮點數應該被格式化為兩位小數。
</b>
不同類型的“類型”字符也會影響格式輸出。我們已經看到`s`、`d`和`f`類型,分別代表字符串、整數和浮點數。大多數其他格式類型都是這些類型的替代版本;例如,o代表整數的八進制格式,X代表整數的十六進制。n可作為以當前區域設置的格式化整數分隔符。對于浮點數字,%類型將乘以100,并將浮點數格式化為百分比。
</b>
雖然這些標準格式化程序適用于大多數內置對象,但是定義其他對象的非標準格式化形式也是可以的。例如,如果我們傳遞一個`datetime`對象到`format`,我們可以在`datetime.strftime`函數中使用格式化形式,如下所示:
```
import datetime
print("{0:%Y-%m-%d %I:%M%p }".format(
datetime.datetime.now()))
```
甚至可以為我們自己創建的對象編寫自定義格式化程序,但是那超出了這本書的范圍。如果你需要在代碼中這樣做,查看如何覆蓋`__format__
`方法。最全面的說明可以在`http://www.python.org/dev/peps/pep-3101/`的PEP 3101中找到,細節有點枯燥。你可以通過網絡搜索找到更容易理解的教程。
</b>
Python格式化語法非常靈活,但很難記住。我每天都使用它,但偶爾還是要查找忘掉的一些概念。它還不足以滿足日益復雜的模板需求,例如生成網頁。有幾個第三方模板庫可以幫助你對一些字符串進行基本格式化以外的操作。
### 字符串是Unicode
在本節的開頭,我們將字符串定義為不可變的Unicode字符集合。有時這會使事情變得非常復雜,因為Unicode并不是真正的存儲格式。如果我們從文件或socket獲得字節字符串,它們都不是Unicode。事實上,它們是內置類型字節`bytes`。字節是不可變的序列...字節。在計算中,字節是最低級別的存儲格式。它們代表8位,通常被描述為介于0和255之間的整數,或介于0和FF之間十六進制等效值。字節不會代表任何特定的事物;字節序列可以存儲編碼字符串的字節或圖像中的像素。
</b>
如果我們打印一個字節對象,任何字節被映射到ASCII,打印出它們的原始字符,而非ASCII字節(無論它們是二進制數據還是其他字符)則被打印為十六進制代碼,由`\x`轉義序列轉義。你們可能覺著表示整數的字節能夠映射到ASCII字符有點奇怪。但是ASCII實際上只是一種代碼,其中每個字母有不同的字節模式,對應不同的整數。例如,字符“a”與整數97有相同的字節,整數97對應的十六進制數是0x61。具體來說,所有這些都是對二進制模式01100001的解釋。
</b>
許多輸入輸出操作只知道如何處理字節,即使字節對象指向的是文本數據。因此,知道如何在字節和Unicode之間轉換是至關重要的。
</b>
問題是有很多方法可以將字節映射到Unicode文本。字節是機器可讀的值,而文本是人類可讀的格式。這些方法承擔將給定字節序列映射到給定文本序列的編碼角色。
</b>
然而,有多種這樣的編碼方法(ASCII只是其中之一)。相同的字節序列,如果使用不同的編碼方法,將代表完全不同的文本字符!因此,`bytes`字節必須使用和在編碼中使用的字符集合進行解碼。在不知道字節應該如何解碼的情況下,從字節中獲取文本是不可能的。如果我們收到沒有指定編碼規則的未知字節,我們能做的最好的就是猜測它們是以什么格式編碼的,通常我們都是錯了。
#### 將字節碼轉換為文本
如果我們有來自某處的字節`bytes`數組,我們可以使用`bytes`類上的`.decode`方法,將字節轉換為Unicode。此方法接受一個字符串參數,該參數指字符編碼名稱。有許多這樣的名稱;西方語言常見的編碼規則包括ASCII、UTF-8和latin-1。
</b>
字節序列(十六進制)63 6c 69 63 68 e9實際上代表latin-1編碼中的陳詞濫調一詞的字符。以下示例將對這個詞編碼成字節序列,然后使用latin-1將其轉換為Unicode字符串:
```
characters = b'\x63\x6c\x69\x63\x68\xe9'
print(characters)
print(characters.decode("latin-1"))
```
第一行創建一個`bytes`對象;緊接在字符串前面的b字符告訴我們,我們正在定義一個`bytes`對象,而不是普通的Unicode字符串。在字符串中,每個字節都是用十六進制數指定。字節串中的\x字符表示轉義,并表示,“接下來的兩個字符將用十六進制數字表示一個字節。”
</b>
假設我們使用一個能夠理解`latin-1`編碼的shell編輯器,兩個`print`調用將輸出以下字符串:
```
b'clich\xe9'
cliché
```
第一個`print`語句將字節用ASCII字符渲染出來。對于未知(對于ASCII未知)字符保持其轉義十六進制格式。輸出在行首包含一個`b`字符,以提醒我們它是一個字節`bytes`表示,而不是字符串。
</b>
下一個調用`latin-1`編碼字符串。解碼`decode`方法返回具有正確字符的普通(Unicode)字符串。然而,如果我們使用西里爾文“iso8859-5”編碼同樣的字節序列,我們最終會得到字符串'clichщ'!這是因為\xe9字節映射到兩種編碼器會得到不同的字符。
#### 將文本轉換為字節碼
如果我們需要將傳入的字節轉換成Unicode,顯然我們也將擁有將Unicode轉換成字節序列的情況。這可以使用`str`類上的`encode`方法實現,與`decode`方法一樣,需要一個編碼字符集。下面的代碼創建一個Unicode字符串,并用不同的字符集進行編碼:
```
characters = "cliché"
print(characters.encode("UTF-8"))
print(characters.encode("latin-1"))
print(characters.encode("CP437"))
print(characters.encode("ascii"))
```
前三種編碼為重音字符創建了不同的字節集。第四個甚至不能將它處理成字節:
```
b'clich\xc3\xa9'
b'clich\xe9'
b'clich\x82'
Traceback (most recent call last):
File "1261_10_16_decode_unicode.py", line 5, in <module>
print(characters.encode("ascii"))
UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in
position 5: ordinal not in range(128)
```
你現在明白編碼的重要性了嗎?對于每個編碼器,重音符號將表示為不同字節;當我們把字節解碼成文本時,如果我們用錯了編碼器,我們將得到錯誤的字符。
</b>
最后一種情況下的例外并不總是我們期望的行為;可能會有一些情況,我們希望未知字符以不同的方式處理。編碼器`encode`方法接受一個名為`errors`的可選字符串參數,該參數可以定義這種情況應該如何處理字符。該字符串參數可以是以下之一:
* strict
* replace
* ignore
* xmlcharrefreplace
`strict`替換策略是我們剛剛看到的默認策略。當字節序列遇到在請求編碼中沒有有效的表示時,將引發一個異常。當使用`replace`策略時,字符被替換成不同的字節;在ASCII中,這是一個問號;其他編碼器可以使用不同的符號,例如一個空盒子。`ignore`策略將放棄任何它不理解的字節,而`xmlcharrefreplace`策略創建一個表示Unicode字符的xml實體。這在轉換XML文檔未知字符串時非常有用。以下展示每種策略是如何影響我們的示例詞:

無需傳遞編碼字符串就可以調用`str.encode`和`bytes.decode`方法。編碼器將被設置為當前平臺的默認編碼器。這將取決于當前的操作系統和地區或地區設置;你可以使用`sys.getdefaultencoding()`函數查找它。顯式指定編碼通常是個好主意,因為默認情況下,平臺的編碼器可能會改變,或者程序可能有一天會擴展到處理來自更廣泛來源的文本。
</b>
如果你正在編碼文本,但不知道使用哪種編碼器,最好使用`UTF-8`編碼器。`UTF-8`能夠代表任何`Unicode`字符。在現代軟件,它是一種事實上的標準編碼器,以確保任何語言的文檔——或者甚至多種語言的文檔——也可以交換。各種其他可能的編碼器對于遺留文檔或仍然使用默認不同字符集的區域非常有用。`UTF-8`編碼器使用一個字節來表示ASCII和其他常見字符,對于更復雜的字符,最多4個字節。`UTF-8`是特別的,因為它向后兼容ASCII碼;任何使用`UTF-8`編碼的ASCII文件將與原始的ASCII文檔相同。
</b>
我永遠不記得應該使用編碼還是解碼將二進制字節轉換為Unicode。我一直希望這些方法被命名為“to_binary”和“from_binary”。如果你有同樣的問題,試著用“二進制”代替“代碼”這個詞;“enbinary”和“debinary”非常接近“to_binary”和“from_binary”。自從設計出這個方法以來,我節省了很多查看方法幫助文檔的時間。(譯注:`encode`是編碼、`decode`是解碼,并不是很難理解的)
### 可變字節字符串
字節類型,像字符串一樣,是不可變的。我們可以在字節對象上使用索引和切片語法,并搜索特定的字節序列,但是我們不能擴展或修改它們。這在處理輸入/輸出時可能非常不方便,因為經常需要緩存輸入或輸出字節,直到它們準備好發送。例如,如果我們從一個socket接收數據,在收到完整信息之前我們可能需要多次`recv`。
</b>
這就是內置`bytearray`。這種類型類似列表,只不過它只保存字節。這個類的構造函數可以接受一個字節對象,并初始化它。擴展方法可以用來附加另一個字節對象添加到現有數組(例如,當更多來自一個socket的數據,或從其他輸入/輸出通道傳來的數據)。
</b>
切片符號可以在`bytearray`上用于修改內部項目。例如,這段代碼從`bytes`對象構造一個`bytearray`數組,然后替換兩個字節:
```
b = bytearray(b"abcdefgh")
b[4:6] = b"\x15\xa3"
print(b)
```
輸出如下所示:
```
bytearray(b'abcd\x15\xa3gh')
```
小心點。如果我們想操縱`bytearray`中的一個元素,它會希望我們傳遞一個0到255之間的整數作為值。這個整數表示特定的字節模式。如果我們試圖傳遞一個字符或字節對象,這將引發一個異常。
</b>
單字節字符可以使用`ord`(序數的縮寫)函數轉換成整數。此函數返回單個字符的整數表示:
```
b = bytearray(b'abcdef')
b[3] = ord(b'g')
b[4] = 68
print(b)
```
輸出如下所示:
```
bytearray(b'abcgDf')
```
構造數組后,我們用字節103替換索引3處的字符(是第四個字符,因為索引從0開始,與列表一樣)。該整數通過`ord`函數返回,是小寫字母g的ASCII字符。為了說明,我們還
用映射到ASCII碼的字節68替換下一個字符,對應大寫字母D的ASCII字符。
</b>
`bytearray`類型的方法允許它像列表一樣工作(我們可以給它追加整數字節),但也像一個字節對象;我們可以使用方法,比如`count`和`find`,和在字節或str對象上的方法是一樣的。不同之處在于`bytearray`是一個可變類型,對于從特定的輸入源建立復雜的字節序列,這會很有用。
## 正則表達式
你知道使用面向對象的原則最難做的是什么嗎?那就是,解析字符串匹配任意模式。有相當多的用面向對象設計來建立字符串解析的學術論文,但是結果總是非常冗長且難以閱讀,實際上并沒有被廣泛使用。
</b>
在現實世界中,大多數編程語言中的字符串解析由正則表達式處理。這些并不冗長,但是,孩子,它們很難讀懂,至少在你學會語法之前。即使正則表達式不是面向對象的,Python正則表達式庫提供了一些類和對象,可以用來構造和運行正則表達式。
</b>
正則表達式用于解決一個常見問題:給定一個字符串,確定該字符串是否匹配給定模式,以及可選地收集包含相關信息的子字符串。它們可以用來回答以下問題:
* 該字符串是有效的網址嗎?
* 日志文件中所有警告消息的日期和時間是什么?
* 給定組中有哪些/etc/passwd用戶?
* 訪問者鍵入的網址要求什么用戶名和文檔?
有許多類似的情況,正則表達式是正確的答案。許多程序員在部署復雜和脆弱的字符串解析庫犯了很多錯誤,因為他們不知道或者沒有去學習正則表達式。在本節中,我們將獲得足夠的正則表達式知識,來避免犯錯誤。
### 模式匹配
正則表達式是一種復雜的微型語言。他們依靠特殊字符來匹配未知字符串,但是讓我們從文字字符開始,例如字母、數字和空格字符,它們總是匹配的。讓我們看一個基本示例:
```
import re
search_string = "hello world"
pattern = "hello world"
match = re.match(pattern, search_string)
if match:
print("regex matches")
```
正則表達式的Python標準庫模塊稱為`re`。我們導入它并設置一個搜索字符串和模式進行搜索;在這個例子中,它們是相同的字符串。由于搜索字符串匹配給定模式,條件匹配(譯注:就是`if match`),`print`語句被執行。
</b>
請記住,匹配函數將從字符串的開始位置進行模式匹配。因此,如果模式是“ello world”,就找不到匹配的結果。由于不對稱,解析器一找到匹配就停止搜索,所以模式“hello wo”匹配成功。讓我們構建一個小示例程序來演示這些差異,并幫助我們學習其他正則表達式語法:
```
import sys
import re
pattern = sys.argv[1]
search_string = sys.argv[2]
match = re.match(pattern, search_string)
if match:
template = "'{}' matches pattern '{}'"
else:
template = "'{}' does not match pattern '{}'"
print(template.format(search_string, pattern))
```
這只是接受模式、從命令行搜索字符串的早期示例的一般版本。我們可以看到模式是如何從頭開始必須匹配的,但一旦在命令行交互中找到匹配,就會返回一個值:
```
$ python regex_generic.py "hello worl" "hello world"
'hello world' matches pattern 'hello worl'
$ python regex_generic.py "ello world" "hello world"
'hello world' does not match pattern 'ello world'
```
我們將在接下來的幾節中使用這個腳本。雖然腳本總是用命令行`python regex _ generic.py "<pattern>" "<string>"`調用,為了節省空間,我們將只在以下示例中看到輸出。
</b>
如果你需要控制匹配發生在一行的開頭還是結尾(或者字符串中沒有換行符,在字符串的開頭和結尾),你可以使用分別表示字符串開頭和結尾的`^`字符和`$`字符。如果你想要模式匹配整個字符串,最好將這兩者都包括在內:
```
'hello world' matches pattern '^hello world$'
'hello worl' does not match pattern '^hello world$'
```
#### 匹配特定字符
讓我們從匹配任意字符開始。句點字符,當用于正則表達式模式,可以匹配任何單個字符。在字符串中使用句點,意味著你不在乎字符是什么,只在乎那里有一個字符。例如:
```
'hello world' matches pattern 'hel.o world'
'helpo world' matches pattern 'hel.o world'
'hel o world' matches pattern 'hel.o world'
'helo world' does not match pattern 'hel.o world'
```
注意最后一個示例是如何不匹配的,因為在模式中句點的位置并沒有一個字符。
</b>
這一切都很好,但是如果我們只想匹配幾個特定的字符呢?我們可以將一組字符放在方括號內,以匹配其中任何一個字符。因此,如果我們在正則表達式模式中遇到字符串`[abc]`,我們要知道那些五(包括兩個方括號)字符只會匹配正在搜索的字符串中的一個字符,此外,這一個字符要么是一個a、一個b或一個c。請參見幾個示例:
```
'hello world' matches pattern 'hel[lp]o world'
'helpo world' matches pattern 'hel[lp]o world'
'helPo world' does not match pattern 'hel[lp]o world'
```
這些方括號應該被命名為字符集,但它們更常見被稱為字符類。通常,我們希望包含大量字符在這些集合中,把它們全部打印出來可能是單調的,容易出錯的。幸運的是,正則表達式設計者想到了這一點,給了我們一條捷徑。字符集中的破折號將創建一個范圍。這在你希望匹配“所有小寫字母”、“所有字母”或“所有數字”,尤其有用,如下所示:
```
'hello world' does not match pattern 'hello [a-z] world'
'hello b world' matches pattern 'hello [a-z] world'
'hello B world' matches pattern 'hello [a-zA-Z] world'
'hello 2 world' matches pattern 'hello [a-zA-Z0-9] world'
```
還有其他方法可以匹配或排除單個字符,但是你需要如果你想了解更多,可以通過網絡搜索找到更全面的教程!
#### 轉義字符
如果模式中的句點字符與任意字符匹配,那我們如何匹配字符串中的一個句點呢?一種方法可能是把句點放在方括號里面來創建一個字符類,但是更通用的方法是使用反斜杠來轉義它。這里有一個匹配介于0.00和0.99之間的數字、兩位小數的正則表達式:
```
'0.05' matches pattern '0\.[0-9][0-9]'
'005' does not match pattern '0\.[0-9][0-9]'
'0,05' does not match pattern '0\.[0-9][0-9]'
```
對于這種模式,兩個字符`\.`匹配一個句點字符。如果沒有這個句點字符或是不同的字符,則不匹配。
</b>
這個反斜杠轉義序列用于正則表達式中的各種特殊字符。你可以使用`\[`插入方括號,而不需要開始一個字符類,用`\(`插入一個括號,我們稍后將看到它也是一個特殊字符。
</b>
更有趣的是,我們還可以使用轉義符后跟一個字符來表示特殊字符,如換行符(\n)和制表符(\t)。此外,使用轉義字符串可以更簡潔地表示字符類;`\s`代表空白字符,`\w`代表字母、數字和下劃線,和`\d`代表一個數字:
```
'(abc]' matches pattern '\(abc\]'
' 1a' matches pattern '\s\d\w'
'\t5n' does not match pattern '\s\d\w'
'5n' matches pattern '\s\d\w'
#譯注:\s表示大于等于0個字符?
```
#### 匹配多個字符
有了這些信息,我們可以匹配大多數已知長度的字符串,但是大多數時候我們不知道在一個模式中需要匹配多少個字符。正則表達式能解決這個問題。我們可以修改模式,添加某個難以記憶的標點符號來匹配多個字符。
</b>
星號(*)表示先前的模式可以匹配零個或多個字符。這可能聽起來很傻,但它是最有用的重復字符之一。在我們探究原因之前,考慮一些愚蠢的例子來確保我們理解它的作用:
```
'hello' matches pattern 'hel*o'
'heo' matches pattern 'hel*o'
'helllllo' matches pattern 'hel*o'
```
因此,模式中的*字符表示前一個模式(l字符)是可選的,如果存在,可以盡可能重復多次,仍然可以匹配模式。其余字符(h、e和o)必須恰好出現一次。
</b>
想要多次匹配一個字母是非常罕見的,但是如果我們把星號和匹配多個字符的模式結合起來,它會變得很有趣(`..*`)。例如,將匹配任何字符串,而`[a-z]*`匹配任何小寫單詞集合,包括空字符串。例如:
```
'A string.' matches pattern '[A-Z][a-z]* [a-z]*\.'
'No .' matches pattern '[A-Z][a-z]* [a-z]*\.'
'' matches pattern '[a-z]*.*'
```
模式中的加號(+)的行為類似于星號;它指出先前的模式可以重復一次或多次,但是,與星號不同的是,加號不是可選的(譯注:怎么都得有一個字符)。問號(?)確保模式恰好顯示零或一次,但不是更多次。讓我們通過玩數字來探索其中的一些(請記住\d與[0-9]匹配相同的字符類別:
```
'0.4' matches pattern '\d+\.\d+'
'1.002' matches pattern '\d+\.\d+'
'1.' does not match pattern '\d+\.\d+'
'1%' matches pattern '\d?\d%'
'99%' matches pattern '\d?\d%'
'999%' does not match pattern '\d?\d%'
```
#### 模式組合
到目前為止,我們已經看到了如何多次重復一個模式,但是我們在我們可以重復什么樣的模式上受到了限制。如果我們想重復單個字符,這個我們已經討論了,但是如果我們想要一個重復的字符序列呢?用括號封閉任何一組模式,允許它們被視為應用重復操作時的單個模式。比較這些模式:
```
'abccc' matches pattern 'abc{3}'
'abccc' does not match pattern '(abc){3}'
'abcabcabc' matches pattern '(abc){3}'
```
結合復雜的模式,這個分組特性極大地擴展了我們的模式匹配范圍。這里有一個匹配簡單英語句子的正則表達式:
```
'Eat.' matches pattern '[A-Z][a-z]*([a-z]+)*\.$'
'Eat more good food.' matches pattern '[A-Z][a-z]*([a-z]+)*\.$'
'A good meal.' matches pattern '[A-Z][a-z]*([a-z]+)*\.$'
```
第一個單詞以大寫字母開頭,后跟零個或更多小寫字母。然后,我們輸入一個括號,匹配一個空格,后跟一個或多個小寫字母組成的單詞。整個括號內容重復零個或更多,模式以句點結束。在句點后不可能有其他的字符,正如匹配字符串結尾的$所示。
</b>
我們已經看到了許多最基本的模式,但是正則表達式語言還可以支持更多。每當我需要做某事的時候,我頭幾年用正則表達式查找語法。為Python的`re`模塊的文檔做書簽并經常查看是值得的。很少有正則表達式無法匹配的東西,它們應該是你解析字符串的第一個工具。
### 從正則表達式獲得信息
現在讓我們關注事物Python的一面。正則表達式語法是離面向對象編程最遠的東西。然而,Python `re`模塊提供一個面向對象的接口來進入正則表達式引擎。
</b>
我們一直在檢查`re.match`函數是否返回有效的對象。如果模式不匹配,該函數將返回`None`。然而,如果匹配,它將返回了一個有用的對象,我們可以通過它來反省模式的信息。
</b>
到目前為止,我們的正則表達式已經回答了諸如“這個字符串符合這個模式嗎?”,匹配模式很有用,但在許多情況下,更有有趣的問題是,“如果這個字符串匹配這個模式,那么
相關子字符串是什么?”,如果你使用組來識別你想稍后引用的模式部分,你可以將它們從匹配返回值中取出,如中下面這個例子:
```
pattern = "^[a-zA-Z.]+@([a-z.]*\.[a-z]+)$"
search_string = "some.user@example.com"
match = re.match(pattern, search_string)
if match:
domain = match.groups()[0]
print(domain)
```
描述有效電子郵件地址的規范極其復雜,精確匹配所有可能性的正則表達式非常長。所以我們簡化這個事情,做了一個簡單的正則表達式來匹配一些常見的電子郵件地址;關鍵是我們想要訪問域名(在@符號之后),這樣我們可以連接到那個地址。這很容易通過將模式的這一部分包裹在括號中,并調用`groups()`方法,返回匹配的對象。
</b>
`groups`方法返回一個匹配模式的所有組的元組,你可以對其進行索引以訪問特定的值。各組按從左到右的順序排列。但是,請記住,組可以嵌套,這意味著你可以有一個或更多組在另一個組中。在這種情況下,組按照最左邊括號排序,所以最外面的組比內部匹配組先返回。
</b>
除了匹配函數,`re`模塊還提供了其他一些有用的函數,`search`和`findall`。`search`函數尋找匹配模式的第一個實例,放寬從匹配第一個開始字母的限制。請注意,你可以通過使用`match`和在模式的開頭放置`^.*`獲得類似的效果,`^.*`匹配字符串開頭和你所尋找的模式之間任意的字符。
</b>
`findall`函數的行為類似于`search`,除了它匹配模式的所有非重疊實例,而不僅僅是第一個實例。基本上,它找到第一個匹配,然后將搜索重置到匹配字符串的末尾,并尋找下一個。
</b>
它不會像你所期望的那樣返回匹配對象的列表,而是返回匹配字符串列表,或者元組。有時是字符串,有時是元組。這不是非常好的應用編程接口!和所有糟糕的API一樣,你必須記住它們的區別,而不是不依賴直覺。返回值的類型取決于正則表達式中方括號內的組的數量:
* 如果模式中沒有組,`re.findall`將返回一個字符串列表
,其中每個值都是來自符合模式的、源字符串的完整子字符串
* 如果模式中只有一個組,`re.findall`將返回一個字符串列表
,其中每個值都是該組的內容
* 如果模式中有多個組,那么`re.findall`將返回一個元組列表
,其中每個元組包含按順序排列的、來自匹配組的值
> 當您在自己的Python庫中設計函數調用時,嘗試使函數始終返回一致的數據結構。它通常很適合設計可以接受任意輸入并處理它們的函數,但是返回值不應該從單值切換到列表中,或者從值列表切換到元組列表中,這具體取決于輸入。讓`re.findall`成為一個教訓吧!
以下交互式會話中的示例將有望澄清差異:
```
>>> import re
>>> re.findall('a.', 'abacadefagah')
['ab', 'ac', 'ad', 'ag', 'ah']
>>> re.findall('a(.)', 'abacadefagah')
['b', 'c', 'd', 'g', 'h']
>>> re.findall('(a)(.)', 'abacadefagah')
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('a', 'g'), ('a', 'h')]
>>> re.findall('((a)(.))', 'abacadefagah')
[('ab', 'a', 'b'), ('ac', 'a', 'c'), ('ad', 'a', 'd'), ('ag', 'a', 'g'),
('ah', 'a', 'h')]
```
#### 讓重復的正則表達式更有效率
無論何時調用正則表達式方法之一,引擎都必須將模式字符串轉換為內部結構,以便更快的搜索字符串。這種轉換需要相當長的時間。如果正則表達式模式被重用多次(例如,在for或while循環中),一次完成這一轉換步驟會更好。
</b>
這可以通過`re.compile`方法實現。它返回一個已經編譯好的正則表達式的面向對象的版本,并且有我們已經探索過一些方法`match`,`search`,`findall`等等。我們將在案例研究中看到這些例子。
</b>
這無疑是對正則表達式的簡明介紹。重點是,我們對基礎有很好的感覺,并且會意識到什么時候需要做進一步的研究。如果我們有字符串模式匹配問題,正則表達式幾乎肯定能為我們解決這些問題。然而,我們可能需要尋找新的語法,以便更全面地涵蓋了這個主題。但是現在我們知道了我們要尋找什么!讓我們進入一個完全不同的主題:存儲序列化數據。
## 序列化對象
現在,我們能夠將數據寫入文件,并在以后任意時候檢索它
理所當然的日期。盡管如此方便(想象一下如果我們做不到,計算的狀態
儲存任何東西!),我們經常發現自己正在轉換存儲在nice中的數據
對象或設計模式轉換成某種笨拙的文本或二進制格式
用于存儲、網絡傳輸或遠程服務器上的遠程調用。
Python pickle模塊是一種面向對象的方法,可以將對象直接存儲在
特殊存儲格式。它本質上轉換一個對象(以及它所擁有的所有對象
作為屬性)轉換成可以存儲或傳輸的字節序列
我們看到了。
對于基本工作,pickle模塊有一個非常簡單的接口。它包括
存儲和加載數據的四個基本功能;兩個用于操縱類文件
對象,兩個用于操作字節對象(后者只是
類似文件的接口,所以我們不必自己創建類似字節文件的對象)。
轉儲方法接受要寫入的對象和要寫入的類文件對象
序列化字節到。這個對象必須有一個寫方法(否則它不會像文件一樣),
該方法必須知道如何處理字節參數(因此為
文本輸出不起作用)。
加載方法正好相反;它從類文件中讀取序列化對象
對象。該對象必須具有適當的類似文件的讀取和讀取行參數,每個參數
其中當然必須返回字節。泡菜模塊將從
這些字節和load方法將返回完全重建的對象。這里有一個
在列表對象中存儲并加載一些數據的示例:
```
import pickle
some_data = ["a list", "containing", 5,
"values including another list",
["inner", "list"]]
with open("pickled_list", 'wb') as file:
pickle.dump(some_data, file)
with open("pickled_list", 'rb') as file:
loaded_data = pickle.load(file)
print(loaded_data)
assert loaded_data == some_data
```
這段代碼的工作原理和廣告一樣:對象存儲在文件中,然后加載
來自同一個文件。在每種情況下,我們都使用with語句打開文件
自動關閉。文件首先被打開用于寫入,然后第二次被打開用于
讀取,這取決于我們是存儲還是加載數據。
如果新加載的對象是
不等于原始對象。平等并不意味著它們是同一個對象。
事實上,如果我們打印兩個對象的id(),我們會發現它們是不同的。
但是,因為它們都是內容相等的列表,所以這兩個列表也是
被認為是平等的。
轉儲和加載函數的行為很像它們類似文件的對應函數,除了
它們返回或接受字節,而不是類似文件的對象。轉儲功能需要
只有一個參數,即要存儲的對象,它返回序列化的bytes對象。
loads函數需要一個bytes對象并返回恢復的對象。' s '
方法名稱中的字符是字符串的縮寫;這是古代遺留下來的名字
Python版本,其中使用字符串對象而不是字節。
兩種轉儲方法都接受可選協議參數。如果我們在存錢
加載只在Python 3程序中使用的腌制對象,我們不
需要提供這個論點。不幸的是,如果我們存儲的對象可能
由舊版本的Python加載,我們不得不使用一個更舊且效率更低的協議。
這通常不應該是一個問題。通常,唯一能加載
腌過的東西和存放它的一樣。泡菜是一種不安全的形式,所以我們
我不想不安全地通過互聯網發送給不知名的翻譯。
提供的參數是整數版本號。默認版本是數字
3,代表當前Python 3酸洗所使用的高效存儲系統。
數字2是較舊的版本,它將存儲一個可以加載到所有
解釋器回到Python 2.3。因為2.6是仍然廣泛使用的最古老的Python
在野外,版本2酸洗通常是足夠的。支持版本0和1
老年口譯員;0是ASCII格式,1是二進制格式。還有
可能有一天會成為默認版本的優化版本4。
根據經驗,如果你知道你腌制的東西
由Python 3程序加載(例如,只有您的程序會加載它們),
使用默認酸洗方案。如果它們可能被未知的解釋程序加載,請通過
協議值為2,除非您真的認為它們可能需要加載
蟒蛇的古老版本。
如果我們確實通過了一個或多個轉儲協議,我們應該使用關鍵字參數來
指定它:pickle.dumps(my_object,protocol=2)。這不是絕對必要的,
因為該方法只接受兩個參數,但是鍵入完整的關鍵字
參數提醒讀者我們的代碼數字的目的是什么。擁有
方法調用中的隨機整數很難讀取。兩個什么?商店二
可能是物體的復制品?記住,代碼應該總是可讀的。在蟒蛇身上,
較少的代碼通常比較長的代碼更易讀,但并不總是如此。直截了當。
可以在單個打開的文件上多次調用轉儲或加載。每次呼叫
轉儲將存儲單個對象(加上由它組成或包含的任何對象),而
調用load將只加載并返回一個對象。所以對于單個文件,每個文件都是獨立的
存儲對象時對dump的調用應該有一個關聯的調用來加載
稍后恢復。
### 定制泡菜
對于大多數常見的蟒蛇對象,酸洗“只是起作用”。基本原語,例如
整數、loats和字符串可以被腌制,任何容器對象也可以,例如
列表或字典,前提是這些容器的內容也是可選擇的。
此外,重要的是,任何物體都可以腌制,只要它的所有屬性
也是可選擇的。
那么是什么使屬性不可拆分呢?通常,它與時間敏感屬性有關,在將來加載這些屬性是沒有意義的。例如,如果
我們有一個開放的網絡套接字、開放的文件、運行的線程或數據庫連接
作為屬性存儲在對象上,腌制這些對象是沒有意義的;許多
當我們試圖重新加載它們時,操作系統的狀態就會消失
稍后。我們不能假裝線程或套接字連接存在,然后讓它出現!
不,我們需要以某種方式定制這些臨時數據的存儲和恢復方式。
這里有一個類每小時加載一個網頁的內容,以確保它們
跟上時代。它使用線程。用于計劃下一次更新的計時器類:
```
from threading import Timer
import datetime
from urllib.request import urlopen
class UpdatedURL:
def __init__(self, url):
self.url = url
self.contents = ''
self.last_updated = None
self.update()
def update(self):
self.contents = urlopen(self.url).read()
self.last_updated = datetime.datetime.now()
self.schedule()
def schedule(self):
self.timer = Timer(3600, self.update)
self.timer.setDaemon(True)
self.timer.start()
```
網址、內容和最后更新都是可以選擇的,但是如果我們嘗試修改
作為這個類的一個實例,self.timer實例有點瘋狂:
```
>>> u = UpdatedURL("http://news.yahoo.com/")
>>> import pickle
>>> serialized = pickle.dumps(u)
Traceback (most recent call last):
File "<pyshell#3>", line 1, in <module>
serialized = pickle.dumps(u)
_pickle.PicklingError: Can't pickle <class '_thread.lock'>: attribute
lookup lock on _thread failed
```
這不是一個非常有用的錯誤,但是看起來我們是在嘗試腌制一些東西
不應該。這就是計時器實例;我們存儲了一個自我的引用。
schedule方法中的計時器,并且該屬性不能序列化。
當pickle試圖序列化一個對象時,它只是試圖存儲該對象的__dict__
屬性;__dict__是一個字典,將對象上的所有屬性名映射到
他們的價值觀。幸運的是,在檢查_ _ dict _ _ _之前,pickle會檢查
__getstate__方法存在。如果是,它將存儲該方法的返回值
而不是陳詞濫調。
讓我們在UpdatedURL類中添加一個__getstate__方法,該方法簡單地返回一個
不帶計時器的__dict__副本:
```
def __getstate__(self):
new_state = self.__dict__.copy()
if 'timer' in new_state:
del new_state['timer']
return new_state
```
如果我們現在腌制這個物體,它就不會再失敗了。我們甚至可以成功
使用負載還原對象。但是,恢復的對象沒有計時器
屬性,所以它不會像設計的那樣刷新內容。我們需要
當對象被
解開。
正如我們所料,有一個互補的__setstate__方法可以
實現自定義拆線。該方法接受單個參數,
這是__getstate__返回的對象。如果我們實現這兩種方法,__
getstate__不需要返回字典,因為__setstate__會知道
如何處理__getstate__選擇返回的任何對象。就我們而言,我們
只想恢復__dict__,然后創建一個新計時器:
```
def __setstate__(self, data):
self.__dict__ = data
self.schedule()
```
pickle模塊非常靈活,并提供其他工具來進一步定制
酸洗過程,如果你需要的話。然而,這些都超出了這一范圍
書。我們所介紹的工具對于許多基本的酸洗任務來說是足夠的。目標
要腌制的通常是相對簡單的數據對象;我們不太可能
例如,酸洗整個運行程序或復雜的設計模式。
### 序列化網絡對象
序列化web對象
從未知或不可信的來源加載腌制對象不是一個好主意。
有可能將任意代碼注入到一個被篡改的文件中來惡意攻擊
電腦通過泡菜。泡菜的另一個缺點是它們只能
由其他Python程序加載,并且不容易與服務共享
用其他語言寫的。
多年來,有許多格式被用于此目的。
可擴展標記語言曾經非常流行,尤其是在Java中
開發商。YAML(又一種標記語言)是你的另一種格式
偶爾會被引用。表格數據經常在CSV中交換
(逗號分隔值)格式。其中許多正在變得模糊不清
隨著時間的推移,你會遇到更多。蟒蛇有堅實的標準
或者第三方庫。
在不受信任的數據上使用此類庫之前,請確保調查安全性
與他們每個人的關系。例如,XML和YAML都有模糊的特性
惡意使用它,可以允許在主機上執行任意命令
機器。默認情況下,這些功能可能不會關閉。做你的研究。
JavaScript對象符號是一種人類可讀的交換格式
原始數據。JSON是一種標準格式,可以被廣泛的數組解釋
異構客戶端系統。因此,JSON對于傳輸非常有用
完全解耦系統之間的數據。此外,JSON沒有任何
支持可執行代碼,只有數據可以序列化;因此,這就更加困難了
向其中注入惡意語句。
因為JSON很容易被JavaScript引擎解釋,所以它通常用于
將數據從網絡服務器傳輸到支持JavaScript的網絡瀏覽器。如果
提供數據的網絡應用程序是用Python編寫的,它需要一種轉換的方式
內部數據轉換為JSON格式。
有一個模塊可以做到這一點,可以預見的命名為json。本模塊提供了一個類似的
pickle模塊的接口,具有轉儲、加載、轉儲和加載功能。這
對這些函數的默認調用與pickle中的調用幾乎相同,所以我們不要
重復細節。有幾個不同之處;顯然,這些調用的輸出
是有效的JSON符號,而不是腌制對象。此外,json功能
操作字符串對象,而不是字節。因此,當傾卸到或裝載時
從一個文件,我們需要創建文本文件,而不是二進制文件。
JSON序列化程序不如pickle模塊健壯;它只能序列化基本的
整數、loats和字符串等類型,以及字典等簡單容器
和列表。每一個都有一個到JSON表示的直接映射,但是JSON
無法表示類、方法或函數。不可能傳輸
以這種格式完成對象。因為我們丟棄的物體的接收器
對于JSON格式通常不是一個Python對象,它將無法理解
無論如何,類或方法的方式與Python相同。盡管奧因
對象的名稱中,JSON是一種數據符號;你記得,對象由
數據和行為。
如果我們確實有只希望序列化數據的對象,我們總是可以
序列化對象的__dict__屬性。或者我們可以通過以下方式半自動完成這項任務
提供自定義代碼來創建或解析JSON可序列化字典
某些類型的物體。
在json模塊中,對象存儲和加載功能都接受可選的
自定義行為的參數。轉儲和轉儲方法接受一個很差的
命名cls(class的縮寫,是保留關鍵字)關鍵字參數。如果
傳遞后,這應該是JSONEncoder類的子類,使用默認方法
被覆蓋。此方法接受任意對象,并將其轉換為字典
json能消化的。如果它不知道如何處理對象,我們應該調用
super()方法,這樣它就可以用正常方式序列化基本類型。
load和loads方法也接受這樣一個cls參數,它可以是
逆類JSONDecoder的子類。然而,通常只需
使用object_hook關鍵字參數將函數傳遞給這些方法。
該函數接受字典并返回一個對象;如果它不知道什么
為了處理輸入字典,它可以不進行二進制返回。
讓我們看一個例子。假設我們有以下簡單的聯系類
我們想要序列化的:
```
class Contact:
def __init__(self, first, last):
self.first = first
self.last = last
@property
def full_name(self):
return("{} {}".format(self.first, self.last))
```
我們可以序列化__dict__屬性:
```
>>> c = Contact("John", "Smith")
>>> json.dumps(c.__dict__)
'{"last": "Smith", "first": "John"}'
```
但是以這種方式訪問特殊(雙下劃線)屬性有點
原油。此外,如果接收代碼(可能是網頁上的一些JavaScript)會怎樣
想要提供全名屬性嗎?當然,我們可以構建
手動字典,但是讓我們創建一個自定義編碼器:
```
import json
class ContactEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Contact):
return {'is_contact': True,
'first': obj.first,
'last': obj.last,
'full': obj.full_name}
return super().default(obj)
```
默認方法基本上是檢查我們要嘗試什么樣的對象
序列化;如果是聯系人,我們會手動將其轉換為字典;否則,我們讓
父類處理序列化(假設它是基本類型,json
知道如何處理)。請注意,我們傳遞了一個額外的屬性來標識這個對象
作為聯系人,因為在加載它時無法分辨。這只是
一項公約;對于更通用的序列化機制,這可能更有意義
在字典中存儲字符串類型,甚至可能是完整的類名,包括
包裝和模塊。請記住,字典的格式取決于
接收端的代碼;必須就數據的進展達成一致
待定。
我們可以使用這個類通過傳遞類(不是實例化的
對象)轉儲到轉儲函數:
```
>>> c = Contact("John", "Smith")
>>> json.dumps(c, cls=ContactEncoder)
'{"is_contact": true, "last": "Smith", "full": "John Smith",
"first": "John"}'
```
對于解碼,我們可以編寫一個接受字典并檢查
is_contact變量的存在決定是否將其轉換為聯系人:
```
def decode_contact(dic):
if dic.get('is_contact'):
return Contact(dic['first'], dic['last'])
else:
return dic
```
我們可以使用object_hook將這個函數傳遞給一個或多個加載函數
關鍵字參數:
```
>>> data = ('{"is_contact": true, "last": "smith",'
'"full": "john smith", "first": "john"}')
>>> c = json.loads(data, object_hook=decode_contact)
>>> c
<__main__.Contact object at 0xa02918c>
>>> c.full_name
'john smith'
```
## 個案研究
讓我們用Python構建一個基本的正則表達式驅動的模板引擎。這
引擎將解析一個文本文件(比如一個網頁),并用
根據這些指令的輸入計算的文本。這是最復雜的
我們想用正則表達式完成的任務;事實上,完整版的
這可能會使用適當的語言解析機制。
考慮以下輸入文件:
```
/** include header.html **/
<h1>This is the title of the front page</h1>
/** include menu.html **/
<p>My name is /** variable name **/.
This is the content of my front page. It goes below the menu.</p>
<table>
<tr><th>Favourite Books</th></tr>
/** loopover book_list **/
<tr><td>/** loopvar **/</td></tr>
/** endloop **/
</table>
/** include footer.html **/
Copyright © Today
```
該文件包含/** <指令> <數據> **/格式的“標簽”,其中數據
一個可選的單詞和指令是:
包括:在此復制另一個文件的內容
變量:在這里插入變量的內容
循環:對列表變量重復循環內容
結束循環:發出循環文本結束的信號
循環變量:從循環的列表中插入一個值
該模板將根據傳遞到的變量呈現不同的頁面
它。這些變量將從所謂的上下文文件傳入。這將被編碼
作為json對象,用鍵表示所討論的變量。我的上下文文件
可能看起來像這樣,但你會得到你自己的:
```
{
"name": "Dusty",
"book_list": [
"Thief Of Time",
"The Thief",
"Snow Crash",
"Lathe Of Heaven"
]
}
```
在我們開始實際的字符串處理之前,讓我們一起來看看
面向對象的樣板代碼,用于處理文件和從
命令行:
```
import re
import sys
import json
from pathlib import Path
DIRECTIVE_RE = re.compile(
r'/\*\*\s*(include|variable|loopover|endloop|loopvar)'
r'\s*([^ *]*)\s*\*\*/')
class TemplateEngine:
def __init__(self, infilename, outfilename, contextfilename):
self.template = open(infilename).read()
self.working_dir = Path(infilename).absolute().parent
self.pos = 0
self.outfile = open(outfilename, 'w')
with open(contextfilename) as contextfile:
self.context = json.load(contextfile)
def process(self):
print("PROCESSING...")
if __name__ == '__main__':
infilename, outfilename, contextfilename = sys.argv[1:]
engine = TemplateEngine(infilename, outfilename, contextfilename)
engine.process()
```
這都是非常基本的,我們創建一個類并用一些變量初始化它
通過命令行傳入。
請注意,我們是如何通過
跨越兩條線?我們使用原始字符串(r preix),所以我們沒有
雙重逃脫我們所有的反擊。這在正則表達式中很常見,
但還是一團糟。(正則表達式總是如此,但它們通常是值得的。)
pos表示我們正在處理的內容中的當前字符;
一會兒我們會看到更多。
現在“剩下的就是實現這個過程方法”。有幾種方法
去做這件事。讓我們以相當明確的方式來做這件事。
process方法必須找到與正則表達式匹配的每個指令
用它做適當的工作。但是,它還必須負責輸出
輸出文件的每個指令之前、之后和之間的普通文本,未被二進制化。
正則表達式編譯版本的一個好特性是我們可以
告訴搜索方法通過傳遞pos在特定位置開始搜索
關鍵字參數。如果我們臨時定義用
指令作為“忽略指令并將其從輸出文件中刪除”,我們的過程
循環看起來很簡單:
```
def process(self):
match = DIRECTIVE_RE.search(self.template, pos=self.pos)
while match:
self.outfile.write(self.template[self.pos:match.start()])
self.pos = match.end()
match = DIRECTIVE_RE.search(self.template, pos=self.pos)
self.outfile.write(self.template[self.pos:])
```
在英語中,這個函數在文本中引入第一個與常規字符串匹配的字符串
表達式,輸出從當前位置到匹配開始的所有內容,
然后將該位置推進到前述比賽結束。一旦用完
匹配,它輸出自最后一個位置以來的所有內容。
當然,在模板引擎中忽略指令是沒有用的,所以讓我們
設置用委托給不同
方法,具體取決于指令:
```
def process(self):
match = DIRECTIVE_RE.search(self.template, pos=self.pos)
while match:
self.outfile.write(self.template[self.pos:match.start()])
directive, argument = match.groups()
method_name = 'process_{}'.format(directive)
getattr(self, method_name)(match, argument)
match = DIRECTIVE_RE.search(self.template, pos=self.pos)
self.outfile.write(self.template[self.pos:])
```
所以我們從正則表達式中獲取指令和單個參數。這
指令成為一個方法名,我們在
self對象(如果模板編寫器提供了
無效指令會更好)。我們將匹配對象和參數傳遞給它
方法,并假設該方法將適當地處理所有事情,包括
移動pos指針。
現在我們已經有了面向對象的架構,它實際上很漂亮
易于實現委托給的方法。包含和變量
指令非常簡單:
```
def process_include(self, match, argument):
with (self.working_dir / argument).open() as includefile:
self.outfile.write(includefile.read())
self.pos = match.end()
def process_variable(self, match, argument):
self.outfile.write(self.context.get(argument, ''))
self.pos = match.end()
```
第一個簡單地查找包含的文件并插入文件內容,而
其次,在上下文字典中查找變量名(從
如果空字符串不存在,則默認為空字符串。
處理循環的三種方法更加激烈,因為它們必須如此
分享他們三個的狀態。為了簡單起見(我相信你渴望看到
這漫長的一章結束了,我們就快到了!),我們將把它作為實例來處理
類本身的變量。作為一項練習,你可能想考慮更好的方法
來設計這個,尤其是在閱讀了接下來的三章之后。
```
def process_loopover(self, match, argument):
self.loop_index = 0
self.loop_list = self.context.get(argument, [])
self.pos = self.loop_pos = match.end()
def process_loopvar(self, match, argument):
self.outfile.write(self.loop_list[self.loop_index])
self.pos = match.end()
def process_endloop(self, match, argument):
self.loop_index += 1
if self.loop_index >= len(self.loop_list):
self.pos = match.end()
del self.loop_index
del self.loop_list
del self.loop_pos
else:
self.pos = self.loop_pos
```
當我們遇到循環指令時,我們不需要輸出任何東西,
但是我們必須在三個變量上設置初始狀態。循環列表變量是
假設是從上下文詞典中提取的列表。循環索引變量
指示在循環迭代中應該輸出列表中的什么位置,
當loop_pos被存儲時,這樣我們就知道當我們到達
循環結束。
loopvar指令輸出loop_list中當前位置的值
變量并跳到指令的末尾。請注意,它不會增加循環
索引,因為loopvar指令可以在循環中調用多次。
endloop指令更復雜。它決定了是否還有更多
循環列表中的元素;如果有,它就跳回到循環的開始,
遞增索引。否則,它會重置所有正在使用的變量
處理循環并跳轉到指令的末尾,以便引擎可以繼續運行
下一場比賽。
請注意,這種特殊的循環機制非常脆弱;如果模板設計者
如果嘗試嵌套循環或忘記endloop調用,對他們來說會很糟糕。
我們需要更多的錯誤檢查,并且可能想要存儲更多的循環
聲明將其作為生產平臺。但是我保證這一章的結尾
就快到了,所以在看完我們的樣本模板后,讓我們開始練習吧
用其上下文呈現:
```
<html>
<body>
<h1>This is the title of the front page</h1>
<a href="link1.html">First Link</a>
<a href="link2.html">Second Link</a>
<p>My name is Dusty.
This is the content of my front page. It goes below the menu.</p>
<table>
<tr><th>Favourite Books</th></tr>
<tr><td>Thief Of Time</td></tr>
<tr><td>The Thief</td></tr>
<tr><td>Snow Crash</td></tr>
<tr><td>Lathe Of Heaven</td></tr>
</table>
</body>
</html>
Copyright © Today
```
由于我們計劃模板的方式,出現了一些奇怪的換行效果,
但是它像預期的那樣工作。
## 摘要
我們已經討論了字符串操作、正則表達式和對象序列化
在本章中。硬編碼字符串和程序變量可以組合成
使用強大的字符串格式化系統輸出字符串。這很重要
為了區分二進制和文本數據,字節和字符串具有特定
必須理解的目的。兩者都是不可變的,但是bytearray類型
可以在操作字節時使用。
正則表達式是一個復雜的話題,但我們觸及了表面。有
序列化Python數據的多種方式;泡菜和JSON是最受歡迎的兩種。
在下一章中,我們將看一個對Python非常重要的設計模式
被賦予特殊語法支持的編程:迭代器模式。