在我们参与更高级的设计模式之前,让我们深入研究 Python 最常见的对象之一:字符串。我们将看到字符串的内容远远超出了我们的想象,还包括搜索字符串中的模式和序列化数据以进行存储或传输。
特别是,我们将访问:
- 字符串、字节和字节数组的复杂性
- 字符串格式的输入和输出
- 序列化数据的几种方法
- 神秘的正则表达式
字符串是 Python 中的一个基本原语;到目前为止,我们在讨论的几乎每个示例中都使用了它们。它们所做的只是表示一个不可变的字符序列。然而,尽管你以前可能没有考虑过,“字符”这个词有点模棱两可;Python 字符串可以表示重音字符序列吗?汉字?希腊语、西里尔语或波斯语呢?
在 Python3 中,答案是肯定的。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 个字符的行长度限制时,它非常有用。
与其他序列一样,字符串可以(逐个字符)迭代、索引、切片或连接。语法与列表相同。
str类上有许多方法,使操作字符串更容易。Python 解释器中的dir和help命令可以告诉我们如何使用它们;我们会直接考虑一些比较常见的问题。
几个布尔方便的方法帮助我们识别字符串中的字符是否与特定模式匹配。下面是这些方法的总结。其中大多数,如isalpha、isupper/islower和startswith/endswith都有明显的解释。isspace方法也很明显,但请记住,所有的空白字符(包括制表符、换行符)都会被考虑,而不仅仅是空格字符。
如果每个单词的第一个字符是大写的,而所有其他字符都是小写的,istitle方法返回True。请注意,它没有严格执行标题格式的英语语法定义。例如,利·亨特的诗《手套与狮子》应该是一个有效的标题,尽管不是所有的词都大写。罗伯特服务的“火葬 Sam McGee”也应该是一个有效的标题,即使有一个大写字母在中间的最后一个字。
小心使用isdigit、isdecimal和isnumeric方法,因为它们比您预期的更加微妙。除了我们习惯使用的十位数之外,许多 Unicode 字符被认为是数字。更糟糕的是,我们用于从字符串构造浮点的句点字符不被视为十进制字符,因此'45.2'.isdecimal()返回False。实际十进制字符由 Unicode 值 0660 表示,如 45.2 所示(或45\u06602)。此外,这些方法不验证字符串是否为有效数字;“127.0.0.1”返回所有三种方法的True。我们可能认为应该使用该十进制字符而不是句点来表示所有数字量,但将该字符传递到float()或nt()构造函数会将该十进制字符转换为零:
>>> 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方法可以使用字典将任意输入字符映射到指定的输出字符。
对于所有的方法,请注意输入字符串保持不变;返回一个全新的str实例。如果我们需要操作结果字符串,我们应该将其分配给一个新变量,如new_value = value.capitalize()中所示。通常,一旦我们执行了转换,我们就不再需要旧的值了,所以一个常见的习惯用法是将它赋给同一个变量,如value = value.title()。
最后,有两个字符串方法返回或操作列表。split方法接受一个子字符串,并在该子字符串出现的任何位置将该字符串拆分为一个字符串列表。可以将数字作为第二个参数传递,以限制结果字符串的数量。如果不限制字符串的数量,rsplit的行为与split相同,但如果提供了限制,它将从字符串的末尾开始分裂。partition和rpartition 方法仅在子字符串的第一次或最后一次出现时分割字符串,并返回三个值的元组:子字符串之前的字符、子字符串本身以及子字符串之后的字符。
与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 用于组合字符串和变量以创建新字符串的方法。
Python3 具有强大的字符串格式化和模板机制,允许我们构造由硬编码文本和散布变量组成的字符串。我们在前面的许多示例中都使用过它,但它比我们使用的简单格式说明符更通用。
任何字符串都可以通过调用其上的format()方法转换为格式字符串。此方法返回一个新字符串,其中输入字符串中的特定字符已替换为作为参数和传递到函数中的关键字参数提供的值。format方法不需要固定的参数集;在内部,它使用了我们在第 7 章、Python 面向对象快捷方式中讨论的*args和**kwargs语法。
格式化字符串中替换的特殊字符是大括号的开头和结尾:{和}。我们可以在字符串中插入这些参数对,它们将依次替换为传递给str.format方法的任何位置参数:
template = "Hello {}, you are currently {}."
print(template.format('Dusty', 'writing'))如果我们运行这些语句,它将用变量替换大括号,顺序如下:
Hello Dusty, you are currently writing.如果我们想在一个字符串中重用变量,或者决定在不同的位置使用变量,那么这种基本语法并不是非常有用。我们可以将零索引整数放在大括号内,告诉格式化程序在字符串中的给定位置插入了哪个位置变量。让我们重复一下这个名字:
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 文件。事实证明,这是关于最简单的 Python 程序来打印最简单的 Java 程序,可以打印最简单的 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字符串中访问这些对象上的索引和变量(但不是方法)。
例如,如果我们的电子邮件将“发件人”和“收件人”电子邮件地址分组为一个元组,并将主题和消息放在字典中,出于某种原因(可能是因为这是我们想要使用的现有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(本例中为emails元组)的第一个位置参数。
方括号内的数字与我们在常规 Python 代码中看到的索引查找相同,因此0[0]映射到emails元组中的emails[0]。索引语法适用于任何可索引对象,因此我们在访问message[subject]时会看到类似的行为,但这次我们在字典中查找字符串键。请注意,与 Python 代码不同,我们不需要在字典查找中的字符串周围加引号。
如果我们有嵌套的数据结构,我们甚至可以进行多级查找。我建议不要经常这样做,因为模板字符串很快就会变得难以理解。如果我们有一个包含元组的字典,我们可以这样做:
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()对象。浮动是危险的,因为它们的计算本身就不精确,超出了特定的精度水平。但我们关注的是字符串,而不是浮动,而货币是格式化的一个很好的例子!
为了修复前面的format字符串,我们可以在大括号中包含一些附加信息,以调整参数的格式。我们可以定制很多东西,但大括号内的基本语法是相同的;首先,我们使用任何一种较早的布局(位置、关键字、索引、属性访问)来指定要放置在模板字符串中的变量。接下来是冒号,然后是格式化的特定语法。这是一个改进的版本:
print("Sub: ${0:0.2f} Tax: ${1:0.2f} "
"Total: ${total:0.2f}".format(
subtotal, tax, total=total))冒号后的0.2f格式说明符基本上是从左到右:对于小于 1 的值,确保小数点左侧显示零;小数点后显示两位;将输入值格式化为浮点数。
我们还可以通过在精度中的句点之前放置一个值来指定每个数字应该占用屏幕上特定数量的字符。这对于输出表格数据非常有用,例如:
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所做的一样。
quantity值的格式化程序为{1: ^9d}。d表示一个整数值。9告诉我们值应该占 9 个字符。但是对于整数,而不是空格,默认情况下,额外的字符是零。看起来有点奇怪。因此,我们显式地指定一个空格(紧跟在冒号之后)作为填充字符。插入符号字符^告诉我们数字应该在这个可用填充的中心对齐;这使专栏看起来更专业一些。说明符的顺序必须正确,尽管所有说明符都是可选的:首先填充,然后对齐,然后是大小,最后是类型。
我们对价格和小计的说明符也做了类似的事情。对于price,我们使用{2: <8.2f},对于subtotal,我们使用{3: >7.2f}。在这两种情况下,我们都指定了一个空格作为填充字符,但我们分别使用了<和>符号来表示数字应该在八个或七个字符的最小空格内向左或向右对齐。此外,每个浮点数的格式应设置为小数点后两位。
不同类型的“type”字符也会影响格式化输出。我们已经看到了字符串、整数和浮点数的s、d和f类型。大多数其他格式说明符都是这些格式说明符的替代版本;例如,o表示八进制格式,X表示整数的十六进制格式。n类型说明符可用于以当前语言环境的格式格式化整数分隔符。对于浮点数,%类型将乘以 100,并将浮点数格式化为百分比。
虽然这些标准格式化程序适用于大多数内置对象,但其他对象也可以定义非标准说明符。例如,如果我们将一个datetime对象传递给format,我们可以使用datetime.strftime函数中使用的说明符,如下所示:
import datetime
print("{0:%Y-%m-%d %I:%M%p }".format(
datetime.datetime.now()))甚至可以为我们自己创建的对象编写自定义格式化程序,但这超出了本书的范围。如果您需要在代码中重写__format__特殊方法,请研究重写该方法。最全面的说明可在 PEP 3101 的中找到 http://www.python.org/dev/peps/pep-3101/ ,虽然细节有点枯燥。你可以通过网络搜索找到更容易理解的教程。
Python 格式语法非常灵活,但它是一种很难记住的迷你语言。我每天都在使用它,偶尔还要在文档中查找被遗忘的概念。它的功能也不足以满足严重的模板需求,例如生成网页。如果您需要对一些字符串进行基本格式化以外的工作,可以查看几个第三方模板库。
在本节开头,我们将字符串定义为不可变 Unicode 字符的集合。这实际上有时会使事情变得非常复杂,因为 Unicode 并不是一种真正的存储格式。例如,如果您从一个文件或套接字中获取一个字节字符串,那么它们就不是 Unicode 格式的。事实上,它们将是内置类型bytes。字节是不可变的。。。嗯,字节。字节是计算中最低级别的存储格式。它们表示 8 位,通常被描述为介于 0 和 255 之间的整数,或介于 0 和 FF 之间的十六进制等效值。字节不代表任何特定的内容;字节序列可以存储编码字符串的字符或图像中的像素。
如果我们打印一个字节对象,映射到 ASCII 表示的任何字节都将打印为其原始字符,而非 ASCII 字节(无论是二进制数据还是其他字符)将打印为通过\x转义序列转义的十六进制代码。您可能会发现奇怪的是,一个以整数表示的字节可以映射到 ASCII 字符。但 ASCII 实际上只是一种代码,其中每个字母都由不同的字节模式表示,因此是一个不同的整数。字符“a”由与整数 97 相同的字节表示,整数 97 是十六进制数 0x61。具体来说,所有这些都是对二进制模式 0110001 的解释。
许多 I/O 操作只知道如何处理bytes,即使 bytes 对象引用文本数据。因此,了解如何在bytes和 Unicode 之间进行转换至关重要。
问题是有很多方法可以将bytes映射到 Unicode 文本。字节是机器可读的值,而文本是人类可读的格式。介于两者之间的是一种编码,它将给定的字节序列映射到给定的文本字符序列。
但是,有多种这样的编码(ASCII 只是其中之一)。当使用不同编码映射时,相同的字节序列表示完全不同的文本字符!因此,bytes必须使用编码它们的相同字符集进行解码。在不知道字节应该如何解码的情况下,从字节中获取文本是不可能的。如果我们在没有指定编码的情况下接收到未知字节,我们所能做的最好的事情就是猜测它们的编码格式,我们可能是错的。
如果我们有一个来自某处的bytes数组,我们可以使用bytes类上的.decode方法将其转换为 Unicode。此方法接受字符串作为字符编码的名称。有很多这样的名字;西方语言的通用标准包括 ASCII、UTF-8 和拉丁语-1。
字节序列(十六进制)63 6c 69 63 68 e9 实际上代表拉丁语 1 编码中的陈词滥调字符。以下示例将对该字节序列进行编码,并使用拉丁-1 编码将其转换为 Unicode 字符串:
characters = b'\x63\x6c\x69\x63\x68\xe9'
print(characters)
print(characters.decode("latin-1"))第一行创建一个bytes对象;字符串前面的b字符告诉我们,我们正在定义一个bytes对象,而不是普通的 Unicode 字符串。在字符串中,每个字节在本例中使用十六进制数指定。\x字符在字节字符串中转义,每个字符都表示:“接下来的两个字符表示使用十六进制数字的字节。”
如果我们使用的 shell 理解拉丁-1 编码,那么两个print调用将输出以下字符串:
b'clich\xe9'
cliché第一条print语句将 ASCII 字符的字节呈现为自身。未知(即 ASCII 未知)字符保持其转义十六进制格式。输出在行首包含一个b字符,提醒我们它是bytes表示,而不是字符串。
下一个调用使用拉丁-1 编码对字符串进行解码。decode方法返回具有正确字符的普通(Unicode)字符串。然而,如果我们使用西里尔字母“iso8859-5”编码解码同一个字符串,我们将得到字符串“陈词滥调”!这是因为\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)你现在明白编码的重要性了吗?重音字符表示为每个编码的不同字节;如果在将字节解码为文本时使用了错误的字符,则会得到错误的字符。
最后一种情况下的异常并不总是期望的行为;在某些情况下,我们可能希望以不同的方式处理未知字符。encode方法采用名为errors的可选字符串参数,该参数可以定义如何处理此类字符。此字符串可以是以下字符串之一:
strictreplaceignorexmlcharrefreplace
strict替换策略是我们刚才看到的默认策略。当遇到在请求的编码中没有有效表示形式的字节序列时,将引发异常。当使用replace策略时,字符被替换为不同的字符;在 ASCII 中,它是一个问号;其他编码可能使用不同的符号,例如空框。ignore策略简单地丢弃它不理解的任何字节,而xmlcharrefreplace策略创建一个表示 Unicode 字符的xml实体。这在转换未知字符串以在 XML 文档中使用时非常有用。以下是每种策略如何影响我们的示例单词:
策略
|
“陈词滥调”。编码(“ascii”,策略)
|
| --- | --- |
| replace | b'clich?' |
| ignore | b'clich' |
| xmlcharrefreplace | b'cliché' |
可以在不传递编码字符串的情况下调用str.encode和bytes.decode方法。编码将设置为当前平台的默认编码。这将取决于当前的操作系统和区域或区域设置;您可以使用sys.getdefaultencoding()功能进行查找。但是,明确指定编码通常是一个好主意,因为平台的默认编码可能会改变,或者有一天程序可能会扩展到处理来自更广泛来源的文本。
如果您正在编码文本,但不知道使用哪种编码,最好使用 UTF-8 编码。UTF-8 能够表示任何 Unicode 字符。在现代软件中,它是一种事实上的标准编码,以确保可以交换任何语言甚至多种语言的文档。其他各种可能的编码对于遗留文档或默认情况下仍使用不同字符集的区域非常有用。
UTF-8 编码使用一个字节表示 ASCII 和其他常用字符,对于更复杂的字符,最多使用四个字节。UTF-8 是特殊的,因为它向后兼容 ASCII;任何使用 UTF-8 编码的 ASCII 文档都将与原始 ASCII 文档相同。
我不记得是使用encode还是decode将二进制字节转换为 Unicode。我一直希望这些方法被命名为“to_binary”和“from_binary”。如果你有同样的问题,试着用“二进制”替换“代码”;“enbinary”和“debinary”与“to_binary”和“from_binary”非常接近。自从设计了这个助记符后,我没有查找方法帮助文件,节省了很多时间。
bytes类型与str一样是不可变的。我们可以在bytes对象上使用索引和切片表示法并搜索特定的字节序列,但我们不能扩展或修改它们。这在处理 I/O 时会非常不方便,因为通常需要缓冲传入或传出字节,直到它们准备好发送。例如,如果我们正在从一个套接字接收数据,在我们接收到整个消息之前,可能需要几次recv调用。
这就是内置的bytearray功能。这种类型的行为类似于列表,只是它只保存字节。类的构造函数可以接受一个bytes对象来初始化它。extend方法可用于将另一个bytes对象附加到现有数组中(例如,当更多数据来自套接字或其他 I/O 通道时)。
切片表示法可用于bytearray以内联方式修改项目。例如,此代码从一个bytes对象构造一个bytearray,然后替换两个字节:
b = bytearray(b"abcdefgh")
b[4:6] = b"\x15\xa3"
print(b)输出如下所示:
bytearray(b'abcd\x15\xa3gh')小心;如果我们想要操作bytearray中的单个元素,它将要求我们传递一个介于 0 和 255 之间(包括 0 和 255)的整数作为值。这个整数表示一个特定的bytes模式。如果我们试图传递一个字符或bytes对象,它将引发一个异常。
单字节字符可以使用ord(序数的缩写)函数转换为整数。此函数返回单个字符的整数表示形式:
b = bytearray(b'abcdef')
b[3] = ord(b'g')
b[4] = 68
print(b)输出如下所示:
bytearray(b'abcgDf')构造数组后,我们将索引3处的字符(第四个字符,索引从0开始,与列表一样)替换为字节 103。此整数由ord函数返回,是小写g的 ASCII 字符。为了便于说明,我们还将下一个字符 up 替换为字节号68,它映射到大写字母D的 ASCII 字符。
bytearray类型的方法允许它像一个列表一样工作(例如,我们可以向它附加整数字节),但也像一个bytes对象;我们可以使用类似于count和find的方法,就像它们在bytes或str对象上的行为一样。不同之处在于bytearray是一种可变类型,可用于从特定输入源构建复杂的字节序列。
您知道使用面向对象的原则很难做到什么吗?解析字符串以匹配任意模式,就是这样。已经有相当多的学术论文使用面向对象的设计来建立字符串解析,但是结果总是非常冗长且难以阅读,并且在实践中没有得到广泛的应用。
在现实世界中,大多数编程语言中的字符串解析都是由正则表达式处理的。这些不是冗长的,但是,孩子,它们曾经很难阅读,至少在你学会语法之前是如此。即使正则表达式不是面向对象的,Python 正则表达式库也提供了一些类和对象,可以用来构造和运行正则表达式。
正则表达式用于解决一个常见问题:给定一个字符串,确定该字符串是否与给定的模式匹配,并(可选)收集包含相关信息的子字符串。它们可用于回答以下问题:
- 这个字符串是有效的 URL 吗?
- 日志文件中所有警告消息的日期和时间是多少?
/etc/passwd中的哪些用户属于给定的组?- 访问者键入的 URL 请求了什么用户名和文档?
有许多类似的场景,正则表达式是正确的答案。许多程序员犯了实现复杂而脆弱的字符串解析库的错误,因为他们不知道或不愿意学习正则表达式。在本节中,我们将获得足够的正则表达式知识,以避免犯此类错误!
正则表达式是一种复杂的小型语言。它们依赖于特殊字符来匹配未知字符串,但让我们从文字字符开始,例如字母、数字和空格字符,它们总是匹配自己。让我们看一个基本示例:
import re
search_string = "hello world"
pattern = "hello world"
match = re.match(pattern, search_string)
if match:
print("regex matches")正则表达式的 Python 标准库模块称为re。我们导入它并设置要搜索的搜索字符串和模式;在本例中,它们是相同的字符串。由于搜索字符串与给定的模式匹配,因此条件传递并执行print语句。
请记住,match函数将模式与字符串开头匹配。因此,如果模式为"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>"调用,但为了节省空间,我们将只在以下示例中看到输出。
如果需要控制项目是发生在行的开头还是结尾(或者如果字符串中没有换行符,则在字符串的开头和结尾),可以使用^和$字符分别表示字符串的开头和结尾。如果希望模式匹配整个字符串,最好同时包含以下两项:
'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'请注意上一个示例如何不匹配,因为句点在模式中的位置没有字符。
这就是好的,但是如果我们只想要几个特定的字符匹配呢?我们可以将一组字符放在方括号内,以匹配其中任何一个字符。因此,如果我们在正则表达式模式中遇到字符串[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]'对于该模式,两个字符\.匹配单个.字符。如果句点字符缺失或是其他字符,则它不匹配。
此反斜杠转义序列用于正则表达式中的各种特殊字符。您可以使用\[插入方括号而无需启动字符类,\(插入圆括号,我们稍后将看到圆括号也是一个特殊字符。
更有趣的是,我们还可以使用转义符号后跟字符来表示特殊字符,例如换行符(\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'有了这个信息,我们可以匹配已知长度的大多数字符串,但大多数时候我们不知道在一个模式中要匹配多少个字符。正则表达式也可以处理这个问题。我们可以通过添加几个难以记忆的标点符号中的一个来修改模式,以匹配多个字符。
星号(*字符表示前一个模式可以匹配零次或多次。这可能听起来很傻,但它是最有用的重复字符之一。在探究原因之前,考虑一些愚蠢的例子来确保我们理解它的作用:
'hello' matches pattern 'hel*o'
'heo' matches pattern 'hel*o'
'helllllo' matches pattern 'hel*o'因此,模式中的*字符表示前面的模式(l字符)是可选的,如果存在,可以重复尽可能多的次数以匹配模式。其余字符(h、e和o必须恰好出现一次。
想要多次匹配一个字母是非常罕见的,但是如果我们将星号与匹配多个字符的模式结合起来,它会变得更有趣。例如,.*将匹配任何字符串,而[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]+)*\.$'第一个单词以大写字母开头,后跟零个或多个小写字母。然后,我们输入一个括号,它匹配一个空格,后跟一个由一个或多个小写字母组成的单词。整个插入语重复零次或多次,模式以句点结束。句点后不能有任何其他字符,如与字符串结尾匹配的$所示。
我们已经看到了许多最基本的模式,但是正则表达式语言支持更多的模式。最初几年,每当我需要做一些事情时,我都会使用正则表达式查找语法。值得为re模块的 Python 文档添加书签并经常查看。正则表达式无法匹配的东西很少,它们应该是解析字符串时使用的第一个工具。
现在让我们把注意力集中在 Python 方面。正则表达式语法是距离面向对象编程最远的东西。然而,Python 的re模块提供了一个面向对象的接口来进入正则表达式引擎。
我们一直在检查re.match函数是否返回有效对象。如果模式不匹配,则该函数返回None。但是,如果它确实匹配,它将返回一个有用的对象,我们可以对其进行内省以获取有关模式的信息。
到目前为止,我们的正则表达式已经回答了诸如“此字符串是否匹配此模式?”之类的问题。匹配模式很有用,但在许多情况下,更有趣的问题是,“如果此字符串匹配此模式,则相关子字符串的值是多少?”如果使用组来标识稍后要引用的模式部分,您可以从匹配返回值中获取它们,如下一个示例所示:
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)描述有效电子邮件地址的规范极其复杂,并且精确匹配所有可能性的正则表达式非常长。因此,我们欺骗并制作了一个简单的正则表达式,它匹配一些常见的电子邮件地址;关键是我们想要访问域名(在@符号之后),这样我们就可以连接到那个地址。通过将模式的这一部分封装在括号中,并对 match 返回的对象调用groups()方法,可以很容易地实现这一点。
groups方法返回模式内匹配的所有组的元组,您可以对其进行索引以访问特定值。这些组从左到右排列。但是,请记住,组可以嵌套,这意味着您可以在另一个组中包含一个或多个组。在这种情况下,将按最左括号的顺序返回组,因此最外层的组将在其内部匹配组之前返回。
除了匹配功能外,re模块还提供了两个其他有用的功能,search和findall。search函数查找匹配模式的第一个实例,放宽模式从字符串的第一个字母开始的限制。请注意,通过使用 match 并在模式的前面放置一个^.*字符来匹配字符串开头和要查找的模式之间的任何字符,可以获得类似的效果。
findall函数的行为类似于搜索,只是它查找匹配模式的所有非重叠实例,而不仅仅是第一个实例。基本上,它会找到第一个匹配项,然后将搜索重置到匹配字符串的末尾,并找到下一个匹配项。
它不会像您预期的那样返回匹配对象的列表,而是返回匹配字符串的列表。或者元组。有时是字符串,有时是元组。这根本不是一个很好的 API!与所有糟糕的 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循环中),最好只执行一次转换步骤。
这可以通过re.compile方法实现。它返回一个已编译的正则表达式的面向对象版本,其中包含我们已经探索过的方法(match、search、findall)。我们将在案例研究中看到这方面的例子。
这绝对是对正则表达式的简明介绍。在这一点上,我们对基础知识有了很好的感觉,并将认识到何时需要进行进一步的研究。如果我们有一个字符串模式匹配问题,正则表达式几乎肯定能够为我们解决它们。然而,我们可能需要在更全面的主题中查找新的语法。但现在我们知道该找什么了!让我们转到一个完全不同的主题:为存储序列化数据。
如今,我们想当然地认为将数据写入文件并在以后任意日期检索数据的能力。尽管这很方便(如果我们不能存储任何东西,想象一下计算的状态!),我们经常发现自己将存储在内存中的漂亮对象或设计模式中的数据转换为某种笨重的文本或二进制格式,以便存储、通过网络传输或在远程服务器上远程调用。
Pythonpickle模块是一种面向对象的方式,以特殊的存储格式直接存储对象。它本质上是将一个对象(以及它作为属性保存的所有对象)转换为一个字节序列,可以按照我们认为合适的方式存储或传输。
对于基础工作,pickle模块有一个非常简单的接口。它由四个基本功能组成,用于存储和加载数据;两个用于操纵类似文件的对象,两个用于操纵bytes对象(后者只是指向类似文件界面的快捷方式,因此我们不必自己创建类似BytesIO文件的对象)。
dump方法接受要写入的对象和要将序列化字节写入的类似文件的对象。这个对象必须有一个write方法(否则它不会像文件一样),并且该方法必须知道如何处理bytes参数(这样为文本输出打开的文件就不起作用)。
load方法正好相反;它从类似文件的对象中读取序列化对象。此对象必须具有适当的文件,如read和readline参数,当然,每个参数都必须返回bytes。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语句打开文件,以便自动关闭。首先打开文件进行写入,然后再打开第二次读取,这取决于我们是存储还是加载数据。
如果新加载的对象与原始对象不相等,则结尾的assert语句将引发错误。平等并不意味着它们是同一个对象。事实上,如果我们打印两个对象的id(),我们会发现它们是不同的。但是,由于它们都是内容相等的列表,因此这两个列表也被视为相等的。
dumps和loads函数的行为与其类似文件的对应函数非常相似,只是它们返回或接受bytes而不是类似文件的对象。dumps函数只需要一个参数,即要存储的对象,并返回一个序列化的bytes对象。loads函数需要一个bytes对象并返回还原的对象。方法名称中的's'字符是字符串的缩写;它是 Python 古代版本的遗留名称,在 Python 中使用了str对象而不是bytes。
两个dump方法都接受可选的protocol参数。如果我们正在保存和加载只在 Python3 程序中使用的 pickle 对象,则不需要提供此参数。不幸的是,如果我们存储的对象可能由较旧版本的 Python 加载,那么我们必须使用较旧且效率较低的协议。这通常不应该是一个问题。通常,加载 pickle 对象的唯一程序与存储该对象的程序相同。Pickle 是一种不安全的格式,所以我们不希望通过互联网将其发送给未知的口译员。
提供的参数是一个整数版本号。默认版本是数字 3,表示 Python3 pickling 使用的当前高效存储系统。数字 2 是较旧的版本,它将存储一个对象,可以在所有解释器上加载回 Python 2.3。由于 2.6 是最古老的 Python,在野外仍然广泛使用,因此版本 2 的酸洗通常就足够了。旧版本的口译员支持版本 0 和 1;0 是 ASCII 格式,而 1 是二进制格式。还有一个优化版本 4,可能有一天会成为默认版本。
根据经验,如果您知道正在酸洗的对象将仅由 Python 3 程序加载(例如,只有您的程序将加载它们),请使用默认的酸洗协议。如果它们可能由未知的解释器加载,则传递协议值 2,除非您确实认为它们可能需要由 Python 的古老版本加载。
如果我们确实将协议传递给dump或dumps,我们应该使用关键字参数来指定它:pickle.dumps(my_object, protocol=2)。严格来说,这不是必需的,因为该方法只接受两个参数,但键入完整的关键字参数会提醒代码的读者该数字的用途。在方法调用中使用随机整数将很难读取。两个什么?也许可以存储两份该对象的副本?记住,代码应该总是可读的。在 Python 中,更少的代码通常比更长的代码更具可读性,但并非总是如此。要明确。
可以对单个打开的文件多次调用dump或load。对dump的每次调用将存储一个对象(加上它所组成或包含的任何对象),而对load的调用将只加载并返回一个对象。因此,对于单个文件,在存储对象时对dump的每个单独调用都应该在稍后恢复时对load进行关联调用。
对于大多数常见的 Python 对象,pickle“只起作用”。基本原语(如整数、浮点和字符串)可以被 pickle,任何容器对象(如列表或字典)也可以被 pickle,只要这些容器的内容也可以被 pickle。更重要的是,任何对象都可以被 pickle,只要它的所有属性都是可 pickle 的。
那么,是什么使属性不可粘贴呢?通常,它与时间敏感属性有关,将来加载这些属性是没有意义的。例如,如果我们有一个打开的网络套接字、打开的文件、正在运行的线程或数据库连接存储为对象上的属性,那么 pickle 这些对象就没有意义;当我们稍后尝试重新加载时,许多操作系统状态都会消失。我们不能假装线程或套接字连接存在,然后让它出现!不,我们需要以某种方式自定义这种瞬态数据的存储和恢复方式。
这里有一个类,每小时加载一个网页的内容,以确保它们保持最新。它使用threading.Timer类来安排下一次更新:
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()url、contents和last_updated都是可以 pickle 的,但是如果我们尝试 pickle 这个类的一个实例,那么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这不是一个非常有用的错误,但看起来我们正在试图改变一些我们不应该做的事情。这就是Timer的例子;我们正在 schedule 方法中存储对self.timer的引用,该属性无法序列化。
当pickle尝试序列化一个对象时,它只是尝试存储该对象的__dict__属性;__dict__是一个字典,将对象上的所有属性名称映射到它们的值。幸运的是,在检查__dict__之前,pickle检查__getstate__方法是否存在。如果是,它将存储该方法的返回值,而不是__dict__。
让我们在UpdatedURL类中添加一个__getstate__方法,该方法只返回__dict__的一个副本,不带计时器:
def __getstate__(self):
new_state = self.__dict__.copy()
if 'timer' in new_state:
del new_state['timer']
return new_state如果我们现在 pickle 对象,它将不再失败。我们甚至可以使用loads成功恢复该对象。但是,还原的对象没有计时器属性,因此它不会像设计的那样刷新内容。当对象被取消勾选时,我们需要以某种方式创建一个新的计时器(以替换丢失的计时器)。
正如我们所期望的,有一个互补的__setstate__方法可以用来定制取消勾选。此方法接受单个参数,即__getstate__返回的对象。如果我们实现这两种方法,__getstate__不需要返回字典,因为__setstate__知道如何处理__getstate__选择返回的任何对象。在我们的例子中,我们只想恢复__dict__,然后创建一个新的计时器:
def __setstate__(self, data):
self.__dict__ = data
self.schedule()pickle模块非常灵活,如果您需要,还可以提供其他工具来进一步定制酸洗过程。然而,这些都超出了本书的范围。我们介绍的工具足以完成许多基本的酸洗任务。要 pickle 的对象通常是相对简单的数据对象;例如,我们不太可能对整个正在运行的程序或复杂的设计模式进行 pickle 处理。
从未知或不受信任的源加载 pickle 对象不是一个好主意。可以将任意代码注入到 pickle 文件中,通过 pickle 恶意攻击计算机。pickle 的另一个缺点是它们只能由其他 Python 程序加载,并且不能很容易地与用其他语言编写的服务共享。
多年来,有许多格式用于此目的。XML(可扩展标记语言)过去非常流行,特别是 Java 开发人员。YAML(另一种标记语言)是您偶尔会看到引用的另一种格式。表格数据经常以 CSV(逗号分隔值)格式交换。其中许多正在逐渐消失,随着时间的推移,你将遇到更多的问题。Python 为它们都提供了可靠的标准库或第三方库。
在对不受信任的数据使用此类库之前,请确保调查每个库的安全问题。例如,XML 和 YAML 都有一些模糊的特性,恶意使用这些特性可以在主机上执行任意命令。默认情况下,不能关闭这些功能。做你的研究。
JavaScript 对象表示法(JSON)是用于交换原始数据的人类可读格式。JSON 是一种标准格式,可以由大量异构客户端系统进行解释。因此,JSON 对于在完全解耦的系统之间传输数据非常有用。此外,JSON 不支持任何可执行代码,只能序列化数据;因此,向其中注入恶意语句更加困难。
因为 JSON 很容易被 JavaScript 引擎解释,所以它通常用于将数据从 web 服务器传输到支持 JavaScript 的 web 浏览器。如果提供数据的 web 应用程序是用 Python 编写的,那么它需要一种将内部数据转换为 JSON 格式的方法。
有一个模块可以做到这一点,可以预见的是,它被命名为json。该模块提供与pickle模块类似的接口,具有dump、load、dumps和loads功能。对这些函数的默认调用与pickle中的调用几乎相同,因此我们不必重复这些细节。有两个不同点;显然,这些调用的输出是有效的 JSON 符号,而不是 pickle 对象。此外,json函数操作str对象,而不是bytes。因此,当转储到文件或从文件加载时,我们需要创建文本文件,而不是二进制文件。
JSON 序列化程序不如pickle模块那么健壮;它只能序列化基本类型(如整数、浮点和字符串)和简单容器(如字典和列表)。其中每一个都有到 JSON 表示的直接映射,但 JSON 无法表示类、方法或函数。无法以这种格式传输完整的对象。因为我们转储为 JSON 格式的对象的接收者通常不是 Python 对象,所以无论如何,它不能像 Python 那样理解类或方法。尽管名称中的对象是 O,但 JSON 是一种数据符号;您还记得,对象由数据和行为组成。
如果我们确实有只想序列化数据的对象,那么我们总是可以序列化对象的__dict__属性。或者,我们可以通过提供自定义代码从特定类型的对象创建或解析 JSON 可序列化字典来半自动化此任务。
在json模块中,对象存储和加载函数都接受可选参数来定制行为。dump和dumps方法接受一个名称很差的cls(class 的缩写,是保留关键字)关键字参数。如果通过,它应该是JSONEncoder类的一个子类,并重写default方法。此方法接受任意对象并将其转换为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)希望提供full_name属性,该怎么办?当然,我们可以手工构建字典,但让我们创建一个自定义编码器:
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)default方法基本上检查要序列化的对象类型;如果是联系人,我们会手动将其转换为字典;否则,我们让父类处理序列化(假设它是一个基本类型,json知道如何处理)。请注意,我们传递了一个额外的属性来将该对象标识为联系人,因为加载时无法判断。这只是一个惯例;对于更通用的序列化机制,在字典中存储字符串类型可能更有意义,甚至可能存储完整的类名,包括包和模块。记住,字典的格式取决于接收端的代码;必须就如何指定数据达成一致。
我们可以通过将类(不是实例化对象)传递给dump或dumps函数,使用该类对联系人进行编码:
>>> 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关键字参数将此函数传递给load或loads函数:
>>> 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 构建一个基本的正则表达式驱动的模板引擎。此引擎将解析文本文件(如 HTML 页面),并用从输入到这些指令的文本计算出的文本替换某些指令。这是我们想要用正则表达式完成的最复杂的任务;事实上,一个成熟的版本很可能会使用一个合适的语言解析机制。
考虑下面的输入文件:
/** 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该文件包含格式为/** <directive> <data> **/的“标签”,其中数据为可选的单个单词,指令为:
include:在此复制另一个文件的内容variable:在此插入变量的内容loopover:对列表变量重复循环内容endloop:表示循环文本结束loopvar:从正在循环的列表中插入一个值
此模板将根据传入的变量呈现不同的页面。这些变量将从所谓的上下文文件传入。这将被编码为一个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 前缀),因此不必对所有反斜杠进行双重转义。这在正则表达式中很常见,但仍然很混乱。(正则表达式总是如此,但它们通常是值得的。)
pos表示我们正在处理的内容中的当前字符;稍后我们会看到更多。
现在“剩下的”就是实现这个过程方法。有几种方法可以做到这一点。让我们以一种相当明确的方式来做。
process 方法必须找到与正则表达式匹配的每个指令,并对其进行适当的处理。但是,它还必须注意在每个指令之前、之后和之间将正常文本输出到输出文件(未修改)。
正则表达式编译版本的一个很好的特性是,我们可以通过传递pos关键字参数来告诉search方法在特定位置开始搜索。如果我们临时将使用指令执行适当的工作定义为“忽略该指令并将其从输出文件中删除”,那么我们的流程循环看起来相当简单:
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对象上动态查找该方法名(如果模板编写器提供无效指令,在这里处理一点错误会更好)。我们将 match 对象和参数传递给该方法,并假设该方法将适当地处理所有事情,包括移动pos指针。
现在我们已经有了面向对象的体系结构,实现委托给我们的方法实际上非常简单。include和variable指令非常简单:
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()第一个简单地查找包含的文件并插入文件内容,而第二个查找上下文字典中的变量名(在__init__方法中从json加载),如果不存在,则默认为空字符串。
处理循环的三种方法更为激烈,因为它们必须在三种方法之间共享状态。为了简单起见(我相信您很想看到这一章的结尾,我们就快到了!),我们将把它作为类本身的实例变量来处理。作为练习,你可能想考虑更好的方法来构建这一点,尤其是在阅读了接下来的三章之后。
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当遇到loopover指令时,我们不必输出任何内容,但我们必须在三个变量上设置初始状态。假设loop_list变量是从上下文字典中提取的列表。loop_index变量表示在循环的这个迭代中应该输出列表中的哪个位置,而loop_pos是存储的,这样当我们到达循环的末尾时,我们就知道跳回哪里。
loopvar指令输出loop_list变量中当前位置的值,并跳到指令末尾。注意,它不会增加循环索引,因为在循环中可以多次调用loopvar指令。
endloop指令更为复杂。确定loop_list中是否有更多元素;如果有,它只是跳回循环的开始,增加索引。否则,它将重置用于处理循环的所有变量,并跳到指令的末尾,以便引擎可以进行下一次匹配。
注意,这种特殊的循环机制非常脆弱;如果模板设计人员尝试嵌套循环或忘记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由于我们计划模板的方式,有一些奇怪的换行效果,但它按预期工作。
在本章中,我们讨论了各种各样的主题,从字符串到正则表达式,再到对象序列化,等等。现在是时候考虑这些想法如何应用到你自己的代码中了。
Python 字符串非常灵活,Python 对于基于字符串的操作来说是一个非常强大的工具。如果您在日常工作中没有进行大量的字符串处理,请尝试设计专门用于处理字符串的工具。尝试想出一些创新的东西,但是如果你陷入困境,考虑写一个 Web 日志分析器(每小时有多少请求?多少人访问超过五页?)或者一个模板工具,用其他文件的内容替换某些变量名。
在记住语法之前,要花很多时间玩弄字符串格式操作符。编写一组要传递到 format 函数的模板字符串和对象,然后查看得到的输出类型。尝试使用特殊的格式运算符,例如百分比或十六进制表示法。尝试使用 fill 和 alignment 操作符,看看它们对整数、字符串和浮点的行为有何不同。考虑写一个你自己的班级,有一个有趣的方法;我们没有详细讨论这一点,但探讨了如何定制格式。
确保您理解了bytes和str对象之间的区别。在较旧版本的 Python 中,这种区别非常复杂(没有bytes,而且str的行为类似于bytes和str,除非我们需要非 ASCII 字符,在这种情况下,有一个单独的unicode对象,类似于 Python 3 的str类。它甚至比听起来更令人困惑!)。现在更清楚了;bytes表示二进制数据,str表示字符数据。唯一棘手的部分是知道如何以及何时在两者之间转换。为了练习,尝试将文本数据写入为写入bytes而打开的文件(您必须自己对文本进行编码),然后从同一文件读取。
用bytearray做一些实验;查看它如何同时充当字节对象和列表或容器对象。尝试写入一个缓冲区,该缓冲区保存字节数组中的数据,直到数据达到一定长度后再返回。您可以通过使用time.sleep调用来模拟将数据放入缓冲区的代码,以确保数据不会太快到达。
在线学习正则表达式。再研究一下。特别是了解命名组贪婪匹配与惰性匹配,以及正则表达式标志,这三个功能我们在本章中没有介绍。有意识地决定何时不使用它们。许多人对正则表达式有非常强烈的看法,要么过度使用它们,要么根本拒绝使用它们。试着说服自己只有在合适的时候才使用它们,并弄清楚什么时候才合适。
如果您曾经编写过一个适配器,用于从文件或数据库中加载少量数据,并将其转换为对象,则考虑使用泡菜来代替。pickle 在存储大量数据时效率不高,但在加载配置或其他简单对象时却很有用。尝试用多种方式编码:使用 pickle、文本文件或小型数据库。你觉得哪一个最容易使用?
尝试使用 pickle 数据进行实验,然后修改保存数据的类,并将 pickle 加载到新类中。什么有效?什么不可以?有没有一种方法可以对类进行剧烈的更改,例如重命名一个属性或将其拆分为两个新属性,并且仍然可以从旧的 pickle 中获取数据?(提示:尝试在每个对象上放置一个私有 pickle 版本号,并在每次更改类时进行更新;然后可以在__setstate__中放置迁移路径。)
如果您进行任何 web 开发,请尝试使用 JSON 序列化程序。就我个人而言,我更喜欢只序列化标准的 JSON 可序列化对象,而不是编写自定义编码器或object_hooks,但预期效果实际上取决于前端(通常是 JavaScript)和后端代码之间的交互。
在模板引擎中创建一些接受多个或任意数量参数的新指令。您可能需要修改正则表达式或添加新的正则表达式。看看 Django 项目的在线文档,看看是否还有其他模板标记需要使用。尝试模仿他们的过滤器语法,而不是使用变量标记。当您研究了迭代和协同过程后,请重新阅读本章,看看是否可以提出一种更简洁的方法来表示相关指令之间的状态,例如循环。
在本章中,我们介绍了字符串操作、正则表达式和对象序列化。使用强大的字符串格式化系统,硬编码字符串和程序变量可以组合成可输出的字符串。区分二进制数据和文本数据很重要,bytes和str具有必须理解的特定用途。两者都是不可变的,但在处理字节时可以使用bytearray类型。
正则表达式是一个复杂的主题,但我们触及了表面。序列化 Python 数据的方法有很多;pickle 和 JSON 是最流行的两种。
在下一章中,我们将研究一种设计模式,它是 Python 编程的基础,因此它得到了特殊的语法支持:迭代器模式。