Python 入门指南第二版(五)
原文:
annas-archive.org/md5/4b0fd2cf0da7c8edae4b5ecfd40159bf译者:飞龙
第二部分:Python 实践篇
第十二章:整理和处理数据
如果你折磨数据足够久,自然会招认。
罗纳德·科斯
到目前为止,我们主要讨论了 Python 语言本身——其数据类型、代码结构、语法等等。本书的其余部分是关于将这些应用到现实世界问题的内容。
在本章中,你将学习许多实用的数据处理技术。有时,这被称为数据清洗,或者更商业化的ETL(提取/转换/加载)在数据库世界中。尽管编程书籍通常不会明确涵盖这个主题,程序员们花费了大量时间来将数据塑造成符合其目的的正确形式。
数据科学这一专业在过去几年变得非常流行。《哈佛商业评论》的一篇文章称数据科学家是“21 世纪最性感的职业”。如果这意味着需求量大且薪资丰厚,那就好,但也有足够的单调乏味。数据科学超越了数据库的 ETL 需求,通常涉及机器学习,以发掘人眼看不到的洞察力。
我将从基本的数据格式开始,然后介绍最有用的新数据科学工具。
数据格式大致分为两类:文本和二进制。Python 的字符串用于文本数据,本章包含了我们迄今为止跳过的字符串信息:
-
Unicode字符
-
正则表达式模式匹配。
然后,我们转向二进制数据,以及 Python 的另外两种内置类型:
-
字节用于不可变的八位值
-
字节数组用于可变的字节
文本字符串:Unicode
你在第五章中看到了 Python 字符串的基础知识。现在是深入了解 Unicode 的时候了。
Python 3 的字符串是 Unicode 字符序列,而不是字节数组。这是从 Python 2 最大的语言变化。
到目前为止,本书中的所有文本示例都是普通的 ASCII(美国标准信息交换码)。ASCII 是在六十年代定义的,在鲑鱼头发流行之前。当时的计算机大小如冰箱,稍微聪明一点。
计算机存储的基本单元是字节,它可以存储 256 个独特的值在它的八位中。出于各种原因,ASCII 只使用了七位(128 个独特的值):26 个大写字母、26 个小写字母、10 个数字、一些标点符号、一些间隔字符和一些不可打印的控制码。
不幸的是,世界上的字母比 ASCII 提供的还要多。你可以在餐馆吃热狗,但在咖啡馆永远也买不到 Gewürztraminer¹。已经尝试过许多方法将更多的字母和符号塞入八位中,有时你会看到它们。其中只有一些包括:
-
Latin-1,或者ISO 8859-1
-
Windows 代码页1252
这些字符都使用了所有的八位,但即使如此也不够,特别是在需要非欧洲语言时。Unicode 是一个持续进行的国际标准,用于定义所有世界语言的字符,以及数学和其他领域的符号。还有表情符号!
Unicode 为每个字符提供了一个唯一的编号,无论是什么平台、什么程序、什么语言。
Unicode 联盟
Unicode 代码图表页面包含所有当前定义的字符集的链接及其图像。最新版本(12.0)定义了超过 137,000 个字符,每个字符都有唯一的名称和标识号码。Python 3.8 可以处理所有这些字符。这些字符被分为称为平面的八位集合。前 256 个平面是基本多语言平面。详细信息请参阅关于Unicode 平面的维基百科页面。
Python 3 Unicode 字符串
如果您知道字符的 Unicode ID 或名称,可以在 Python 字符串中使用它。以下是一些示例:
-
\u后跟四个十六进制数字²指定 Unicode 的 256 个基本多语言平面中的一个字符。前两个数字是平面编号(00到FF),后两个数字是平面内字符的索引。平面00是老旧的 ASCII,该平面内的字符位置与 ASCII 相同。 -
对于高平面中的字符,我们需要更多的位数。Python 中这些字符的转义序列是
\U后跟八个十六进制字符;最左边的数字需要是0。 -
对于所有字符,
\N{*`name`*}允许您通过其标准名称指定它。Unicode 字符名称索引页面列出了这些名称。
Python 的 unicodedata 模块具有双向转换的功能:
-
lookup()—接受一个不区分大小写的名称,并返回一个 Unicode 字符。 -
name()—接受一个 Unicode 字符并返回其大写名称。
在下面的示例中,我们将编写一个测试函数,该函数接受一个 Python Unicode 字符,查找其名称,然后根据名称再次查找字符(应该与原始字符匹配):
>>> def unicode_test(value):
... import unicodedata
... name = unicodedata.name(value)
... value2 = unicodedata.lookup(name)
... print('value="%s", name="%s", value2="%s"' % (value, name, value2))
...
让我们尝试一些字符,首先是一个普通的 ASCII 字母:
>>> unicode_test('A')
value="A", name="LATIN CAPITAL LETTER A", value2="A"
ASCII 标点符号:
>>> unicode_test('$')
value="$", name="DOLLAR SIGN", value2="$"
Unicode 货币字符:
>>> unicode_test('\u00a2')
value="¢", name="CENT SIGN", value2="¢"
另一个 Unicode 货币字符:
>>> unicode_test('\u20ac')
value="€", name="EURO SIGN", value2="€"
你可能遇到的唯一问题是字体显示文本的限制。很少有字体包含所有 Unicode 字符的图像,可能会为缺失的字符显示一些占位符字符。例如,这是 SNOWMAN 的 Unicode 符号,类似于装饰符字体中的符号:
>>> unicode_test('\u2603')
value="☃", name="SNOWMAN", value2="☃"
假设我们想要在 Python 字符串中保存单词 café。一种方法是从文件或网站复制并粘贴它,然后希望它能正常工作:
>>> place = 'café'
>>> place
'café'
这有效是因为我从使用 UTF-8 编码的源复制和粘贴了文本。
如何指定最后的 é 字符?如果你查看 E 的字符索引,你会看到名称 E WITH ACUTE, LATIN SMALL LETTER 具有值 00E9。让我们用刚才玩过的 name() 和 lookup() 函数来检查。首先给出获取名称的代码:
>>> unicodedata.name('\u00e9')
'LATIN SMALL LETTER E WITH ACUTE'
接下来,给出查找代码的名称:
>>> unicodedata.lookup('E WITH ACUTE, LATIN SMALL LETTER')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: "undefined character name 'E WITH ACUTE, LATIN SMALL LETTER'"
注意
Unicode 字符名称索引页上列出的名称已经重新格式化,使其在显示时可以很好地排序。要将它们转换为真实的 Unicode 名称(Python 使用的名称),去掉逗号并将逗号后面的部分移动到开头。因此,将 E WITH ACUTE, LATIN SMALL LETTER 改为 LATIN SMALL LETTER E WITH ACUTE:
>>> unicodedata.lookup('LATIN SMALL LETTER E WITH ACUTE')
'é'
现在我们可以通过代码或名称指定字符串 café:
>>> place = 'caf\u00e9'
>>> place
'café'
>>> place = 'caf\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> place
'café'
在前面的代码片段中,我们直接在字符串中插入了 é,但我们也可以通过附加构建字符串:
>>> u_umlaut = '\N{LATIN SMALL LETTER U WITH DIAERESIS}'
>>> u_umlaut
'ü'
>>> drink = 'Gew' + u_umlaut + 'rztraminer'
>>> print('Now I can finally have my', drink, 'in a', place)
Now I can finally have my Gewürztraminer in a café
字符串 len() 函数计算 Unicode 字符 数量,而不是字节数:
>>> len('$')
1
>>> len('\U0001f47b')
1
注意
如果你知道 Unicode 的数值 ID,你可以使用标准的 ord() 和 chr() 函数快速转换整数 ID 和单字符 Unicode 字符串:
>>> chr(233)
'é'
>>> chr(0xe9)
'é'
>>> chr(0x1fc6)
'ῆ'
UTF-8
在正常的字符串处理中,你不需要担心 Python 如何存储每个 Unicode 字符。
但是,当你与外界交换数据时,你需要一些东西:
-
编码 字符字符串为字节的方法
-
解码 字节到字符字符串的方法
如果 Unicode 中的字符少于 65,536 个,我们可以将每个 Unicode 字符 ID 塞入两个字节中。不幸的是,字符太多了。我们可以将每个 ID 编码为四个字节,但这将使常见文本字符串的内存和磁盘存储空间需求增加四倍。
Ken Thompson 和 Rob Pike,Unix 开发者熟悉的名字,设计了一夜之间在新泽西餐馆的餐垫上的 UTF-8 动态编码方案。它每个 Unicode 字符使用一到四个字节:
-
ASCII 占一个字节
-
大多数拉丁衍生(但不包括西里尔语)语言需要两个字节
-
基本多语言平面的其余部分需要三个字节
-
其余部分包括一些亚洲语言和符号需要四个字节
UTF-8 是 Python、Linux 和 HTML 中的标准文本编码。它快速、全面且运行良好。如果你在代码中始终使用 UTF-8 编码,生活将比试图在各种编码之间跳转要容易得多。
注意
如果你从网页等其他源复制粘贴创建 Python 字符串,请确保源以 UTF-8 格式编码。经常看到将以 Latin-1 或 Windows 1252 编码的文本复制到 Python 字符串中,这将导致后来出现无效字节序列的异常。
编码
你可以将字符串 编码 为 字节。字符串 encode() 函数的第一个参数是编码名称。选择包括 Table 12-1 中的那些。
表 12-1. 编码
| 编码名称 | 描述 |
|---|---|
'ascii' | 七比特 ASCII 编码 |
'utf-8' | 八位变长编码,几乎总是你想要使用的 |
'latin-1' | 也称为 ISO 8859-1 |
'cp-1252' | 常见的 Windows 编码 |
'unicode-escape' | Python Unicode 文本格式,\uxxxx 或 \Uxxxxxxxx |
你可以将任何东西编码为 UTF-8。让我们将 Unicode 字符串'\u2603'赋给名称snowman:
>>> snowman = '\u2603'
snowman是一个 Python Unicode 字符串,只有一个字符,无论内部存储它需要多少字节:
>>> len(snowman)
1
接下来,让我们将这个 Unicode 字符编码为一个字节序列:
>>> ds = snowman.encode('utf-8')
正如我之前提到的,UTF-8 是一种变长编码。在这种情况下,它用三个字节来编码单个snowman Unicode 字符:
>>> len(ds)
3
>>> ds
b'\xe2\x98\x83'
现在,len()返回字节数(3),因为ds是一个bytes变量。
你可以使用除 UTF-8 之外的其他编码,但如果 Unicode 字符串不能被处理,你会得到错误。例如,如果你使用ascii编码,除非你的 Unicode 字符恰好是有效的 ASCII 字符,否则会失败:
>>> ds = snowman.encode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeEncodeError: 'ascii' codec can't encode character '\u2603'
in position 0: ordinal not in range(128)
encode()函数接受第二个参数,帮助你避免编码异常。它的默认值,在前面的例子中你可以看到,是'strict';如果遇到非 ASCII 字符,它会引发一个UnicodeEncodeError。还有其他编码方式。使用'ignore'来丢弃任何无法编码的内容:
>>> snowman.encode('ascii', 'ignore')
b''
使用'replace'来用?替换未知字符:
>>> snowman.encode('ascii', 'replace')
b'?'
使用'backslashreplace'来生成一个 Python Unicode 字符字符串,比如unicode-escape:
>>> snowman.encode('ascii', 'backslashreplace')
b'\\u2603'
如果你需要一个 Unicode 转义序列的可打印版本,你可以使用这个方法。
使用'xmlcharrefreplace'来生成 HTML 安全字符串:
>>> snowman.encode('ascii', 'xmlcharrefreplace')
b'☃'
我在“HTML 实体”中提供了更多 HTML 转换的细节。
解码
我们将字节字符串解码为 Unicode 文本字符串。每当我们从外部来源(文件、数据库、网站、网络 API 等)获取文本时,它都会被编码为字节字符串。棘手的部分是知道实际使用了哪种编码方式,这样我们才能逆向操作并获取 Unicode 字符串。
问题在于字节字符串本身没有说明使用了哪种编码。我之前提到过从网站复制粘贴的危险。你可能访问过一些奇怪字符的网站,本应是普通的 ASCII 字符。
让我们创建一个名为place的 Unicode 字符串,其值为'café':
>>> place = 'caf\u00e9'
>>> place
'café'
>>> type(place)
<class 'str'>
用 UTF-8 格式编码,存入一个名为place_bytes的bytes变量:
>>> place_bytes = place.encode('utf-8')
>>> place_bytes
b'caf\xc3\xa9'
>>> type(place_bytes)
<class 'bytes'>
注意place_bytes有五个字节。前三个与 ASCII 相同(UTF-8 的优势),最后两个编码了'é'。现在让我们将该字节字符串解码回 Unicode 字符串:
>>> place2 = place_bytes.decode('utf-8')
>>> place2
'café'
这个方法的原因是我们编码为 UTF-8 并解码为 UTF-8。如果我们告诉它从其他编码解码会怎样呢?
>>> place3 = place_bytes.decode('ascii')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 3:
ordinal not in range(128)
ASCII 解码器因为字节值0xc3在 ASCII 中是非法的而抛出了异常。有些 8 位字符集编码允许 128(十六进制80)到 255(十六进制FF)之间的值合法,但与 UTF-8 不同。
>>> place4 = place_bytes.decode('latin-1')
>>> place4
'café'
>>> place5 = place_bytes.decode('windows-1252')
>>> place5
'café'
唔。
这个故事的教训是:只要可能,请使用 UTF-8 编码。它适用,被到处支持,可以表示每个 Unicode 字符,并且快速解码和编码。
注意
尽管您可以指定任何 Unicode 字符,这并不意味着您的计算机将显示所有这些字符。这取决于您使用的字体,该字体可能对许多字符显示空白或填充图像。苹果为 Unicode 联盟创建了最后的应急字体,并在其自己的操作系统中使用它。这个Wikipedia 页面有更多细节。另一种包含从\u0000到\uffff以及更多字符的字体是Unifont。
HTML 实体
Python 3.4 增加了另一种转换 Unicode 的方法,但是使用 HTML 字符实体。³ 这可能比查找 Unicode 名称更容易,特别是在您在网络上工作时:
>>> import html
>>> html.unescape("è")
'è'
这种转换也适用于编号实体,十进制或十六进制:
>>> import html
>>> html.unescape("é")
'é'
>>> html.unescape("é")
'é'
您甚至可以将命名实体转换导入为字典并自行进行转换。删除字典键的初始'&'(您也可以删除最后的;,但似乎两种方式都可以工作):
>>> from html.entities import html5
>>> html5["egrave"]
'è'
>>> html5["egrave;"]
'è'
要从单个 Python Unicode 字符向 HTML 实体名称的另一方向转换,请首先使用ord()获取字符的十进制值:
>>> import html
>>> char = '\u00e9'
>>> dec_value = ord(char)
>>> html.entities.codepoint2name[dec_value]
'eacute'
对于超过一个字符的 Unicode 字符串,请使用这两步转换:
>>> place = 'caf\u00e9'
>>> byte_value = place.encode('ascii', 'xmlcharrefreplace')
>>> byte_value
b'café'
>>> byte_value.decode()
'café'
表达式place.encode('ascii', 'xmlcharrefreplace')返回 ASCII 字符,但是作为类型bytes(因为它是*编码的)。需要以下byte_value.decode()来将byte_value转换为 HTML 兼容字符串。
标准化
一些 Unicode 字符可以用多种 Unicode 编码表示。它们看起来一样,但由于具有不同的内部字节序列,它们不能进行比较。例如,在'café'中,急性重音'é'可以用多种方式制作单个字符'é':
>>> eacute1 = 'é' # UTF-8, pasted
>>> eacute2 = '\u00e9' # Unicode code point
>>> eacute3 = \ # Unicode name
... '\N{LATIN SMALL LETTER E WITH ACUTE}'
>>> eacute4 = chr(233) # decimal byte value
>>> eacute5 = chr(0xe9) # hex byte value
>>> eacute1, eacute2, eacute3, eacute4, eacute5
('é', 'é', 'é', 'é', 'é')
>>> eacute1 == eacute2 == eacute3 == eacute4 == eacute5
True
尝试几个健全性检查:
>>> import unicodedata
>>> unicodedata.name(eacute1)
'LATIN SMALL LETTER E WITH ACUTE'
>>> ord(eacute1) # as a decimal integer
233
>>> 0xe9 # Unicode hex integer
233
现在让我们通过将一个普通的e与一个重音符号结合来制作一个带重音的e:
>>> eacute_combined1 = "e\u0301"
>>> eacute_combined2 = "e\N{COMBINING ACUTE ACCENT}"
>>> eacute_combined3 = "e" + "\u0301"
>>> eacute_combined1, eacute_combined2, eacute_combined3
('é', 'é', 'é'))
>>> eacute_combined1 == eacute_combined2 == eacute_combined3
True
>>> len(eacute_combined1)
2
我们用两个字符构建了一个 Unicode 字符,它看起来与原始的'é'相同。但正如他们在芝麻街上所说的那样,其中一个与其他不同:
>>> eacute1 == eacute_combined1
False
如果您有来自不同来源的两个不同的 Unicode 文本字符串,一个使用eacute1,另一个使用eacute_combined1,它们看起来相同,但是神秘地不起作用。
您可以使用unicodedata模块中的normalize()函数修复这个问题:
>>> import unicodedata
>>> eacute_normalized = unicodedata.normalize('NFC', eacute_combined1)
>>> len(eacute_normalized)
1
>>> eacute_normalized == eacute1
True
>>> unicodedata.name(eacute_normalized)
'LATIN SMALL LETTER E WITH ACUTE'
'NFC'的意思是组合的正常形式。
更多信息
如果您想了解更多关于 Unicode 的信息,这些链接特别有帮助:
文本字符串:正则表达式
第五章讨论了简单的字符串操作。掌握了这些基础知识后,你可能已经在命令行上使用了简单的“通配符”模式,比如 UNIX 命令 ls *.py,意思是列出所有以 .py 结尾的文件名。
是时候通过使用正则表达式来探索更复杂的模式匹配了。这些功能在标准模块 re 中提供。你定义一个要匹配的字符串模式,以及要匹配的源字符串。对于简单的匹配,用法如下:
>>> import re
>>> result = re.match('You', 'Young Frankenstein')
在这里,'You' 是我们要查找的模式,'Young Frankenstein' 是源(我们要搜索的字符串)。match() 检查源是否以模式开头。
对于更复杂的匹配,你可以先编译你的模式以加快后续的匹配速度:
>>> import re
>>> youpattern = re.compile('You')
然后,你可以对编译后的模式执行匹配:
>>> import re
>>> result = youpattern.match('Young Frankenstein')
注意
因为这是一个常见的 Python 陷阱,我在这里再次强调:match() 只匹配从源的开头开始的模式。search() 则可以在源的任何位置匹配模式。
match() 不是比较模式和源的唯一方法。以下是你可以使用的几种其他方法(我们在下面的各节中讨论每一种方法):
-
search()如果有的话返回第一个匹配项。 -
findall()返回所有非重叠匹配项的列表(如果有的话)。 -
split()在源中匹配模式并返回字符串片段列表。 -
sub()还需要另一个替换参数,并将源中与模式匹配的所有部分更改为替换。
注意
这里大多数正则表达式示例都使用 ASCII,但 Python 的字符串函数,包括正则表达式,可以处理任何 Python 字符串和任何 Unicode 字符。
使用 match() 找到确切的起始匹配
字符串 'Young Frankenstein' 是否以 'You' 开头?以下是带有注释的代码:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('You', source) # match starts at the beginning of source
>>> if m: # match returns an object; do this to see what matched
... print(m.group())
...
You
>>> m = re.match('^You', source) # start anchor does the same
>>> if m:
... print(m.group())
...
You
'Frank' 怎么样?
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('Frank', source)
>>> if m:
... print(m.group())
...
这次,match() 没有返回任何内容,因此 if 语句没有运行 print 语句。
正如我在 “新功能:我是海象” 中提到的,在 Python 3.8 中,你可以使用所谓的海象操作符简化这个例子:
>>> import re
>>> source = 'Young Frankenstein'
>>> if m := re.match('Frank', source):
... print(m.group())
...
现在让我们使用 search() 来查看 'Frank' 是否出现在源字符串中:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m:
... print(m.group())
...
Frank
让我们改变模式,再次尝试使用 match() 进行起始匹配:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.match('.*Frank', source)
>>> if m: # match returns an object
... print(m.group())
...
Young Frank
这里简要解释了我们新的 '.*Frank' 模式的工作原理:
-
.表示任何单个字符。 -
*表示前一个内容的零个或多个。.*在一起表示任意数量的字符(甚至是零个)。 -
Frank是我们想要匹配的短语,某个地方。
match() 返回与 .*Frank 匹配的字符串:'Young Frank'。
使用 search() 找到第一个匹配项
你可以使用 search() 在字符串 'Young Frankenstein' 中找到模式 'Frank' 的任何位置,而不需要使用 .* 通配符:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.search('Frank', source)
>>> if m: # search returns an object
... print(m.group())
...
Frank
使用 findall() 查找所有匹配项
前面的例子只查找了一个匹配。但是如果你想知道字符串中单字母 'n' 的实例数量呢?
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n', source)
>>> m # findall returns a list
['n', 'n', 'n', 'n']
>>> print('Found', len(m), 'matches')
Found 4 matches
后面跟着任何字符的 'n' 是怎样的?
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.', source)
>>> m
['ng', 'nk', 'ns']
注意它没有匹配最后的 'n'。我们需要说 'n' 后面的字符是可选的,用 ?:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.findall('n.?', source)
>>> m
['ng', 'nk', 'ns', 'n']
使用 split() 在匹配处分割
下一个示例展示了如何通过模式而不是简单字符串(正常字符串 split() 方法会执行的方式)将字符串分割成列表:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.split('n', source)
>>> m # split returns a list
['You', 'g Fra', 'ke', 'stei', '']
使用 sub() 替换匹配项
这类似于字符串 replace() 方法,但用于模式而不是字面字符串:
>>> import re
>>> source = 'Young Frankenstein'
>>> m = re.sub('n', '?', source)
>>> m # sub returns a string
'You?g Fra?ke?stei?'
模式:特殊字符
许多正则表达式的描述从如何定义它们的所有细节开始。我认为这是一个错误。正则表达式是一个不那么小的语言,有太多细节无法一次掌握。它们使用了很多标点符号,看起来像卡通人物在咒骂。
掌握了这些表达式 (match()、search()、findall() 和 sub())之后,让我们深入了解如何构建它们的细节。你制作的模式适用于这些函数中的任何一个。
你已经了解了基础知识:
-
匹配所有非特殊字符的文字
-
除
\n外的任何单个字符用. -
任意数量的前一个字符(包括零)用
* -
前一个字符的可选(零次或一次)用
?
首先,特殊字符显示在 表 12-2 中。
表 12-2. 特殊字符
| 模式 | 匹配项 |
|---|---|
\d | 单个数字 |
\D | 单个非数字字符 |
\w | 字母数字字符 |
\W | 非字母数字字符 |
\s | 空白字符 |
\S | 非空白字符 |
\b | 单词边界(在 \w 和 \W 之间,顺序不限) |
\B | 非单词边界 |
Python 的 string 模块有预定义的字符串常量,我们可以用它们进行测试。让我们使用 printable,其中包含 100 个可打印的 ASCII 字符,包括大小写字母、数字、空格字符和标点符号:
>>> import string
>>> printable = string.printable
>>> len(printable)
100
>>> printable[0:50]
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'
>>> printable[50:]
'OPQRSTUVWXYZ!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~ \t\n\r\x0b\x0c'
printable 中哪些字符是数字?
>>> re.findall('\d', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
哪些字符是数字、字母或下划线?
>>> re.findall('\w', printable)
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b',
'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '_']
哪些是空格?
>>> re.findall('\s', printable)
[' ', '\t', '\n', '\r', '\x0b', '\x0c']
按顺序,这些是:普通空格、制表符、换行符、回车符、垂直制表符和换页符。
正则表达式不仅限于 ASCII。\d 将匹配任何 Unicode 所谓的数字,而不仅仅是 ASCII 字符 '0' 到 '9'。让我们从 FileFormat.info 添加两个非 ASCII 小写字母:
在这个测试中,我们将加入以下内容:
-
三个 ASCII 字母
-
三个标点符号不应该与
\w匹配 -
Unicode 带抑音的拉丁小写字母 E (\u00ea)
-
Unicode 带抑音的拉丁小写字母 E (\u0115)
>>> x = 'abc' + '-/*' + '\u00ea' + '\u0115'
如预期,这个模式仅找到了字母:
>>> re.findall('\w', x)
['a', 'b', 'c', 'ê', 'ĕ']
模式:使用限定符
现在让我们制作“标点披萨”,使用正则表达式的主要模式限定符,这些限定符在 表 12-3 中介绍。
在表中,expr和其他斜体字表示任何有效的正则表达式。
表 12-3。模式说明符
| 模式 | 匹配 |
|---|---|
abc | 字面abc |
( expr ) | expr |
expr1 | expr2 | expr1 或 expr2 |
. | 除\n外的任意字符 |
^ | 源字符串的开头 |
$ | 源字符串的结尾 |
prev ? | 零或一个prev |
prev * | 零或多个prev,尽可能多地匹配 |
prev *? | 零或多个prev,尽可能少地匹配 |
prev + | 一个或多个prev,尽可能多地匹配 |
prev +? | 一个或多个prev,尽可能少地匹配 |
prev { m } | m个连续的prev |
prev { m, n } | m到n个连续的prev,尽可能多地匹配 |
prev { m, n }? | m到n个连续的prev,尽可能少地匹配 |
[ abc ] | a或b或c(等同于a|b|c) |
[^ abc ] | 非(a或b或c) |
prev (?= next ) | 若紧随其后则prev |
prev (?! next ) | 若不紧随其后则prev |
(?<= prev ) next | 若之前有prev则next |
(?<! prev ) next | 若不紧随其前则next |
当试图阅读这些示例时,你的眼睛可能永久地交叉了。首先,让我们定义我们的源字符串:
>>> source = '''I wish I may, I wish I might
... Have a dish of fish tonight.'''
现在我们应用不同的正则表达式模式字符串来尝试在source字符串中匹配某些内容。
注意
在以下示例中,我使用普通的引号字符串表示模式。在本节稍后,我将展示如何使用原始模式字符串(在初始引号前加上r)来避免 Python 正常字符串转义与正则表达式转义之间的冲突。因此,为了更安全,所有以下示例中的第一个参数实际上应该是原始字符串。
首先,在任意位置找到wish:
>>> re.findall('wish', source)
['wish', 'wish']
接下来,在任意位置找到wish或fish:
>>> re.findall('wish|fish', source)
['wish', 'wish', 'fish']
查找开头的wish:
>>> re.findall('^wish', source)
[]
查找开头的I wish:
>>> re.findall('^I wish', source)
['I wish']
查找结尾的fish:
>>> re.findall('fish$', source)
[]
最后,在结尾找到fish tonight.:
>>> re.findall('fish tonight.$', source)
['fish tonight.']
字符^和$称为锚点:^锚定搜索到搜索字符串的开始,而$锚定到结尾。. $匹配行尾的任意字符,包括句号,所以它起作用了。为了更精确,我们应该转义句点以确实匹配它:
>>> re.findall('fish tonight\.$', source)
['fish tonight.']
从找到w或f后面跟着ish开始:
>>> re.findall('[wf]ish', source)
['wish', 'wish', 'fish']
查找一个或多个w、s或h的连续序列:
>>> re.findall('[wsh]+', source)
['w', 'sh', 'w', 'sh', 'h', 'sh', 'sh', 'h']
找到以非字母数字字符跟随的ght:
>>> re.findall('ght\W', source)
['ght\n', 'ght.']
找到以I开头的wish:
>>> re.findall('I (?=wish)', source)
['I ', 'I ']
最后,wish之前有I:
>>> re.findall('(?<=I) wish', source)
[' wish', ' wish']
我之前提到过,有几种情况下正则表达式模式规则与 Python 字符串规则相冲突。以下模式应匹配以fish开头的任何单词:
>>> re.findall('\bfish', source)
[]
为什么不这样做呢?如第五章中所述,Python 为字符串使用了一些特殊的转义字符。例如,\b 在字符串中表示退格,但在正则表达式的迷你语言中表示单词的开头。通过在定义正则表达式字符串时始终在其前加上 r 字符,可以避免意外使用转义字符,这样将禁用 Python 转义字符,如下所示:
>>> re.findall(r'\bfish', source)
['fish']
模式:指定 match() 输出
在使用 match() 或 search() 时,所有匹配项都作为结果对象 m 的 m.group() 返回。如果将模式括在括号中,则匹配将保存到自己的组中,并作为 m.groups() 的元组可用,如下所示:
>>> m = re.search(r'(. dish\b).*(\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
如果使用此模式 (?P< name > expr ),它将匹配 expr,并将匹配保存在组 name 中:
>>> m = re.search(r'(?P<DISH>. dish\b).*(?P<FISH>\bfish)', source)
>>> m.group()
'a dish of fish'
>>> m.groups()
('a dish', 'fish')
>>> m.group('DISH')
'a dish'
>>> m.group('FISH')
'fish'
二进制数据
文本数据可能会有挑战,但二进制数据可能会更加有趣。您需要了解诸如字节顺序(计算机处理器如何将数据分解为字节)和整数的符号位等概念。您可能需要深入了解二进制文件格式或网络数据包,以提取甚至更改数据。本节向您展示了在 Python 中进行二进制数据处理的基础知识。
bytes 和 bytearray
Python 3 引入了以下八位整数序列,可能值为 0 到 255,有两种类型:
-
bytes 不可变,类似于字节元组
-
bytearray 可变,类似于字节列表
以名为 blist 的列表开始,下一个示例创建了名为 the_bytes 的 bytes 变量和名为 the_byte_array 的 bytearray 变量:
>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes
b'\x01\x02\x03\xff'
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
注意
bytes 值的表示以 b 和引号字符开头,后跟诸如 \x02 或 ASCII 字符的十六进制序列,并以匹配的引号字符结束。Python 将十六进制序列或 ASCII 字符转换为小整数,但显示也有效的 ASCII 编码的字节值作为 ASCII 字符:
>>> b'\x61'
b'a'
>>> b'\x01abc\xff'
b'\x01abc\xff'
下一个示例演示了您不能更改 bytes 变量:
>>> blist = [1, 2, 3, 255]
>>> the_bytes = bytes(blist)
>>> the_bytes[1] = 127
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'bytes' object does not support item assignment
但 bytearray 变量温和且可变:
>>> blist = [1, 2, 3, 255]
>>> the_byte_array = bytearray(blist)
>>> the_byte_array
bytearray(b'\x01\x02\x03\xff')
>>> the_byte_array[1] = 127
>>> the_byte_array
bytearray(b'\x01\x7f\x03\xff')
这些操作会生成一个包含 0 到 255 的 256 元素结果:
>>> the_bytes = bytes(range(0, 256))
>>> the_byte_array = bytearray(range(0, 256))
在打印 bytes 或 bytearray 数据时,Python 使用 \xxx 表示不可打印字节及其 ASCII 等效字符,对于可打印字符则显示其 ASCII 值(以及一些常见的转义字符,例如 \n 而非 \x0a)。以下是手动重新格式化以显示每行 16 个字节的 the_bytes 的打印表示:
>>> the_bytes
b'\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f
\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f
!"#$%&\'()*+,-./
0123456789:;<=>?
@ABCDEFGHIJKLMNO
PQRSTUVWXYZ[\\]^_
`abcdefghijklmno
pqrstuvwxyz{|}~\x7f
\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f
\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f
\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf
\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf
\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf
\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf
\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef
\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff'
这可能会令人困惑,因为它们是字节(小整数),而不是字符。
使用 struct 转换二进制数据
正如您所见,Python 有许多用于操作文本的工具。用于二进制数据的工具则较少。标准库包含了处理类似于 C 和 C++ 中结构体的数据的 struct 模块。使用 struct,您可以将二进制数据转换为 Python 数据结构,反之亦然。
我们来看看如何处理来自 PNG 文件的数据——一种常见的图像格式,通常与 GIF 和 JPEG 文件一起出现。我们将编写一个小程序,从一些 PNG 数据中提取图像的宽度和高度。
我们将使用奥莱利的商标——在 图 12-1 中展示的小眼睛猫熊。
图 12-1. 奥莱利猫熊
此图像的 PNG 文件可在 维基百科 上找到。在 第 14 章 我才会介绍如何读取文件,因此我下载了这个文件,编写了一个小程序将其值作为字节打印出来,并只在一个名为 data 的 Python bytes 变量中键入了前 30 个字节的值,用于接下来的示例中。(PNG 格式规范指出宽度和高度存储在前 24 字节中,因此我们现在不需要更多。)
>>> import struct
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
... b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> if data[:8] == valid_png_header:
... width, height = struct.unpack('>LL', data[16:24])
... print('Valid PNG, width', width, 'height', height)
... else:
... print('Not a valid PNG')
...
Valid PNG, width 154 height 141
以下是此代码的功能:
-
data包含来自 PNG 文件的前 30 个字节。为了适应页面,我用+和续行符(\)连接了两个字节字符串。 -
valid_png_header包含标记有效 PNG 文件起始的八字节序列。 -
width从第 16 至 19 字节提取,height从第 20 至 23 字节提取。
>LL 是格式字符串,指示 unpack() 如何解释其输入字节序列并将其组装成 Python 数据类型。以下是详细说明:
-
>表示整数以 大端 格式存储。 -
每个
L指定一个四字节无符号长整数。
你可以直接检查每个四字节值:
>>> data[16:20]
b'\x00\x00\x00\x9a'
>>> data[20:24]0x9a
b'\x00\x00\x00\x8d'
大端整数将最重要的字节放在左边。因为宽度和高度都小于 255,它们适合每个序列的最后一个字节。你可以验证这些十六进制值是否与预期的十进制值匹配:
>>> 0x9a
154
>>> 0x8d
141
当你想反向操作并将 Python 数据转换为字节时,请使用 struct pack() 函数:
>>> import struct
>>> struct.pack('>L', 154)
b'\x00\x00\x00\x9a'
>>> struct.pack('>L', 141)
b'\x00\x00\x00\x8d'
表 12-4 和 12-5 显示了 pack() 和 unpack() 的格式说明符。
字节顺序说明符在格式字符串中优先。
表 12-4. 字节顺序说明符
| 格式说明符 | 字节顺序 |
|---|---|
< | 小端 |
> | 大端 |
表 12-5. 格式说明符
| 格式说明符 | 描述 | 字节 |
|---|---|---|
x | 跳过一个字节 | 1 |
b | 有符号字节 | 1 |
B | 无符号字节 | 1 |
h | 有符号短整数 | 2 |
H | 无符号短整数 | 2 |
i | 有符号整数 | 4 |
I | 无符号整数 | 4 |
l | 有符号长整数 | 4 |
L | 无符号长整数 | 4 |
Q | 无符号长长整数 | 8 |
f | 单精度浮点数 | 4 |
d | 双精度浮点数 | 8 |
p | count 和字符 | 1 + count |
s | 字符 | count |
类型说明符跟在字节顺序字符之后。任何说明符前都可以加一个数字,表示 count;5B 等同于 BBBBB。
你可以使用 count 前缀代替 >LL:
>>> struct.unpack('>2L', data[16:24])
(154, 141)
我们使用切片 data[16:24] 直接抓取感兴趣的字节。我们也可以使用 x 标识符来跳过不感兴趣的部分:
>>> struct.unpack('>16x2L6x', data)
(154, 141)
这意味着:
-
使用大端整数格式 (
>) -
跳过 16 个字节 (
16x) -
读取八个字节——两个无符号长整数 (
2L) -
跳过最后六个字节 (
6x)
其他二进制数据工具
一些第三方开源软件包提供了以下更具声明性的方法来定义和提取二进制数据:
附录 B 中详细介绍了如何下载和安装外部包,例如这些。在下一个示例中,您需要安装 construct。这是您需要做的全部工作:
$ pip install construct
以下是如何通过使用 construct 从我们的 data 字节串中提取 PNG 尺寸的方法:
>>> from construct import Struct, Magic, UBInt32, Const, String
>>> # adapted from code at https://github.com/construct
>>> fmt = Struct('png',
... Magic(b'\x89PNG\r\n\x1a\n'),
... UBInt32('length'),
... Const(String('type', 4), b'IHDR'),
... UBInt32('width'),
... UBInt32('height')
... )
>>> data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR' + \
... b'\x00\x00\x00\x9a\x00\x00\x00\x8d\x08\x02\x00\x00\x00\xc0'
>>> result = fmt.parse(data)
>>> print(result)
Container:
length = 13
type = b'IHDR'
width = 154
height = 141
>>> print(result.width, result.height)
154, 141
使用 binascii() 转换字节/字符串
标准的 binascii 模块具有将二进制数据与各种字符串表示形式(十六进制(基数 16)、Base64、uuencoded 等)之间转换的函数。例如,在下一段代码中,让我们将那八字节的 PNG 头部打印为一系列十六进制值,而不是 Python 用来显示 bytes 变量的混合 ASCII 和 \x xx 转义的方式:
>>> import binascii
>>> valid_png_header = b'\x89PNG\r\n\x1a\n'
>>> print(binascii.hexlify(valid_png_header))
b'89504e470d0a1a0a'
嘿,这个东西也可以反向操作:
>>> print(binascii.unhexlify(b'89504e470d0a1a0a'))
b'\x89PNG\r\n\x1a\n'
位运算符
Python 提供了类似 C 语言的位级整数操作符。表 12-6 总结了这些操作符,并包括对整数变量 x(十进制 5,二进制 0b0101)和 y(十进制 1,二进制 0b0001)的示例。
表 12-6. 位级整数运算符
| 操作符 | 描述 | 示例 | 十进制结果 | 二进制结果 |
|---|---|---|---|---|
& | 与 | x & y | 1 | 0b0001 |
| | 或 | x | y | 5 | 0b0101 |
^ | 异或 | x ^ y | 4 | 0b0100 |
~ | 反转位 | ~x | -6 | 二进制表示取决于整数大小 |
<< | 左移 | x << 1 | 10 | 0b1010 |
>> | 右移 | x >> 1 | 2 | 0b0010 |
这些操作符的工作方式类似于 第 8 章 中的集合操作符。& 操作符返回两个参数中相同的位,| 返回两个参数中设置的位。^ 操作符返回一个参数中的位,而不是两者都有的位。~ 操作符反转其单个参数中的所有位;这也反转了符号,因为整数的最高位在 二进制补码 算术中表示其符号(1 = 负数),这种算法在所有现代计算机中使用。<< 和 >> 操作符只是将位向左或向右移动。向左移动一位与乘以二相同,向右移动相当于除以二。
一个珠宝类比
Unicode 字符串就像魅力手链,而字节则像串珠。
即将到来
接下来是另一个实用章节:如何处理日期和时间。
待办事项
12.1 创建一个名为mystery的 Unicode 字符串,并将其赋值为'\U0001f984'。打印mystery及其 Unicode 名称。
12.2 使用 UTF-8 对mystery进行编码,并将结果存入名为pop_bytes的bytes变量中。打印pop_bytes。
12.3 使用 UTF-8 将pop_bytes解码为字符串变量pop_string。打印pop_string。pop_string等于mystery吗?
12.4 当您处理文本时,正则表达式非常方便。我们将以多种方式应用它们到我们特色的文本样本中。这是一首名为“Ode on the Mammoth Cheese”的诗,由詹姆斯·麦金泰尔于 1866 年写作,致敬于一个重七千磅的奶酪,在安大略省制作并发送国际巡回展。如果您不想全部输入,请使用您喜爱的搜索引擎并将单词剪切并粘贴到 Python 程序中,或者直接从Project Gutenberg获取。将文本字符串命名为mammoth。
例子 12-1. mammoth.txt
We have seen thee, queen of cheese,
Lying quietly at your ease,
Gently fanned by evening breeze,
Thy fair form no flies dare seize.
All gaily dressed soon you'll go
To the great Provincial show,
To be admired by many a beau
In the city of Toronto.
Cows numerous as a swarm of bees,
Or as the leaves upon the trees,
It did require to make thee please,
And stand unrivalled, queen of cheese.
May you not receive a scar as
We have heard that Mr. Harris
Intends to send you off as far as
The great world's show at Paris.
Of the youth beware of these,
For some of them might rudely squeeze
And bite your cheek, then songs or glees
We could not sing, oh! queen of cheese.
We'rt thou suspended from balloon,
You'd cast a shade even at noon,
Folks would think it was the moon
About to fall and crush them soon.
12.5 导入re模块以使用 Python 的正则表达式函数。使用re.findall()打印所有以c开头的单词。
12.6 找出所有以c开头的四字单词。
12.7 找出所有以r结尾的单词。
12.8 找出所有包含恰好三个连续元音字母的单词。
12.9 使用unhexlify将这个十六进制字符串(从两个字符串组合成一个以适应页面)转换为名为gif的bytes变量:
'47494638396101000100800000000000ffffff21f9' +
'0401000000002c000000000100010000020144003b'
12.10 gif中的字节定义了一个像素的透明 GIF 文件,这是最常见的图形文件格式之一。合法的 GIF 以 ASCII 字符GIF89a开头。gif是否符合这个规范?
12.11 GIF 的像素宽度是从字节偏移量 6 开始的 16 位小端整数,高度也是相同大小,从偏移量 8 开始。提取并打印这些值以供gif使用。它们都是1吗?
¹ 这种酒在德国有一个分音符号,但在去法国的途中在阿尔萨斯地区失去了它。
² 基数 16,由字符0-9和A-F指定。
³ 参见 HTML5 命名字符引用图表。
第十三章:日历和钟表
“一!”在钟楼塔上的钟敲响,
前不久不过六十分钟前
十二点的钟声响起。
Frederick B. Needham,《钟的轮回》
我有日历但我从未准时过。
玛丽莲·梦露
程序员花费了令人惊讶的精力处理日期和时间。让我们谈谈他们遇到的一些问题,然后介绍一些最佳实践和技巧,使情况稍微不那么混乱。
日期可以用多种方式表示——实际上太多了。即使在使用罗马日历的英语中,你也会看到简单日期的许多变体:
-
July 21 1987
-
21 Jul 1987
-
21/7/1987
-
7/21/1987
在其他问题中,日期表示可能会产生歧义。在前面的示例中,很容易确定 7 是月份,21 是月中的日期,因为月份不会达到 21 日。但是 1/6/2012 是指 1 月 6 日还是 6 月 1 日呢?
月份名称在罗马日历中在不同的语言中会有所变化。甚至年份和月份在其他文化中的定义也可能有所不同。
时间也有它们自己的烦恼来源,特别是由于时区和夏令时。如果你看一下时区地图,这些区域遵循的是政治和历史边界,而不是每 15 度(360 度 / 24)经度上的清晰分界线。并且各国在一年中开始和结束夏令时的日期也不同。南半球国家在北半球朋友结束夏令时时,他们自己的时钟也在前进,反之亦然。
Python 的标准库包括许多日期和时间模块,包括:datetime、time、calendar、dateutil 等。它们有些重叠,并且有些令人困惑。
闰年
闰年是时间的一个特殊问题。你可能知道每四年是一个闰年(夏季奥运会和美国总统选举)。你还知道每 100 年不是闰年,但每 400 年是吗?下面是测试各年份是否为闰年的代码:
>>> import calendar
>>> calendar.isleap(1900)
False
>>> calendar.isleap(1996)
True
>>> calendar.isleap(1999)
False
>>> calendar.isleap(2000)
True
>>> calendar.isleap(2002)
False
>>> calendar.isleap(2004)
True
对于好奇的人:
-
一年有 365.242196 天(绕太阳一周后,地球大约从其轴上旋转四分之一)
-
每四年增加一天。现在平均一年有 365.242196 - 0.25 = 364.992196 天
-
每 100 年减少一天。现在平均一年有 364.992196 + 0.01 = 365.002196 天
-
每 400 年增加一天。现在平均一年有 365.002196 - 0.0025 = 364.999696 天
暂且这样吧!我们不会谈论闰秒。
datetime 模块
标准的 datetime 模块处理(这应该不会让你感到惊讶)日期和时间。它定义了四个主要的对象类,每个类都有许多方法:
-
date用于年、月和日 -
time用于小时、分钟、秒和小数 -
datetime用于日期和时间的组合 -
timedelta用于日期和/或时间间隔
你可以通过指定年、月和日来创建一个 date 对象。这些值随后可作为属性使用:
>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> halloween
datetime.date(2019, 10, 31)
>>> halloween.day
31
>>> halloween.month
10
>>> halloween.year
2019
您可以使用其 isoformat() 方法打印一个 date:
>>> halloween.isoformat()
'2019-10-31'
iso 指的是 ISO 8601,这是一个国际标准,用于表示日期和时间。它从最一般的(年)到最具体的(日)进行排序。因此,它也正确排序:按年、月、日排序。我通常选择这种格式来表示程序中的日期,并用于按日期保存数据的文件名。下一节描述了更复杂的 strptime() 和 strftime() 方法,用于解析和格式化日期。
此示例使用 today() 方法生成今天的日期:
>>> from datetime import date
>>> now = date.today()
>>> now
datetime.date(2019, 4, 5)
这个示例利用了一个 timedelta 对象,将一些时间间隔添加到一个 date 中:
>>> from datetime import timedelta
>>> one_day = timedelta(days=1)
>>> tomorrow = now + one_day
>>> tomorrow
datetime.date(2019, 4, 6)
>>> now + 17*one_day
datetime.date(2019, 4, 22)
>>> yesterday = now - one_day
>>> yesterday
datetime.date(2019, 4, 4)
date 的范围是从 date.min(年=1,月=1,日=1)到 date.max(年=9999,月=12,日=31)。因此,您不能将其用于历史或天文计算。
datetime 模块的 time 对象用于表示一天中的时间:
>>> from datetime import time
>>> noon = time(12, 0, 0)
>>> noon
datetime.time(12, 0)
>>> noon.hour
12
>>> noon.minute
0
>>> noon.second
0
>>> noon.microsecond
0
参数从最大的时间单位(小时)到最小的(微秒)进行。如果您没有提供所有参数,time 将假定其余的都是零。顺便说一句,仅因为您可以存储和检索微秒,并不意味着您可以精确到微秒从计算机检索时间。次秒测量的准确性取决于硬件和操作系统中的许多因素。
datetime 对象包含日期和时间。您可以直接创建一个,例如接下来的一个,用于 2019 年 1 月 2 日凌晨 3 点 04 分,加上 5 秒和 6 微秒:
>>> from datetime import datetime
>>> some_day = datetime(2019, 1, 2, 3, 4, 5, 6)
>>> some_day
datetime.datetime(2019, 1, 2, 3, 4, 5, 6)
datetime 对象还有一个 isoformat() 方法:
>>> some_day.isoformat()
'2019-01-02T03:04:05.000006'
那个中间的 T 分隔了日期和时间部分。
datetime 有一个 now() 方法返回当前日期和时间:
>>> from datetime import datetime
>>> now = datetime.now()
>>> now
datetime.datetime(2019, 4, 5, 19, 53, 7, 580562)
>>> now.year
2019
>>> now.month
4
>>> now.day
5
>>> now.hour
19
>>> now.minute
53
>>> now.second
7
>>> now.microsecond
580562
您可以将 date 对象和 time 对象组合成 datetime:
>>> from datetime import datetime, time, date
>>> noon = time(12)
>>> this_day = date.today()
>>> noon_today = datetime.combine(this_day, noon)
>>> noon_today
datetime.datetime(2019, 4, 5, 12, 0)
您可以使用 date() 和 time() 方法从 datetime 中提取 date 和 time:
>>> noon_today.date()
datetime.date(2019, 4, 5)
>>> noon_today.time()
datetime.time(12, 0)
使用时间模块
令人困惑的是,Python 有一个带有 time 对象的 datetime 模块,以及一个单独的 time 模块。此外,time 模块有一个名为——等待它——time() 的函数。
表示绝对时间的一种方法是计算自某个起始点以来的秒数。Unix 时间 使用自 1970 年 1 月 1 日午夜以来的秒数。这个值通常称为 时代,通常是在系统之间交换日期和时间的最简单方法。
time 模块的 time() 函数返回当前时间的时代值:
>>> import time
>>> now = time.time()
>>> now
1554512132.778233
自 1970 年元旦以来已经过去了超过十亿秒。时间都去哪了?
您可以通过使用 ctime() 将时代值转换为字符串:
>>> time.ctime(now)
'Fri Apr 5 19:55:32 2019'
在下一节中,您将看到如何生成更吸引人的日期和时间格式。
epoch 值是与不同系统交换日期和时间的有用的最小公分母,如 JavaScript。然而,有时候你需要实际的天数、小时等,time 提供了 struct_time 对象。localtime() 提供系统时区的时间,而 gmtime() 提供 UTC 的时间:
>>> time.localtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=5, tm_hour=19,
tm_min=55, tm_sec=32, tm_wday=4, tm_yday=95, tm_isdst=1)
>>> time.gmtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=6, tm_hour=0,
tm_min=55, tm_sec=32, tm_wday=5, tm_yday=96, tm_isdst=0)
我的 19:55(中部时区,夏令时)在 UTC 的下一天的 00:55(以前称为 格林威治时间 或 Zulu 时间)。如果省略 localtime() 或 gmtime() 的参数,它们将使用当前时间。
struct_time 中的一些 tm_... 值可能有些模糊,因此请查看 表 13-1 获取更多详细信息。
表格 13-1. struct_time 的值
| 索引 | 名称 | 意义 | 值 |
|---|---|---|---|
| 0 | tm_year | 年份 | 0000 到 9999 |
| 1 | tm_mon | 月份 | 1 到 12 |
| 2 | tm_mday | 月份的某一天 | 1 到 31 |
| 3 | tm_hour | 小时 | 0 到 23 |
| 4 | tm_min | 分钟 | 0 到 59 |
| 5 | tm_sec | 秒 | 0 到 61 |
| 6 | tm_wday | 星期几 | 0(周一)到 6(周日) |
| 7 | tm_yday | 年内天数 | 1 到 366 |
| 8 | tm_isdst | 夏令时? | 0 = 否,1 = 是,-1 = 不明 |
如果你不想输入所有 tm_... 的名称,struct_time 也像一个命名元组一样(见 “命名元组”),所以你可以使用前面表格中的索引:
>>> import time
>>> now = time.localtime()
>>> now
time.struct_time(tm_year=2019, tm_mon=6, tm_mday=23, tm_hour=12,
tm_min=12, tm_sec=24, tm_wday=6, tm_yday=174, tm_isdst=1)
>>> now[0]
2019
print(list(now[x] for x in range(9)))
[2019, 6, 23, 12, 12, 24, 6, 174, 1]
mktime() 则反过来,将 struct_time 对象转换为 epoch 秒数:
>>> tm = time.localtime(now)
>>> time.mktime(tm)
1554512132.0
这与我们之前的 now() 的 epoch 值不完全匹配,因为 struct_time 对象仅保留到秒。
注意
一些建议:在可能的情况下,使用协调世界时 (UTC) 而不是时区。UTC 是一个绝对时间,独立于时区。如果你有一个服务器,将其时间设置为 UTC;不要使用本地时间。
更多建议:尽量避免使用夏令时。如果可以避免使用夏令时,一个时期的一个小时会消失(“提前”),而在另一个时期会出现两次(“倒退”)。出于某种原因,许多组织在计算机系统中使用本地时间和夏令时,但每年两次都会被那个神秘的小时弄得迷惑不解。
读写日期和时间
isoformat() 并不是写入日期和时间的唯一方式。你已经在 time 模块中看到了 ctime() 函数,你可以用它来将 epoch 转换为字符串:
>>> import time
>>> now = time.time()
>>> time.ctime(now)
'Fri Apr 5 19:58:23 2019'
你还可以通过使用 strftime() 将日期和时间转换为字符串。这作为 datetime、date 和 time 对象的一个方法提供,也作为 time 模块的一个函数提供。strftime() 使用格式字符串来指定输出,你可以在 表 13-2 中看到。
表格 13-2. strftime() 的输出格式说明符
| 格式字符串 | 日期/时间单元 | 范围 |
|---|---|---|
%Y | 年份 | 1900-… |
%m | 月份 | 01-12 |
%B | 月份名称 | January, … |
%b | 月份缩写 | Jan, … |
%d | 日期 | 01-31 |
%A | 星期名称 | Sunday, … |
a | 缩写星期 | Sun, … |
%H | 小时(24 小时制) | 00-23 |
%I | 小时(12 小时制) | 01-12 |
%p | 上午/下午 | AM, PM |
%M | 分钟 | 00-59 |
%S | 秒 | 00-59 |
数字在左侧补零。
这是由time模块提供的strftime()函数。它将struct_time对象转换为字符串。我们首先定义格式字符串fmt,稍后再次使用它:
>>> import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> t = time.localtime()
>>> t
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=13, tm_hour=15,
tm_min=23, tm_sec=46, tm_wday=2, tm_yday=72, tm_isdst=1)
>>> time.strftime(fmt, t)
"It's Wednesday, March 13, 2019, local time 03:23:46PM"
如果我们尝试使用date对象,只有日期部分会生效,时间默认为午夜:
>>> from datetime import date
>>> some_day = date(2019, 7, 4)
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_day.strftime(fmt)
"It's Thursday, July 04, 2019, local time 12:00:00AM"
对于time对象,只有时间部分会被转换:
>>> from datetime import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> some_time = time(10, 35)
>>> some_time.strftime(fmt)
"It's Monday, January 01, 1900, local time 10:35:00AM"
您不会想使用time对象的日部分,因为它们没有意义。
要反向转换并将字符串转换为日期或时间,请使用具有相同格式字符串的strptime()。没有正则表达式模式匹配;字符串的非格式部分(没有%)需要完全匹配。让我们指定一个与年-月-日匹配的格式,例如2019-01-29。如果要解析的日期字符串中有空格而不是破折号会发生什么?
>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019 01 29", fmt)
Traceback (most recent call last):
File "<stdin>",
line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
line 571, in _strptime_time
tt = _strptime(data_string, format)[0]
File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/_strptime.py",
line 359, in _strptime(data_string, format))
ValueError: time data '2019 01 29' does not match format '%Y-%m-%d
如果我们给strptime()输入一些破折号,它会开心吗?
>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019-01-29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)
或者修复fmt字符串以匹配日期字符串:
>>> import time
>>> fmt = "%Y %m %d"
>>> time.strptime("2019 01 29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0,
tm_min=0, tm_sec=0, tm_wday=1, tm_yday=29, tm_isdst=-1)
即使字符串似乎与其格式匹配,如果值超出范围,将引发异常(文件名因空间而被截断):
>>> time.strptime("2019-13-29", fmt)
Traceback (most recent call last):
File "<stdin>",
line 1, in <module>
File ".../3.7/lib/python3.7/_strptime.py",
line 571, in _strptime_time
tt = _strptime(data_string, format)[0]
File ".../3.7/lib/python3.7/_strptime.py",
line 359, in _strptime(data_string, format))
ValueError: time data '2019-13-29' does not match format '%Y-%m-%d
名称特定于您的locale——操作系统的国际化设置。如果您需要打印不同的月份和日期名称,请通过使用setlocale()来更改您的 locale;它的第一个参数是用于日期和时间的locale.LC_TIME,第二个参数是一个字符串,结合语言和国家缩写。让我们邀请一些国际朋友参加万圣节聚会。我们将用美国英语、法语、德语、西班牙语和冰岛语(冰岛人真的有精灵)打印月份、日期和星期几:
>>> import locale
>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> for lang_country in ['en_us', 'fr_fr', 'de_de', 'es_es', 'is_is',]:
... locale.setlocale(locale.LC_TIME, lang_country)
... halloween.strftime('%A, %B %d')
...
'en_us'
'Thursday, October 31'
'fr_fr'
'Jeudi, octobre 31'
'de_de'
'Donnerstag, Oktober 31'
'es_es'
'jueves, octubre 31'
'is_is'
'fimmtudagur, október 31'
>>>
您从哪里找到这些lang_country的魔法值?这有点奇怪,但您可以尝试这样做以获取所有这些值(有几百个):
>>> import locale
>>> names = locale.locale_alias.keys()
从names中,让我们获取看起来可以与setlocale()一起使用的区域设置名称,例如我们在前面示例中使用的那些——两个字符的语言代码,后面跟着下划线和两个字符的国家代码:
>>> good_names = [name for name in names if \
len(name) == 5 and name[2] == '_']
前五个长什么样?
>>> good_names[:5]
['sr_cs', 'de_at', 'nl_nl', 'es_ni', 'sp_yu']
因此,如果您想要所有的德语语言区域设置,请尝试这样做:
>>> de = [name for name in good_names if name.startswith('de')]
>>> de
['de_at', 'de_de', 'de_ch', 'de_lu', 'de_be']
注意
如果运行set_locale()并出现错误
locale.Error: unsupported locale setting
该 locale 不受您的操作系统支持。您需要弄清楚您的操作系统需要添加什么。即使 Python 告诉您(使用locale.locale_alias.keys())这是一个好的 locale,我在使用cy_gb(威尔士语,英国)locale 时在 macOS 上遇到过此错误,即使它之前接受了is_is(冰岛语)的情况也是如此。
所有的转换
图 13-1(来自 Python wiki)总结了所有标准 Python 时间转换。
图 13-1. 日期和时间转换
替代模块
如果你觉得标准库模块混乱,或者缺少你想要的特定转换,那么有许多第三方替代方案。以下是其中几个:
将许多日期和时间函数与简单的 API 结合起来。
几乎可以解析任何日期格式,并且良好处理相对日期和时间。
填补了 ISO8601 格式在标准库中的空白。
许多时区功能。
直观的日期、时间和间隔接口。
从日期/时间字符串中猜测正确的格式字符串。
即将到来
文件和目录也需要关爱。
要做的事情
13.1 将当前日期作为字符串写入文本文件 today.txt。
13.2 将文本文件 today.txt 读入字符串 today_string 中。
13.3 解析来自 today_string 的日期。
13.4 创建你的生日的日期对象。
13.5 你的生日是星期几?
13.6 你将(或者已经)十千天岁时是哪天?
¹ 大约是 Unix 诞生的起点,忽略那些烦人的闰秒。
第十四章:文件和目录。
我有文件,我有电脑文件,你知道的,在纸上也有文件。但大部分都在我脑子里。所以如果我的脑子出了问题,上帝帮帮我!
乔治·R·R·马丁。
当你刚开始学习编程时,你会反复听到一些词,但不确定它们是否具有特定的技术含义还是随意的说法。文件和目录就是这样的词,它们确实有实际的技术含义。文件是一系列字节,存储在某个文件系统中,并通过文件名访问。目录是文件和可能其他目录的集合。术语文件夹是目录的同义词。它出现在计算机获得图形用户界面时,模仿办公室概念,使事物看起来更加熟悉。
许多文件系统是分层的,通常被称为类似于树。真实的办公室里不会有树,文件夹类比只有在你能够想象出所有子文件夹的情况下才有效。
文件的输入和输出。
最简单的持久性形式是普通的文件,有时称为平面文件。你从文件中读取到内存中,然后从内存中写入到文件中。Python 使得这些工作变得容易。与许多语言一样,它的文件操作在很大程度上是模仿熟悉且受欢迎的 Unix 等效操作。
使用open()创建或打开。
在执行以下操作之前,您需要调用open函数:
-
读取现有文件。
-
写入到一个新文件。
-
追加到现有文件。
-
覆盖现有文件。
*`fileobj`* = open( *`filename`*, *`mode`* )
这里是对这个调用的各部分的简要解释:
-
*
fileobj*是open()返回的文件对象。 -
*
filename*是文件的字符串名称。 -
*
mode*是一个表示文件类型及其操作的字符串。
*mode*的第一个字母表示操作:
-
r表示读取。 -
w表示写入。如果文件不存在,则创建该文件。如果文件存在,则覆盖它。 -
x表示写入,但只有在文件不存在时才会写入。 -
a表示追加(在末尾写入),如果文件存在。
mode的第二个字母表示文件的类型:
-
t(或什么都不写)表示文本。 -
b表示二进制。
打开文件后,您可以调用函数来读取或写入数据;这些将在接下来的示例中展示。
最后,您需要关闭文件以确保任何写入操作都已完成,并且内存已被释放。稍后,您将看到如何使用with来自动化此过程。
此程序打开一个名为oops.txt的文件,并在不写入任何内容的情况下关闭它。这将创建一个空文件:
>>> fout = open('oops.txt', 'wt')
>>> fout.close()
使用print()写入文本文件。
让我们重新创建oops.txt,然后向其中写入一行内容,然后关闭它:
>>> fout = open('oops.txt', 'wt')
>>> print('Oops, I created a file.', file=fout)
>>> fout.close()
我们在上一节创建了一个空的oops.txt文件,所以这只是覆盖它。
我们使用了print函数的file参数。如果没有这个参数,print会将内容写入标准输出,也就是你的终端(除非你已经告诉你的 shell 程序使用>重定向输出到文件或使用|管道传输到另一个程序)。
使用write()写入文本文件。
我们刚刚使用print向文件中写入了一行。我们也可以使用write。
对于我们的多行数据源,让我们使用这首关于狭义相对论的打油诗作为例子:¹
>>> poem = '''There was a young lady named Bright,
... Whose speed was far faster than light;
... She started one day
... In a relative way,
... And returned on the previous night.'''
>>> len(poem)
150
下面的代码一次性将整首诗写入到名为'relativity'的文件中:
>>> fout = open('relativity', 'wt')
>>> fout.write(poem)
150
>>> fout.close()
write函数返回写入的字节数。它不像print那样添加空格或换行符。同样,你也可以使用print将多行字符串写入文本文件:
>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout)
>>> fout.close()
那么,应该使用write还是print?正如你所见,默认情况下,print在每个参数后添加一个空格,并在末尾添加换行符。在前一个示例中,它向relativity文件附加了一个换行符。要使print像write一样工作,将以下两个参数传递给它:
-
sep(分隔符,默认为空格,' ') -
end(结束字符串,默认为换行符,'\n')
我们将使用空字符串来替换这些默认值:
>>> fout = open('relativity', 'wt')
>>> print(poem, file=fout, sep='', end='')
>>> fout.close()
如果你有一个大的源字符串,你也可以写入分片(使用切片),直到源字符串处理完毕:
>>> fout = open('relativity', 'wt')
>>> size = len(poem)
>>> offset = 0
>>> chunk = 100
>>> while True:
... if offset > size:
... break
... fout.write(poem[offset:offset+chunk])
... offset += chunk
...
100
50
>>> fout.close()
这一次在第一次尝试中写入了 100 个字符,下一次写入了最后 50 个字符。切片允许你“超过结尾”而不会引发异常。
如果对我们来说relativity文件很重要,让我们看看使用模式x是否真的保护我们免受覆盖:
>>> fout = open('relativity', 'xt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileExistsError: [Errno 17] File exists: 'relativity'
你可以将其与异常处理器一起使用:
>>> try:
... fout = open('relativity', 'xt')]
... fout.write('stomp stomp stomp')
... except FileExistsError:
... print('relativity already exists!. That was a close one.')
...
relativity already exists!. That was a close one.
使用read()、readline()或readlines()读取文本文件
你可以不带参数调用read()一次性读取整个文件,就像下面的示例一样(在处理大文件时要小心;一个 1GB 文件将消耗 1GB 内存):
>>> fin = open('relativity', 'rt' )
>>> poem = fin.read()
>>> fin.close()
>>> len(poem)
150
你可以提供一个最大字符数来限制read()一次返回多少内容。让我们一次读取 100 个字符,并将每个块追加到poem字符串以重建原始内容:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> chunk = 100
>>> while True:
... fragment = fin.read(chunk)
... if not fragment:
... break
... poem += fragment
...
>>> fin.close()
>>> len(poem)
150
当你读取到结尾后,进一步调用read()会返回一个空字符串(''),这在if not fragment中被视作False。这会跳出while True循环。
你也可以使用readline()一次读取一行。在下一个示例中,我们将每一行追加到poem字符串中以重建原始内容:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> while True:
... line = fin.readline()
... if not line:
... break
... poem += line
...
>>> fin.close()
>>> len(poem)
150
对于文本文件,即使是空行也有长度为一(换行符),并且被视作True。当文件被读取完毕时,readline()(和read()一样)也会返回一个空字符串,同样被视作False。
读取文本文件的最简单方法是使用迭代器。它一次返回一行。与前面的示例类似,但代码更少:
>>> poem = ''
>>> fin = open('relativity', 'rt' )
>>> for line in fin:
... poem += line
...
>>> fin.close()
>>> len(poem)
150
所有前述示例最终构建了单个字符串poem。readlines()方法逐行读取,返回一个包含每行字符串的列表:
>>> fin = open('relativity', 'rt' )
>>> lines = fin.readlines()
>>> fin.close()
>>> print(len(lines), 'lines read')
5 lines read
>>> for line in lines:
... print(line, end='')
...
There was a young lady named Bright,
Whose speed was far faster than light;
She started one day
In a relative way,
And returned on the previous night.>>>
我们告诉print()不要自动换行,因为前四行已经有了换行。最后一行没有换行,导致交互提示符>>>出现在最后一行之后。
使用write()写入二进制文件
如果在模式字符串中包含 'b',文件将以二进制模式打开。在这种情况下,你读取和写入的是 bytes 而不是字符串。
我们手头没有二进制诗歌,所以我们只会生成从 0 到 255 的 256 个字节值:
>>> bdata = bytes(range(0, 256))
>>> len(bdata)
256
以二进制模式打开文件进行写入,并一次性写入所有数据:
>>> fout = open('bfile', 'wb')
>>> fout.write(bdata)
256
>>> fout.close()
同样,write() 返回写入的字节数。
和文本一样,你可以将二进制数据分块写入:
>>> fout = open('bfile', 'wb')
>>> size = len(bdata)
>>> offset = 0
>>> chunk = 100
>>> while True:
... if offset > size:
... break
... fout.write(bdata[offset:offset+chunk])
... offset += chunk
...
100
100
56
>>> fout.close()
使用 read() 读取二进制文件
这个很简单;你只需要用 'rb' 打开即可:
>>> fin = open('bfile', 'rb')
>>> bdata = fin.read()
>>> len(bdata)
256
>>> fin.close()
使用 with 自动关闭文件
如果你忘记关闭已打开的文件,在不再引用后 Python 会关闭它。这意味着如果你在函数中打开文件但没有显式关闭它,在函数结束时文件会被自动关闭。但你可能在长时间运行的函数或程序的主要部分中打开了文件。应该关闭文件以确保所有未完成的写入被完成。
Python 有上下文管理器来清理诸如打开的文件之类的资源。你可以使用形式 with 表达式 as 变量:
>>> with open('relativity', 'wt') as fout:
... fout.write(poem)
...
就是这样。在上下文管理器(在本例中就是一行代码块)完成(正常完成 或 通过抛出异常)后,文件会自动关闭。
使用 seek() 改变位置
当你读取和写入时,Python 会跟踪你在文件中的位置。tell() 函数返回你当前从文件开头的偏移量,以字节为单位。seek() 函数让你跳到文件中的另一个字节偏移量。这意味着你不必读取文件中的每个字节来读取最后一个字节;你可以 seek() 到最后一个字节并只读取一个字节。
对于这个示例,使用你之前写的 256 字节二进制文件 'bfile':
>>> fin = open('bfile', 'rb')
>>> fin.tell()
0
使用 seek() 跳转到文件末尾前一个字节:
>>> fin.seek(255)
255
读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
seek() 也会返回当前偏移量。
你可以给 seek() 调用一个第二参数:seek(*`offset`*, *`origin`*):
-
如果
origin是0(默认值),就从文件开头向后offset字节 -
如果
origin是1,就从当前位置向后offset字节 -
如果
origin是2,就从文件末尾相对offset字节
这些值也在标准的 os 模块中定义:
>>> import os
>>> os.SEEK_SET
0
>>> os.SEEK_CUR
1
>>> os.SEEK_END
2
因此,我们可以用不同的方式读取最后一个字节:
>>> fin = open('bfile', 'rb')
文件末尾前一个字节:
>>> fin.seek(-1, 2)
255
>>> fin.tell()
255
读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
注意
你不需要调用 tell() 来让 seek() 工作。我只是想展示它们报告相同的偏移量。
下面是从文件当前位置进行搜索的示例:
>>> fin = open('bfile', 'rb')
这个示例最终会在文件末尾前两个字节处结束:
>>> fin.seek(254, 0)
254
>>> fin.tell()
254
现在向前移动一个字节:
>>> fin.seek(1, 1)
255
>>> fin.tell()
255
最后,读取直到文件末尾:
>>> bdata = fin.read()
>>> len(bdata)
1
>>> bdata[0]
255
这些函数对于二进制文件最有用。你可以用它们处理文本文件,但除非文件是 ASCII(每个字符一个字节),否则计算偏移量会很困难。这将取决于文本编码,而最流行的编码(UTF-8)使用不同数量的字节表示每个字符。
内存映射
读取和写入文件的替代方法是使用标准mmap模块将其内存映射。 这使得文件内容在内存中看起来像一个bytearray。 有关详细信息,请参阅文档和一些示例。
文件操作
Python,像许多其他语言一样,根据 Unix 模式化其文件操作。 一些函数,例如chown()和chmod(),具有相同的名称,但还有一些新函数。
首先,我将展示 Python 如何使用os.path模块的函数以及使用较新的pathlib模块处理这些任务。
使用exists()检查存在性。
要验证文件或目录是否确实存在,或者您只是想象了它,您可以提供exists(),并提供相对或绝对路径名,如此示例所示:
>>> import os
>>> os.path.exists('oops.txt')
True
>>> os.path.exists('./oops.txt')
True
>>> os.path.exists('waffles')
False
>>> os.path.exists('.')
True
>>> os.path.exists('..')
True
使用isfile()检查类型。
此部分中的函数检查名称是否引用文件、目录或符号链接(有关链接讨论的示例,请参见后续内容)。
我们将首先查看的第一个函数isfile,它提出一个简单的问题:这是一个普通的老实文件吗?
>>> name = 'oops.txt'
>>> os.path.isfile(name)
True
下面是确定目录的方法:
>>> os.path.isdir(name)
False
单个点(.)是当前目录的简写,两个点(..)代表父目录。 这些始终存在,因此像以下语句将始终报告True:
>>> os.path.isdir('.')
True
os模块包含许多处理路径名(完全合格的文件名,以/开头并包括所有父级)的函数。 其中一个函数isabs()确定其参数是否为绝对路径名。 参数不需要是真实文件的名称:
>>> os.path.isabs(name)
False
>>> os.path.isabs('/big/fake/name')
True
>>> os.path.isabs('big/fake/name/without/a/leading/slash')
False
使用copy()复制。
copy()函数来自另一个模块shutil。 例如,将文件oops.txt复制到文件ohno.txt:
>>> import shutil
>>> shutil.copy('oops.txt', 'ohno.txt')
shutil.move()函数复制文件,然后删除原始文件。
使用rename()函数更改名称。
此函数正如其名称所示。 在此示例中,它将ohno.txt重命名为ohwell.txt:
>>> import os
>>> os.rename('ohno.txt', 'ohwell.txt')
使用link()或symlink()链接。
在 Unix 中,文件存在于一个位置,但可以有多个名称,称为链接。 在低级硬链接中,很难找到给定文件的所有名称。 符号链接是一种替代方法,它将新名称存储为自己的文件,使您可以同时获取原始名称和新名称。 link()调用创建硬链接,symlink()创建符号链接。 islink()函数检查文件是否是符号链接。
下面是如何为现有文件oops.txt创建硬链接到新文件yikes.txt的方法:
>>> os.link('oops.txt', 'yikes.txt')
>>> os.path.isfile('yikes.txt')
True
>>> os.path.islink('yikes.txt')
False
要为现有文件oops.txt创建到新文件jeepers.txt的符号链接,请使用以下命令:
>>> os.symlink('oops.txt', 'jeepers.txt')
>>> os.path.islink('jeepers.txt')
True
使用chmod()更改权限。
在 Unix 系统中,chmod() 改变文件权限。对于用户(通常是你,如果你创建了该文件)、用户所在的主要组和其余世界,有读、写和执行权限。该命令使用紧凑的八进制(基数 8)值,结合用户、组和其他权限。例如,要使 oops.txt 只能由其所有者读取,输入以下内容:
>>> os.chmod('oops.txt', 0o400)
如果你不想处理晦涩的八进制值,而宁愿处理(稍微不那么)晦涩的符号,可以从 stat 模块导入一些常量,并使用如下语句:
>>> import stat
>>> os.chmod('oops.txt', stat.S_IRUSR)
使用 chown() 更改所有权
这个函数同样适用于 Unix/Linux/Mac。你可以通过指定数值用户 ID (uid) 和组 ID (gid) 来改变文件的所有者和/或组所有权:
>>> uid = 5
>>> gid = 22
>>> os.chown('oops', uid, gid)
使用 remove() 删除文件
在这段代码中,我们使用 remove() 函数,告别 oops.txt:
>>> os.remove('oops.txt')
>>> os.path.exists('oops.txt')
False
目录操作
在大多数操作系统中,文件存在于层级结构的目录(通常称为文件夹)中。所有这些文件和目录的容器是一个文件系统(有时称为卷)。标准的 os 模块处理这些操作系统的具体细节,并提供以下函数,用于对它们进行操作。
使用 mkdir() 创建
此示例展示了如何创建一个名为 poems 的目录来存储那些珍贵的诗句:
>>> os.mkdir('poems')
>>> os.path.exists('poems')
True
使用 rmdir() 删除目录
经过重新考虑²,你决定其实根本不需要那个目录。以下是如何删除它的方法:
>>> os.rmdir('poems')
>>> os.path.exists('poems')
False
使用 listdir() 列出内容
好的,重来一次;让我们再次创建 poems,并添加一些内容:
>>> os.mkdir('poems')
现在获取其内容列表(到目前为止还没有):
>>> os.listdir('poems')
[]
接下来,创建一个子目录:
>>> os.mkdir('poems/mcintyre')
>>> os.listdir('poems')
['mcintyre']
在这个子目录中创建一个文件(如果你真的感觉有诗意,才输入所有这些行;确保使用匹配的单引号或三重引号开头和结尾):
>>> fout = open('poems/mcintyre/the_good_man', 'wt')
>>> fout.write('''Cheerful and happy was his mood,
... He to the poor was kind and good,
... And he oft' times did find them food,
... Also supplies of coal and wood,
... He never spake a word was rude,
... And cheer'd those did o'er sorrows brood,
... He passed away not understood,
... Because no poet in his lays
... Had penned a sonnet in his praise,
... 'Tis sad, but such is world's ways.
... ''')
344
>>> fout.close()
最后,让我们看看我们有什么。它最好在那里:
>>> os.listdir('poems/mcintyre')
['the_good_man']
使用 chdir() 更改当前目录
使用此函数,你可以从一个目录切换到另一个目录。让我们离开当前目录,花一点时间在 poems 中:
>>> import os
>>> os.chdir('poems')
>>> os.listdir('.')
['mcintyre']
使用 glob() 列出匹配的文件
glob() 函数使用 Unix shell 规则而非更完整的正则表达式语法来匹配文件或目录名。以下是这些规则:
-
*匹配任何内容(re应该期望.*) -
?匹配一个单字符 -
[abc]匹配字符a、b或c -
[!abc]匹配除了a、b或c之外的任何字符
尝试获取所有以 m 开头的文件或目录:
>>> import glob
>>> glob.glob('m*')
['mcintyre']
任何两个字母的文件或目录如何?
>>> glob.glob('??')
[]
我在想一个以 m 开头、以 e 结尾的八个字母的单词:
>>> glob.glob('m??????e')
['mcintyre']
那么任何以 k、l 或 m 开头、以 e 结尾的内容呢?
>>> glob.glob('[klm]*e')
['mcintyre']
路径名
几乎所有的计算机都使用层次化文件系统,其中目录(“文件夹”)包含文件和其他目录,向下延伸到不同的层级。当您想引用特定的文件或目录时,您需要它的 路径名:到达那里所需的目录序列,可以是 绝对 从顶部(根)或 相对 到您当前目录。
当您指定名称时,您经常会听到人们混淆正斜杠('/',而不是 Guns N’ Roses 的家伙)和反斜杠('\')。³ Unix 和 Mac(以及 Web URL)使用正斜杠作为 路径分隔符,而 Windows 使用反斜杠。⁴
Python 允许您在指定名称时使用斜杠作为路径分隔符。在 Windows 上,您可以使用反斜杠,但是您知道反斜杠在 Python 中是普遍的转义字符,所以您必须在所有地方加倍使用它,或者使用 Python 的原始字符串:
>>> win_file = 'eek\\urk\\snort.txt'
>>> win_file2 = r'eek\urk\snort.txt'
>>> win_file
'eek\\urk\\snort.txt'
>>> win_file2
'eek\\urk\\snort.txt'
当您构建路径名时,您可以做以下操作:
-
使用适当的路径分隔符 (
'/'或'\') -
使用 os.path.join() 构建路径名(参见 “使用 os.path.join() 构建路径名”)
-
使用 pathlib(参见 “使用 pathlib”)
通过 abspath() 获取路径名
这个函数将相对名扩展为绝对名。如果您的当前目录是 /usr/gaberlunzie 并且文件 oops.txt 就在那里,您可以输入以下内容:
>>> os.path.abspath('oops.txt')
'/usr/gaberlunzie/oops.txt'
使用 realpath() 获取符号链接路径名
在较早的某一部分中,我们从新文件 jeepers.txt 创造了对 oops.txt 的符号链接。在这种情况下,您可以使用 realpath() 函数从 jeepers.txt 获取 oops.txt 的名称,如下所示:
>>> os.path.realpath('jeepers.txt')
'/usr/gaberlunzie/oops.txt'
使用 os.path.join() 构建路径名
当您构建一个多部分的路径名时,您可以使用 os.path.join() 将它们成对地组合,使用适合您操作系统的正确路径分隔符:
>>> import os
>>> win_file = os.path.join("eek", "urk")
>>> win_file = os.path.join(win_file, "snort.txt")
如果我在 Mac 或 Linux 系统上运行这个程序,我会得到这个结果:
>>> win_file
'eek/urk/snort.txt'
在 Windows 上运行会产生这个结果:
>>> win_file
'eek\\urk\\snort.txt'
但是如果相同的代码在不同位置运行会产生不同的结果,这可能是一个问题。新的 pathlib 模块是这个问题的一个便携解决方案。
使用 pathlib
Python 在版本 3.4 中添加了 pathlib 模块。它是我刚刚描述的 os.path 模块的一个替代方案。但我们为什么需要另一个模块呢?
它不是把文件系统路径名当作字符串,而是引入了 Path 对象来在稍高一级处理它们。使用 Path() 类创建一个 Path,然后用裸斜线(而不是 '/' 字符)将您的路径编织在一起:
>>> from pathlib import Path
>>> file_path = Path('eek') / 'urk' / 'snort.txt'
>>> file_path
PosixPath('eek/urk/snort.txt')
>>> print(file_path)
eek/urk/snort.txt
这个斜杠技巧利用了 Python 的 “魔术方法”。一个 Path 可以告诉您关于自己的一些信息:
>>> file_path.name
'snort.txt'
>>> file_path.suffix
'.txt'
>>> file_path.stem
'snort'
您可以像对待任何文件名或路径名字符串一样将 file_path 提供给 open()。
您还可以看到如果在另一个系统上运行此程序会发生什么,或者如果需要在您的计算机上生成外国路径名:
>>> from pathlib import PureWindowsPath
>>> PureWindowsPath(file_path)
PureWindowsPath('eek/urk/snort.txt')
>>> print(PureWindowsPath(file_path))
eek\urk\snort.txt
参见文档以获取所有细节。
BytesIO 和 StringIO
你已经学会了如何修改内存中的数据以及如何将数据读取到文件中和从文件中获取数据。如果你有内存中的数据,但想调用一个期望文件的函数(或者反过来),你会怎么做?你想修改数据并传递这些字节或字符,而不是读取和写入临时文件。
你可以使用 io.BytesIO 处理二进制数据(bytes)和 io.StringIO 处理文本数据(str)。使用其中任何一个都可以将数据包装为类文件对象,适用于本章中介绍的所有文件函数。
这种情况的一个用例是数据格式转换。让我们将其应用于 PIL 库(详细信息将在“PIL 和 Pillow”中介绍),该库读取和写入图像数据。其 Image 对象的 open() 和 save() 方法的第一个参数是文件名或类文件对象。示例 14-1 中的代码使用 BytesIO 在内存中读取 并且 写入数据。它从命令行读取一个或多个图像文件,将其图像数据转换为三种不同的格式,并打印这些输出的长度和前 10 个字节。
示例 14-1. convert_image.py
from io import BytesIO
from PIL import Image
import sys
def data_to_img(data):
"""Return PIL Image object, with data from in-memory <data>"""
fp = BytesIO(data)
return Image.open(fp) # reads from memory
def img_to_data(img, fmt=None):
"""Return image data from PIL Image <img>, in <fmt> format"""
fp = BytesIO()
if not fmt:
fmt = img.format # keeps the original format
img.save(fp, fmt) # writes to memory
return fp.getvalue()
def convert_image(data, fmt=None):
"""Convert image <data> to PIL <fmt> image data"""
img = data_to_img(data)
return img_to_data(img, fmt)
def get_file_data(name):
"""Return PIL Image object for image file <name>"""
img = Image.open(name)
print("img", img, img.format)
return img_to_data(img)
if __name__ == "__main__":
for name in sys.argv[1:]:
data = get_file_data(name)
print("in", len(data), data[:10])
for fmt in ("gif", "png", "jpeg"):
out_data = convert_image(data, fmt)
print("out", len(out_data), out_data[:10])
注意
因为它的行为类似于文件,所以你可以像处理普通文件一样使用 seek()、read() 和 write() 方法处理 BytesIO 对象;如果你执行了 seek() 后跟着一个 read(),你将只获得从该 seek 位置到结尾的字节。getvalue() 返回 BytesIO 对象中的所有字节。
这是输出结果,使用了你将在第二十章中看到的输入图像文件:
$ python convert_image.py ch20_critter.png
img <PIL.PngImagePlugin.PngImageFile image mode=RGB size=154x141 at 0x10340CF28> PNG
in 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 14751 b'GIF87a\\x9a\\x00\\x8d\\x00'
out 24941 b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00'
out 5914 b'\\xff\xd8\\xff\\xe0\\x00\\x10JFIF'
即将到来
下一章内容稍微复杂一些。它涉及并发(即大约同时执行多个任务的方式)和进程(运行程序)。
要做的事情
14.1 列出当前目录中的文件。
14.2 列出父目录中的文件。
14.3 将字符串 'This is a test of the emergency text system' 赋值给变量 test1,并将 test1 写入名为 test.txt 的文件。
14.4 打开文件 test.txt 并将其内容读取到字符串 test2 中。test1 和 test2 是否相同?
¹ 在本书的第一份手稿中,我说的是广义相对论,被一位物理学家审阅者友善地纠正了。
² 为什么它从来都不是第一个?
³ 一种记忆方法是:斜杠向前倾斜,反斜杠向后倾斜。
⁴ 当 IBM 联系比尔·盖茨,询问他们的第一台个人电脑时,他以 $50,000 购买了操作系统 QDOS,以获得“MS-DOS”。它模仿了使用斜杠作为命令行参数的 CP/M。当 MS-DOS 后来添加了文件夹时,它不得不使用反斜杠。