0.12. PY4E - Python for Everybody¶
切换导航
第 1 章:简介 第 2 章:变量 第 3 章:条件语句 第 4 章:函数 第 5 章:迭代 第 6 章:字符串 第 7 章:文件 第 8 章:列表 第 9 章:字典 第 10 章:元组 第 11 章:正则表达式 第 12 章:网络程序 第 13 章:Python 与 Web 服务 第 14 章:Python 对象 第 15 章:Python 与数据库 第 16 章:数据可视化
0.12.1. 正则表达式¶
到目前为止,我们一直在读取文件,查找模式并提取我们感兴趣的行的各个部分。我们一直使用字符串方法,如 split 和 find,并使用列表和字符串切片来提取行的部分。
查找和提取这项任务如此常见,以至于 Python 拥有一个名为 正则表达式 的强大模块,能够优雅地处理许多此类任务。我们之所以没有在本书早期引入正则表达式,是因为虽然它们非常强大,但略显复杂,且其语法需要一些时间才能适应。
正则表达式几乎是用于查找和解析字符串的独立小编程语言。事实上,已有整本书专门论述正则表达式。在本章中,我们仅涵盖正则表达式的基础。关于正则表达式的更多细节,请参阅:
https://en.wikipedia.org/wiki/Regular_expression
https://docs.python.org/library/re.html
在您的程序中使用正则表达式模块 re 之前,必须先将其导入。正则表达式模块最简单的用法是 search() 函数。以下程序演示了该搜索函数的一个简单用法。
# Search for lines that contain 'From'
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('From:', line):
print(line)
# Code: http://www.py4e.com/code3/re01.py
我们打开文件,逐行遍历,并使用正则表达式的 search() 方法,仅打印包含字符串 "From:" 的行。该程序并未真正发挥正则表达式的强大功能,因为我们可以同样轻松地使用 line.find() 来实现相同的结果。
正则表达式的强大之处在于,当我们向查找字符串添加特殊字符时,这些字符允许我们更精确地控制哪些行与字符串匹配。将这些特殊字符添加到我们的正则表达式中,允许我们在编写极少代码的同时执行复杂的匹配和提取操作。
例如,尖括号字符用于正则表达式中匹配行的“开头”。我们可以修改程序,使其仅匹配“From:”位于行开头的行,如下所示:
# Search for lines that start with 'From'
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:', line):
print(line)
# Code: http://www.py4e.com/code3/re02.py
现在我们将只匹配以字符串 "From:" 开头 的行。这仍然是一个非常简单的例子,我们可以用字符串模块中的 startswith() 方法以同等方式完成它。但它有助于引入正则表达式包含特殊动作字符的概念,这些字符让我们对匹配正则表达式的内容拥有更多控制权。
0.12.1.1. 正则表达式中的字符匹配¶
还有其他一些特殊字符可以让我们构建功能更强大的正则表达式。最常用的特殊字符是句号(period),它匹配任意字符。
在以下示例中,正则表达式 F..m: 会匹配字符串 "From:"、"Fxxm:"、"F12m:" 或 "F!@m:" 中的任意一个,因为正则表达式中的点号字符匹配任意字符。
# Search for lines that start with 'F', followed by
# 2 characters, followed by 'm:'
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^F..m:', line):
print(line)
# Code: http://www.py4e.com/code3/re03.py
这在结合使用指示字符可以重复任意次数的能力时特别强大,该能力通过您在正则表达式中使用的 * 或 + 字符来实现。这些特殊字符意味着它们不再匹配搜索字符串中的单个字符,而是匹配零个或多个字符(在星号的情况下)或一个或多个字符(在加号的情况下)。
我们可以在以下示例中通过重复使用 通配符 字符进一步缩小我们要匹配的行:
# Search for lines that start with From and have an at sign
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^From:.+@', line):
print(line)
# Code: http://www.py4e.com/code3/re04.py
之前的查找字符串 `^From:.+@` 将成功匹配以 "From:" 开头,后跟一个或多个字符 (`.+`),再后跟一个 @ 符号的行。因此,这将匹配以下行:
From: stephen.marquard@uct.ac.za
您可以将 `.+` 通配符视为在冒号字符与 @ 符号之间的所有字符的展开匹配。
From:.+@
将加号和星号字符视为“激进”是个好主意。例如,下面的字符串会匹配该字符串中的最后一个 @ 符号,因为 .+ 向外扩展,如下图所示:
From: stephen.marquard@uct.ac.za, csev@umich.edu, and cwen @iupui.edu
可以通过添加另一个字符来告诉星号 (*) 或加号 (+) 不要如此“贪婪”。有关关闭贪婪行为的详细信息,请参阅详细文档。
0.12.1.2. 使用正则表达式提取数据¶
如果在 Python 中想要从字符串中提取数据,我们可以使用 findall() 方法来提取所有匹配正则表达式的子串。让我们以从任何行中提取任何看起来像电子邮件地址的内容为例,无论其格式如何。例如,我们要从以下每一行中提取电子邮件地址:
From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008
Return-Path: <postmaster@collab.sakaiproject.org>
for <source@collab.sakaiproject.org>;
Received: (from apache@localhost)
Author: stephen.marquard@uct.ac.za
我们不希望为每种类型的行编写代码,并为每一行分别实现不同的分割和切片操作。下面的程序使用 findall() 查找包含电子邮件地址的行,并从每一行中提取一个或多个地址。
import re
s = 'A message from csev@umich.edu to cwen@iupui.edu about meeting @2PM'
lst = re.findall('\S+@\S+', s)
print(lst)
# Code: http://www.py4e.com/code3/re05.py
findall() 方法在第二个参数中搜索字符串,并返回所有看起来像电子邮件地址的字符串列表。我们使用一个匹配非空白字符的双字符序列(\S)。
程序的输出结果将是:
['csev@umich.edu', 'cwen@iupui.edu']
翻译正则表达式,我们要查找至少包含一个非空白字符,后跟一个 @ 符号,再后跟至少一个非空白字符的子串。\S+ 匹配尽可能多的非空白字符。
该正则表达式将匹配两次 (csev@umich.edu 和 cwen@iupui.edu),但不会匹配字符串"@2PM",因为 at 符号之前没有非空白字符。我们可以使用此正则表达式编写程序,读取文件中的所有行,并打印出任何看起来像电子邮件地址的内容,如下所示:
# Search for lines that have an at sign between characters
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('\S+@\S+', line)
if len(x) > 0:
print(x)
# Code: http://www.py4e.com/code3/re06.py
我们逐行读取,然后提取所有匹配我们正则表达式的子串。由于 `findall()` 返回一个列表,我们只需检查返回列表中的元素数量是否大于零,以仅打印至少找到一个看起来像电子邮件地址的子串的行的内容。
如果我们在 mbox-short.txt 上运行该程序,我们得到以下输出:
...
['<source@collab.sakaiproject.org>;']
['<source@collab.sakaiproject.org>;']
['apache@localhost)']
['source@collab.sakaiproject.org;']
['cwen@iupui.edu']
['source@collab.sakaiproject.org']
['cwen@iupui.edu']
['cwen@iupui.edu']
['wagnermr@iupui.edu']
我们的一些电子邮件地址在开头或结尾包含不正确的字符,如"<"或";"。让我们声明,我们只关心字符串中以字母或数字开始和结束的部分。
为此,我们使用正则表达式的另一个功能。方括号用于指示一组我们愿意考虑匹配的可接受字符。从某种意义上说,\S 是在询问匹配“非空白字符”的集合。现在,我们将以更明确的方式指定我们要匹配的字符。
这里是我们新的正则表达式:
[a-zA-Z0-9]\S*@\S*[a-zA-Z]
这变得有点复杂,你可以开始理解为什么正则表达式是一种独立的小语言。翻译这个正则表达式,我们要寻找的子串以单个小写字母、大写字母或数字"[a-zA-Z0-9]"开头,后跟零个或多个非空白字符 (\S*),再后跟一个@符号,接着是零个或多个非空白字符 (\S*),最后是一个大写或小写字母。请注意,我们将 + 替换为 * 来表示零个或多个非空白字符,因为 [a-zA-Z0-9] 已经表示一个非空白字符。记住,* 或 + 适用于加号或星号左侧的单个字符。
如果我们在程序中使用了这个表达式,我们的数据会更加整洁:
# Search for lines that have an at sign between characters
# The characters must be a letter or number
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('[a-zA-Z0-9]\S*@\S*[a-zA-Z]', line)
if len(x) > 0:
print(x)
# Code: http://www.py4e.com/code3/re07.py
...
['wagnermr@iupui.edu']
['cwen@iupui.edu']
['postmaster@collab.sakaiproject.org']
['200801032122.m03LMFo4005148@nakamura.uits.iupui.edu']
['source@collab.sakaiproject.org']
['source@collab.sakaiproject.org']
['source@collab.sakaiproject.org']
['apache@localhost']
请注意,在 source@collab.sakaiproject.org 行中,我们的正则表达式消除了字符串末尾的两个字母(">;”)。这是因为当我们把 [a-zA-Z] 附加到正则表达式的末尾时,我们要求正则表达式解析器找到的任何字符串必须以字母结尾。因此,当它看到 "sakaiproject.org>;" 末尾的 ">" 时,它简单地停在它找到的最后一个“匹配”字母处(即 "g" 是最后一个有效匹配)。
另外请注意,该程序的输出是一个 Python 列表,其中包含一个字符串作为列表中的唯一元素。
0.12.1.3. 结合查找与提取¶
如果我们要查找以字符串 "X-" 开头的行中的数字,例如:
X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
我们不仅仅想要来自任意行的任意浮点数。我们只想要从具有上述语法的行中提取数字。
我们可以构建以下正则表达式来选择行:
^X-.*: [0-9.]+
翻译这段话,我们是在说,我们希望以 `X-` 开头的行,后跟零个或多个字符 (`.*`),再跟一个冒号 (`:`) 和一个空格。在该空格之后,我们寻找一个或多个字符,这些字符要么是数字 (0-9),要么是句号 `[0-9.]+`。请注意,在方括号内部,句号匹配实际的句号(即,它不是方括号之间的通配符)。
这是一个非常紧凑的表达式,它将几乎只匹配我们感兴趣的行,如下所示:
# Search for lines that start with 'X' followed by any non
# whitespace characters and ':'
# followed by a space and any number.
# The number can include a decimal.
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
if re.search('^X\S*: [0-9.]+', line):
print(line)
# Code: http://www.py4e.com/code3/re10.py
当我们运行程序时,我们会看到数据被很好地过滤,只显示我们正在查找的行。
X-DSPAM-Confidence: 0.8475
X-DSPAM-Probability: 0.0000
X-DSPAM-Confidence: 0.6178
X-DSPAM-Probability: 0.0000
...
但如今我们必须解决提取数字的问题。虽然使用 `split` 足够简单,但我们可以利用正则表达式的另一个特性,同时进行搜索和解析行。
圆括号是正则表达式中的另一种特殊字符。当您在正则表达式中添加圆括号时,它们在匹配字符串时会被忽略。但是,当您使用``findall()``时,圆括号表示您希望整个表达式匹配,但您只对提取匹配正则表达式的子字符串的一部分感兴趣。
因此我们对程序做了如下修改:
# Search for lines that start with 'X' followed by any
# non whitespace characters and ':' followed by a space
# and any number. The number can include a decimal.
# Then print the number if it is greater than zero.
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^X\S*: ([0-9.]+)', line)
if len(x) > 0:
print(x)
# Code: http://www.py4e.com/code3/re11.py
与其调用 search(),我们给正则表达式中表示浮点数的部分加上括号,以表明我们只希望 findall() 返回匹配字符串中的浮点数部分。
本程序的输出如下:
['0.8475']
['0.0000']
['0.6178']
['0.0000']
['0.6961']
['0.0000']
...
数字仍然在列表中,需要从字符串转换为浮点数,但我们已经利用正则表达式的功能来查找并提取我们感兴趣的信息。
作为该技术的一个又一个例子,如果你查看该文件,你会发现存在若干行形式如下:
Details: http://source.sakaiproject.org/viewsvn/?view=rev&rev=39772
如果我们想使用与上述相同的技术提取所有的修订号(这些行末尾的整数),我们可以编写如下程序:
# Search for lines that start with 'Details: rev='
# followed by numbers
# Then print the number if one is found
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^Details:.*rev=([0-9]+)', line)
if len(x) > 0:
print(x)
# Code: http://www.py4e.com/code3/re12.py
将我们的正则表达式翻译过来,我们要查找以 Details: 开头、接着是任意数量的字符 (.*)、然后是 rev=、最后是一个或多个数字的行。我们想要找到匹配整个表达式的行,但只想要提取行尾的整数,因此我们用括号将 [0-9]+ 包围起来。
当我们运行程序时,我们得到以下输出:
['39772']
['39771']
['39770']
['39769']
...
记住,[0-9]+ 是“贪婪”的,它试图在提取这些数字之前尽可能多地生成一串数字。这种“贪婪”行为就是我们为什么能为每个数字获取全部五个数字的原因。正则表达式模块会向两个方向扩展,直到遇到非数字字符,或到达行的开头或结尾。
现在我们可以使用正则表达式来重做本书前面的一道练习,我们当时对每封邮件消息的时间感兴趣。我们查找如下形式的行:
From stephen.marquard@uct.ac.za Sat Jan 5 09:14:16 2008
并且希望提取每一行的小时。之前我们通过两次调用 split 来实现这一点:首先将行拆分为单词,然后取出第五个单词,再在冒号字符处将其拆分,以提取我们感兴趣的两个字符。
虽然这种方法可行,但实际上会产生相当脆弱的代码,该代码假设行格式良好。如果你添加足够的错误检查(或一个大的 try/except 块)以确保你的程序在遇到格式错误的行时永远不会失败,那么代码将膨胀到 10-15 行难以阅读的代码。
我们可以通过以下正则表达式以更简单的方式完成此操作:
^From .* [0-9][0-9]:
该正则表达式的含义是:我们寻找以 From``(注意其后的空格)开头,后跟任意数量的字符(.*``),再跟一个空格,接着是两个数字 [0-9][0-9],最后以一个冒号字符结尾的行。这正是我们要查找的行类型的定义。
为了仅提取小时,我们使用 findall() 并将两位数字用括号括起来,如下所示:
^From .* ([0-9][0-9]):
这导致了以下程序:
# Search for lines that start with From and a character
# followed by a two digit number between 00 and 99 followed by ':'
# Then print the number if one is found
import re
hand = open('mbox-short.txt')
for line in hand:
line = line.rstrip()
x = re.findall('^From .* ([0-9][0-9]):', line)
if len(x) > 0: print(x)
# Code: http://www.py4e.com/code3/re13.py
当程序运行时,它产生以下输出:
['09']
['18']
['16']
['15']
...
0.12.1.4. 转义字符¶
由于我们在正则表达式中使用特殊字符来匹配行的开头或结尾,或指定通配符,因此我们需要一种方法来指示这些字符是“普通”的,并且我们希望匹配实际的字符,例如美元符号或脱字符。
我们可以通过在该字符前添加反斜杠来指示我们只想匹配该字符。例如,我们可以使用以下正则表达式查找金额。
import re
x = 'We just received $10.00 for cookies.'
y = re.findall('\$[0-9.]+',x)
由于我们在美元符号前添加了反斜杠,它实际上匹配输入字符串中的美元符号,而不是匹配“行尾”,而正则表达式的其余部分匹配一个或多个数字或句点字符。Note: 在方括号内,字符不是“特殊”的。因此,当我们说 [0-9.] 时,它实际上意味着数字或句点。在方括号外,句点是“通配符”字符,匹配任何字符。在方括号内,句点就是句点。
0.12.1.5. 摘要¶
虽然这仅触及了正则表达式的皮毛,但我们已对正则表达式的语言有所了解。它们是包含特殊字符的查找字符串,这些字符向正则表达式系统传达您的意愿,即定义什么是“匹配”以及从匹配字符串中提取什么。以下是一些特殊字符和字符序列:
^ 匹配行首。
$ 匹配行尾。
. 匹配任意字符(通配符)。
\s 匹配空白字符。
\S 匹配一个非空白字符(与 \s 相反)。
* 适用于前一个字符,并指示匹配零次或多次。
*? 适用于前一个字符,并指示以“非贪婪模式”匹配零次或多次。
+ 适用于前一个字符,并指示匹配一次或多次。
+? 适用于前一个字符,并指示以“非贪婪模式”匹配一次或多次。
? 适用于前一个字符,并指示匹配零次或一次。
?? 适用于前一个字符,并指示在“非贪婪模式”下匹配零次或一次。
[aeiou] 匹配单个字符,只要该字符在指定集合中即可。在本例中,它将匹配"a"、"e"、"i"、"o"或"u",但不会匹配其他字符。
[a-z0-9] 您可以使用减号指定字符范围。本例为单个字符,必须为小写字母或数字。
[^A-Za-z] 当集合表示法中的第一个字符是脱字符时,它会反转逻辑。此示例匹配单个字符,该字符为任何 非 大写或小写字母。
( ) 当在正则表达式中添加括号时,它们用于匹配的目的被忽略,但在使用 findall() 时,允许您提取匹配字符串的特定子集,而不是整个字符串。
\b 匹配空字符串,但仅位于单词的开头或结尾。
\B 匹配空字符串,但不在单词的开头或结尾处。
\d 匹配任意十进制数字;等价于集合 [0-9]。
\D 匹配任意非数字字符;等价于集合 [^0-9]。
0.12.1.6. 针对 Unix / Linux 用户的附加部分¶
自 20 世纪 60 年代起,使用正则表达式搜索文件的功能就被内置到 Unix 操作系统中,并且以某种形式存在于几乎所有编程语言中。
事实上,Unix 内置了一个名为 grep (Generalized Regular Expression Parser)的命令行程序,其功能与本章节中的 search() 示例大致相同。因此,如果您拥有 Macintosh 或 Linux 系统,可以在命令行窗口中尝试以下命令。
$ grep '^From:' mbox-short.txt
From: stephen.marquard@uct.ac.za
From: louis@media.berkeley.edu
From: zqian@umich.edu
From: rjlowe@iupui.edu
这告诉 grep 显示文件中以字符串 "From:" 开头的行。如果你尝试 grep 命令并阅读 grep 的文档,你会发现 Python 中的正则表达式支持与 grep 中的正则表达式支持之间存在一些微妙的差异。例如,grep 不支持非空白字符 \S,因此你需要使用稍微复杂的集合表示法 [^ ],其含义是匹配任何非空格字符。
0.12.1.7. 调试¶
Python 有一些简单且基础的内置文档,如果您需要快速复习以触发对某个特定方法确切名称的记忆,这些文档会非常有帮助。这些文档可以在 Python 解释器的交互模式下查看。
您可以使用 help() 启动交互式帮助系统。
>>> help()
help> modules
如果您知道想要使用的模块,可以使用 `dir()` 命令按如下方式查找该模块中的方法:
>>> import re
>>> dir(re)
[.. 'compile', 'copy_reg', 'error', 'escape', 'findall',
'finditer', 'match', 'purge', 'search', 'split', 'sre_compile',
'sre_parse', 'sub', 'subn', 'sys', 'template']
您也可以使用 dir 命令获取特定方法的少量文档。
>>> help (re.search)
Help on function search in module re:
search(pattern, string, flags=0)
Scan through string looking for a match to the pattern, returning
a match object, or None if no match was found.
>>>
内置文档并不十分详尽,但在您急需或缺乏网页浏览器或搜索引擎时,它仍能提供帮助。
0.12.1.8. 术语表¶
脆弱代码 当输入数据处于特定格式时能正常工作,但若出现任何偏离正确格式的情况则容易崩溃的代码。我们称之为“脆弱代码”,因为它极易出错。贪婪匹配 正则表达式中 + 和 * 字符向外扩展以匹配尽可能大的字符串的概念。grep 大多数 Unix 系统中可用的命令,用于搜索文本文件中匹配正则表达式的行。该命令名称代表“通用正则表达式解析器”。正则表达式 用于表达更复杂搜索字符串的语言。正则表达式可以包含特殊字符,指示搜索仅匹配行的开头或结尾,或具有许多其他类似功能。通配符 匹配任意字符的特殊字符。在正则表达式中,通配符字符是句点。
0.12.1.9. 练习题¶
练习 1:编写一个简单的程序来模拟 Unix 中 ```grep``` 命令的操作。请用户输入一个正则表达式,并统计匹配该正则表达式的行数:
$ python grep.py
Enter a regular expression: ^Author
mbox.txt had 1798 lines that matched ^Author
$ python grep.py
Enter a regular expression: ^X-
mbox.txt had 14368 lines that matched ^X-
$ python grep.py
Enter a regular expression: java$
mbox.txt had 4175 lines that matched java$
练习 2:编写一个程序来查找如下形式的行:
New Revision: 39772
使用正则表达式和 ``findall()`` 方法从每一行中提取数字。计算这些数字的平均值,并将其作为整数打印出来。
Enter file:mbox.txt
38549
Enter file:mbox-short.txt
39756
如果您在本书中发现错误,欢迎使用 Github 发送修正。
