企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
[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: &lt;8.2f},对于小计,我们使用{3: &gt;7.2f}。在这两种情况下,我们都指定了一个空格作为填充字符,但是我们分别使用&lt;和&gt;符号,分别表示这些数字应该在最小的七个或八个字符串内向左或向右对齐。此外,每个浮点数应该被格式化为两位小数。 </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文档未知字符串时非常有用。以下展示每种策略是如何影响我们的示例词: ![](https://box.kancloud.cn/4adab201980173bf8ab79003176b6f9d_770x150.png) 无需传递编码字符串就可以调用`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 &copy; Today ``` 该文件包含/** &lt;指令&gt; &lt;数据&gt; **/格式的“标签”,其中数据 一个可选的单词和指令是: 包括:在此复制另一个文件的内容 变量:在这里插入变量的内容 循环:对列表变量重复循环内容 结束循环:发出循环文本结束的信号 循环变量:从循环的列表中插入一个值 该模板将根据传递到的变量呈现不同的页面 它。这些变量将从所谓的上下文文件传入。这将被编码 作为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 &copy; Today ``` 由于我们计划模板的方式,出现了一些奇怪的换行效果, 但是它像预期的那样工作。 ## 摘要 我们已经讨论了字符串操作、正则表达式和对象序列化 在本章中。硬编码字符串和程序变量可以组合成 使用强大的字符串格式化系统输出字符串。这很重要 为了区分二进制和文本数据,字节和字符串具有特定 必须理解的目的。两者都是不可变的,但是bytearray类型 可以在操作字节时使用。 正则表达式是一个复杂的话题,但我们触及了表面。有 序列化Python数据的多种方式;泡菜和JSON是最受欢迎的两种。 在下一章中,我们将看一个对Python非常重要的设计模式 被赋予特殊语法支持的编程:迭代器模式。