Python学习笔记九11:re模块

8. re

有些人面临问题时会想:“我知道,我将使用正则表达式来解决这个问题。”这
让他们面临的问题变成了两个。
——Jamie Zawinski
模块re提供了对正则表达式的支持。如果你听说过正则表达式,就可能知道它们有多厉害;如果没有,就等着大吃一惊吧。
然而,需要指出的是,要掌握正则表达式有点难。关键是每次学习一点点:只考虑完成特定任务所需的知识。预先将所有的知识牢记在心毫无意义。本节描述模块re和正则表达式的主要功能,让你能够快速上手。

(1) 正则表达式是什么

正则表达式是可匹配文本片段的模式。最简单的正则表达式为普通字符串,与它自己匹配。换而言之,正则表达式’python’与字符串’python’匹配。你可使用这种匹配行为来完成如下工作:在文本中查找模式,将特定的模式替换为计算得到的值,以及将文本分割成片段。
compile函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象,该对象拥有一系列方法用于正则表达式匹配和替换。
正则表达式的设计思想是用描述性语言为字符串定义一个规则,凡符合规则的字符串,我们就认为匹配,否则就不匹配。正则表达式的大致匹配过程是:依次拿出表达式和文本中的字符比较,如果每一个字符都能匹配,匹配就成功;一旦有匹配不成功的字符,匹配就失败。
在正则表达式中,如果直接给出字符,就是精确匹配。
通配符(.)
正则表达式可与多个字符串匹配,你可使用特殊字符来创建这种正则表达式。例如,句点与除换行符外的其他字符都匹配,因此正则表达式’.ython’与字符串’python’和’jython’都匹配。它还与’qython’、’+ython’和’ ython’(第一个字符为空格)等字符串匹配,但不与’cpython’、'ython’等字符串匹配,因为句点只与一个字符匹配,而不与零或两个字符匹配。
句点与除换行符外的任何字符都匹配,因此被称为通配符(wildcard)。

对特殊字符进行转义(\或r)
普通字符只与自己匹配,但特殊字符的情况完全不同。例如,假设要匹配字符串’python.org’,可以直接使用模式’python.org’吗?可以,但它也与’pythonzorg’匹配(句点与除换行符外的其他字符都匹配),这可能不是你想要的结果。要让特殊字符的行为与普通字符一样,可对其进行转义:在它前面加上一个反斜杠。因此,在这个示例中,可使用模式’python\.org’,它只与’python.org’匹配。
请注意,为表示模块re要求的单个反斜杠,需要在字符串中书写两个反斜杠,让解释器对其进行转义。换而言之,这里包含两层转义:解释器执行的转义和模块re执行的转义。实际上,在有些情况下也可使用单个反斜杠,让解释器自动对其进行转义,但请不要这样依赖解释器。如果你厌烦了两个反斜杆,可使用原始字符串,如r’python.org’。

表 特殊字符类在正则表达式中的应用
实例 描述
. 匹配除换行符之外的任何单个字符。要匹配包括’\n’在内的任意字符,请使用’[.\n]‘的模式
\d 匹配一个数字字符,等价于[0-9]
\D 匹配一个非数字字符,等价于[^0-9]
\s 匹配任意空白字符,包括空格、制表符、换页符等,等价于[\f\n\r\t\v]
\S 匹配任意非空白字符,等价于[^\f\n\r\t\v]
\w 匹配包括下划线的任意单词字符,等价于’[A-Za=z0-9]’
\W 匹配任意非单词字符,等价于’[^A-Za=z0-9]’
用\d可以匹配一个数字,用\w可以匹配一个字母或数字,例如:
‘00\d’可以匹配’007’
‘\d\d\d’可以匹配’123
‘\w\w\d’可以匹配’py3’
下面来看一个更复杂的例子:\d{3}\s+\d{3,8}。该字符串从左到右解读如下:
\d{3}表示匹配3个数字;\s可以匹配一个空格,所以\s+表示至少有一个空格;\d{3,8}表示匹配3-8个数字。
综上所述,正则表达式可以匹配以任意个数的空格隔开的带区号的电话号码。比如匹配010-12345:由于-是特殊字符所以需要转义,正则表达式为\d{3}-\d{3,8}。
不过如果需要匹配带有字符串的字符串(如’010-12345’),使用上面的方式就做不到了,在此我们继续讨论更复杂的匹配方式。

字符集([])
匹配任何字符很有用,但有时你需要更细致地控制。为此,可以用方括号将一个子串括起,创建一个所谓的字符集。这样的字符集与其包含的字符都匹配,例如’[pj]ython’与’python’和’jython’都匹配,但不与其他字符串匹配。你还可使用范围,例如’[a-z]‘与a~z的任何字母都匹配。你还可组合多个访问,方法是依次列出它们。
例如’[a-zA-Z0-9_]‘与大写字母、小写字母、数字或下划线都匹配,这种方式可以在一些场所做输入值或命名的合法性校验。
[0-9a-zA-Z_]+可以匹配至少由一个数字、字母或下划线组成的字符串,这种方式可以检验一个字符串是否包含数字、字母或下划线。
[a-zA-Z_][0-9a-zA-Z_]可以匹配由字母或下划线开头,后接任意个数字、字母或下划线组成的字符串,也就是Python的合法变量。
[a-zA-Z_][0-9a-zA-Z_]{0,19}更精确地限制了变量的长度是1-20个字符。(前面的1加后面最多19个字符)
请注意,字符集只能匹配一个字符。
要指定排除字符集,可在开头添加一个字符,例如’[abc]‘与除a、b和c外的其他任何字符都匹配。
字符集中的特殊字符
一般而言,对于诸如句点、星号和问号等特殊字符,要在模式中将其用作字面字符而不是正则表达式运算符,必须使用反斜杠对其进行转义。在字符集中,通常无需对这些字符进行转义,但进行转义也是完全合法的。然而,你应牢记如下规则。
脱字符(^)位于字符集开头时,除非要将其用作排除运算符,否则必须对其进行转义。换而言之,除非有意为之,否则不要将其放在字符集开头。
同样,对于右方括号(])和连字符(-),要么将其放在字符集开头,要么使用反斜杠对其进行转义。实际上,如果你愿意,也可将连字符放在字符集末尾。
二选一和子模式(|和())
需要以不同的方式处理每个字符时,字符集很好,但如果只想匹配字符串’python’和’perl’,该如何办呢?使用字符集或通配符无法指定这样的模式,而必须使用表示二选一的特殊字符:管道字符(|)。所需的模式为’python|perl’。
然而,有时候你不想将二选一运算符用于整个模式,而只想将其用于模式的一部分。为此,可将这部分(子模式)放在圆括号内。对于前面的示例,可重写为’p(ython|erl)’。请注意,单个字符也可称为子模式。
可选模式和重复模式(?和
+{m,n})
通过在子模式后面加上问号(?),可将其指定为可选的,即可包含可不包含。例如,下面这个不太好懂的模式:
r’(http://)?(www.)?python.org’
只与下面这些字符串匹配:
‘http://www.python.org’
‘http://python.org’
‘www.python.org’
‘python.org’
对于这个示例,需要注意如下几点。
我对句点进行了转义,以防它充当通配符。
为减少所需的反斜杠数量,我使用了原始字符串。
每个可选的子模式都放在圆括号内。
每个可选的子模式都可以出现,也可以不出现。
问号表示可选的子模式可出现一次,也可不出现。还有其他几个运算符用于表示子模式可重复多次。
(pattern):pattern可重复0、1或多次。
(pattern)+:pattern可重复1或多次。
(pattern){n}:模式可重复n次
(pattern){m,n}:模式可重复m~n次。
例如,r’w
.python.org’与’www.python.org’匹配,也与’.python.org’、‘ww.python.org’和’wwwwwww.python.org’匹配。同样,r’w+.python.org’与’w.python.org’匹配,但与’.python.org’不匹配,而r’w{3,4}.python.org’只与’www.python.org’和’wwww.python.org’匹配。
注意:在这里,术语匹配指的是与整个字符串匹配,而函数match(参见表10-9)只要求模式与字符串开头匹配。
字符串的开头和末尾
你可能想确定字符串的开头是否与模式’ht+p’匹配,为此可使用脱字符(’’)来指出这一点。例如,'ht+p’与’http://python.org’和’htttttp://python.org’匹配,但与’www.http.org’不匹配。同样,要指定字符串末尾,可使用美元符号(KaTeX parse error: Undefined control sequence: \d at position 12: )。^表示行的开头,^\̲d̲表示必须以数字开头;表示行的结束,\d$表示必须以数字结束。

(2)模块re的内容
模块re包含多个使用正则表达式的函数,表10-9描述了其中最重要的一些。
一般使用re的步骤是先将正则表达式的字符串形式编译为pattern实例,然后使用pattern实例处理文本并获得匹配结果(一个match函数),最后使用match函数获得信息,进行其他操作。
表10-9 模块re中一些重要的函数
函数 描 述
compile()
search(pattern,string[,flags])
match(pattern,string[,flags])
split(pattern,string[,maxsplit=0])
findall(pattern,string)

sub(pat,repl,string[,count=0])

escape(string)
根据包含正则表达式的字符串创建模式对象
在字符串中查找模式
在字符串开头匹配模式
根据模式来分割字符串
返回一个列表,其中包含字符串中所有与模式匹配的子串
将字符串中与模式pat匹配的子串都替换为repl
对字符串中所有的正则表达式特殊字符都进行转义

(2.1)compile()
函数re.compile将用字符串表示的正则表达式转换为模式对象,以提高匹配效率。

re.compile(‘www.python’) #re.compile(‘www.python’)
调用search、match等函数时,如果提供的是用字符串表示的正则表达式,都必须在内部将它们转换为模式对象。通过使用函数compile对正则表达式进行转换后,每次使用它时都无需再进行转换。模式对象也有搜索/匹配方法,因此re.search(pat, string)(其中pat是一个使用字符串表示的正则表达式)等价于pat.search(string)(其中pat是使用compile创建的模式对象)。编译后的正则表达式对象也可用于模块re中的普通函数中。

(2.2)search(pattern,string[,flags])
函数re.search(pattern,string[,flags])在给定字符串中查找第一个与指定正则表达式匹配的子串。如果找到这样的子串,将返回MatchObject(结果为真),否则返回None(结果为假)。鉴于返回值的这种特征,可在条件语句中使用这个函数,如下所示:
if re.search(pat, string):
print(‘Found it!’)
然而,如果你需要获悉有关匹配的子串的详细信息,可查看返回的MatchObject。下一节将更详细地介绍MatchObject。

re.match(‘p’,‘python’) #返回真(MatchObject)
re.match(‘p’, ‘www.python.org’) #返回真(MatchObject)

(2.3)match(pattern,string[,flags])
函数match(pattern,string[,flags])尝试在给定字符串开头查找与正则表达式匹配的子串。

re.match(‘p’,‘python’) #返回真(MatchObject)
re.match(‘p’, ‘www.python.org’) #返回假(None)
注意:函数match在模式与字符串开头匹配时就返回True,而不要求模式与整个字符串匹配。如果要求与整个字符串匹配,需要在模式末尾加上一个美元符号。美元符号要求与字符串末尾匹配,从而将匹配检查延伸到整个字符串。

(2.4)split(pattern,string[,maxsplit=0])
函数re.split(pattern,string[,maxsplit=0])根据与模式匹配的子串来分割字符串。这类似于字符串方法split,但使用正则表达式来指定分隔符,而不是指定固定的分隔符。例如,使用字符串方法split时,可以字符串’, '为分隔符来分割字符串,但使用re. split时,可以空格和逗号为分隔符来分割字符串。

some_text = ‘alpha, beta,gamma delta’
re.split(’[, ]+’, some_text)
[‘alpha’, ‘beta’, ‘gamma’, ‘delta’]
注意:如果模式包含圆括号,将在分割得到的子串之间插入括号中的内容。例如,re.split(‘o(o)’,‘foobar’)的结果为[‘f’, ‘o’, ‘bar’]。
从这个示例可知,返回值为子串列表。参数maxsplit指定最多分割多少次。

re.split(’[, ]+’, some_text, maxsplit=2)
#[‘alpha’, ‘beta’, ‘gamma delta’]

re.split(’[, ]+’, some_text, maxsplit=1)
#[‘alpha’, ‘beta,gamma delta’]

(2.5)findall(pattern,string)
函数re.findall(pattern,string)返回一个列表,其中包含所有与给定模式匹配的子串。例如,要找出字符串包含的所有单词,可像下面这样做:

pat = ‘[a-zA-Z]+’
text = ‘“Hm… Err – are you sure?” he said, sounding insecure.’
re.findall(pat, text)
[‘Hm’, ‘Err’, ‘are’, ‘you’, ‘sure’, ‘he’, ‘said’, ‘sounding’, ‘insecure’]
要查找所有的标点符号,可像下面这样做:

pat = r’[.?-",]+’
re.findall(pat, text)
#[’"’, ‘…’, ‘–’, ‘?"’, ‘,’, ‘.’]
请注意,这里对连字符(-)进行了转义,因此Python不会认为它是用来指定字符范围的(如a-z)。

(2.6)sub(pat,repl,string[,count=0])
函数re.sub(pat,repl,string[,count=0])从左往右将与模式匹配的子串替换为指定内容。返回替换后的字符串。请看下面的示例:

pat = ‘{name}’
text = ‘Dear {name}…’
re.sub(pat, ‘Mr. Gumby’, text)
‘Dear Mr. Gumby…’
当repl是一个方法时,这个方法应当只接收一个参数(match对象),并返回一个字符串用于替换(返回的字符串中不能再引用分组)。count用于指定最多替换次数,不指定时全部替换。
例如:
import re
pt = re.compile(r’(w+)(w+)’)
greeting = ‘i say, hello world!’
print(pt.sub(r’2 1’, greeting))
def func(m):
return m.group(1).title()+’’+m.group(2).title()
print(pt.sub(func,greeting))
执行结果如下:
i say, hello world!
i say, hello world!

(2.7)escape(string)
re.escape(string)是一个工具函数,用于对字符串中所有可能被视为正则表达式运算符的字符进行转义。使用这个函数的情况有:字符串很长,其中包含大量特殊字符,而你不想输入大量的反斜杠;你从用户那里获取了一个字符串(例如,通过函数input),想将其用于正则表达式中。
下面的示例说明了这个函数的工作原理:

re.escape(‘www.python.org’)
#‘www\.python\.org’

re.escape(‘But where is the ambiguity?’)
#‘But\ where\ is\ the\ ambiguity\?’
注意:在表10-9中,注意到有些函数接受一个名为flags的可选参数。这个参数可用于修改正则表达式的解读方式

(3)匹配对象和编组
在模块re中,查找与模式匹配的子串的函数都在找到时返回MatchObject对象。这种对象包含与模式匹配的子串的信息,还包含模式的哪部分与子串的哪部分匹配的信息。这些子串部分称为编组(group)。
编组就是放在圆括号内的子模式,它们是根据左边的括号数编号的,其中编组0指的是整个模式。因此,在下面的模式中:
‘There (was a (wee) (cooper)) who (lived in Fyfe)’
包含如下编组:
0 There was a wee cooper who lived in Fyfe
1 was a wee cooper
2 wee
3 cooper
4 lived in Fyfe
通常,编组包含诸如通配符和重复运算符等特殊字符,因此你可能想知道与给定编组匹配的内容。
例如,在下面的模式中:
r’www.(.+).com$’
编组0包含整个字符串,而编组1包含’www.‘和’.com’之间的内容。通过创建类似于这样的模式,可提取字符串中你感兴趣的部分。
表10-10 re匹配对象的重要方法
函数 描 述
group([group1, …])
groups()
start([group])
end([group])

span([group]) 获取与给定子模式(编组)匹配的子串
以元组形式返回全部分组截获的字符串
返回与给定编组匹配的子串的起始位置
返回与给定编组匹配的子串的终止位置(与切片一样,不包含终止位置)
返回与给定编组匹配的子串的起始和终止位置

(3.1)group([group1, …])
方法group返回与模式中给定编组匹配的子串。如果没有指定编组号,则默认为0。如果只指定了一个编组号(或使用默认值0),将只返回一个字符串;否则返回一个元组,其中包含与给定编组匹配的子串。

a = “123abc456”
print(re.search("([0-9])([a-z])([0-9]*)",a).group(0))
#123abc456

print(re.search("([0-9])([a-z])([0-9]*)",a).group(0,1))
#(‘123abc456’, ‘123’)
注意:除整个模式(编组0)外,最多还可以有99个编组,编号为1~99。

(3.2)groups()
以元组形式返回全部分组截获的字符串,相当于调用group(1,2,…last)。default表示没有截获字符串的组以这个值代替,默认为None。

print(re.search("([0-9])([a-z])([0-9]*)",a).groups())
(‘123’, ‘abc’, ‘456’)

(3.3)start([group])
方法start([group])返回与给定编组(默认为0,即整个模式)匹配的子串的起始索引。

print(re.search(“23”,a).start(0))
#1

(3.4)end([group])
方法end([group])类似于start,但返回终止索引加1

print(re.search(“23”,a).end(0))
#3 #终止索引2加1等于3

(3.5)span([group])
方法span([group])返回一个元组,其中包含与给定编组(默认为0,即整个模式)匹配的子串的起始索引和终止索引。

print(re.search(“23”,a).span(0))
#(1, 3)

(4)替换中的组号和函数
在第一个re.sub使用示例中,我只是将一个子串替换为另一个。这也可使用字符串方法replace(参见3.4节)轻松地完成。当然,正则表达式很有用,因为它们让你能够以更灵活的方式进行搜索,还让你能够执行更复杂的替换。
为利用re.sub(pat,repl,string[,count=0])的强大功能,最简单的方式是在替代字符串中使用组号。在替换字符串中,任何类似于’\n’的转义序列都将被替换为与模式中编组n匹配的字符串。
例如,假设要将’something’替换为’something’,其中前者是在纯文本文档(如电子邮件)中表示突出的普通方式,而后者是相应的HTML代码(用于网页中)。下面先来创建一个正则表达式。

emphasis_pattern = r’*([^*]+)*’
请注意,正则表达式容易变得难以理解,因此为方便其他人(也包括你自己)以后阅读代码,使用有意义的变量名很重要。
提示:要让正则表达式更容易理解,一种办法是在调用模块re中的函数时使用标志VERBOSE。这让你能够在模式中添加空白(空格、制表符、换行符等),而re将忽略它们——除非将它放在字符类中或使用反斜杠对其进行转义。在这样的正则表达式中,你还可添加注释。下述代码创建的模式对象与emphasis_pattern等价,但使用了VERBOSE标志:

emphasis_pattern = re.compile(r’’’
* # 起始突出标志——一个星号
( # 与要突出的内容匹配的编组的起始位置
[^*]+ # 与除星号外的其他字符都匹配
) # 编组到此结束
* # 结束突出标志
‘’’, re.VERBOSE)
创建模式后,就可使用re.sub来完成所需的替换了。

re.sub(emphasis_pattern, r’\1’, ‘Hello, world!’)
‘Hello, world!’
如你所见,成功地将纯文本转换成了HTML代码。
然而,通过将函数用作替换内容,可执行更复杂的替换。这个函数将MatchObject作为唯一的参数,它返回的字符串将用作替换内容。换而言之,你可以对匹配的字符串做任何处理,并通过细致的处理来生成替换内容。你可能会问,这有何用途呢?等你开始尝试使用正则表达式后,将发现这种机制的用途非常多,随后会介绍其中的一个。

(4.1)贪婪和非贪婪模式
重复运算符默认是贪婪的,这意味着它们将匹配尽可能多的内容。例如,假设重写了前面的突出程序,在其中使用了如下模式:

emphasis_pattern = r’*(.+)*’
这个模式与以星号打头和结尾的内容匹配。好像很完美,不是吗?但情况并非如此。

re.sub(emphasis_pattern, r’\1’, ‘This is it!’)
This* is *it!’
如你所见,这个模式匹配了从第一个星号到最后一个星号的全部内容,其中包含另外两个星号!这就是贪婪的意思:能匹配多少就匹配多少。
在这里,你想要的显然不是这种过度贪婪的行为。在你知道不应将某个特定的字符包含在内时,本章前面的解决方案(使用一个匹配任何非星号字符的字符集)很好。下面再来看另一个场景:如果使用’something’来表示突出呢?在这种情形下,在要强调的内容中包含单个星号不是问题,但如何避免过度贪婪呢?
这实际上很容易,只需使用重复运算符的非贪婪版即可。对于所有的重复运算符,都
可在后面加上问号来将其指定为非贪婪的。

emphasis_pattern = r’**(.+?)**’
re.sub(emphasis_pattern, r’\1’, ‘This is it!’)
This is it!’
这里使用的是运算符+?而不是+。这意味着与以前一样,这个模式将匹配一个或多个通配符,但匹配尽可能少的内容,因为它是非贪婪的。因此,这个模式只匹配到下一个’**’,即它末尾的内容。如你所见,效果很好。

(5)找出发件人
你曾将邮件保存为文本文件吗?如果这样做过,你可能注意到文件开头有大量难以理解的文本,如代码清单10-9所示。
代码清单10-9 一组虚构的邮件头
From foo@bar.baz Thu Dec 20 01:22:50 2008
Return-Path: foo@bar.baz
Received: from xyzzy42.bar.com (xyzzy.bar.baz [123.456.789.42])
by frozz.bozz.floop (8.9.3/8.9.3) with ESMTP id BAA25436
for magnus@bozz.floop; Thu, 20 Dec 2004 01:22:50 +0100 (MET)
Received: from [43.253.124.23] by bar.baz
(InterMail vM.4.01.03.27 201-229-121-127-20010626) with ESMTP
id <20041220002242.ADASD123.bar.baz@[43.253.124.23]>; Thu, 20 Dec 2004 00:22:42 +0000
User-Agent: Microsoft-Outlook-Express-Macintosh-Edition/5.02.2022
Date: Wed, 19 Dec 2008 17:22:42 -0700
Subject: Re: Spam
From: Foo Fie foo@bar.baz
To: Magnus Lie Hetland magnus@bozz.floop
CC: Mr.Gumby@bar.baz
Message-ID: B8467D62.84F%foo@baz.com
In-Reply-To: 20041219013308.A2655@bozz.floop Mime- version: 1.0
Content-type: text/plain; charset=“US-ASCII” Content-transfer-encoding: 7bit
Status: RO
Content-Length: 55
Lines: 6
So long, and thanks for all the spam!
Yours,
Foo Fie
我们来尝试找出这封邮件的发件人。如果你仔细查看上面的文本,肯定能找出发件人(尤其是看到邮件末尾的签名时)。但你能找出普适的规律吗?如何提取发件人姓名(不包含邮件地址)呢?如何列出邮件头中提及的所有邮件地址呢?先来解决第一个问题。
包含发件人的文本行以’From: '打头,并以包含在尖括号(<和>)内的邮件地址结尾,你要提取的是这两部分之间的文本。如果使用模块fileinput,这个任务应该很容易完成。解决这个问题的程序如代码清单10-10所示。
代码清单10-10 找出发件人的程序

find_sender.py

import fileinput, re
pat = re.compile(‘From: (.) <.?>$’)
for line in fileinput.input():
m = pat.match(line)
if m: print(m.group(1))
可像下面这样运行这个程序(假设电子邮件保存在文本文件message.eml中):
$ python find_sender.py message.eml
Foo Fie
对于这个程序,应注意如下几点。
 为提高处理效率,我编译了正则表达式。
 我将用于匹配要提取文本的子模式放在圆括号内,使其变成了一个编组。
 我使用了一个非贪婪模式,使其只匹配最后一对尖括号(以防姓名也包含尖括号)。
 我使用了美元符号指出要使用这个模式来匹配整行(直到行尾)。
 我使用了if语句来确保匹配后才提取与特定编组匹配的内容。
要列出邮件头中提及的所有邮件地址,需要创建一个只与邮件地址匹配的正则表达式,然后使用方法findall找出所有与之匹配的内容。为避免重复,可将邮件地址存储在本章前面介绍的集合中。最后,提取键,将它们排序并打印出来。
import fileinput, re
pat = re.compile(r’[a-z-.]+@[a-z-.]+’, re.IGNORECASE)
addresses = set()
for line in fileinput.input():
for address in pat.findall(line):
addresses.add(address)
for address in sorted(addresses):
print address
将代码清单10-9所示的邮件作为输入时,这个程序的输出如下:
Mr.Gumby@bar.baz
foo@bar.baz
foo@baz.com
magnus@bozz.floop
请注意,排序时大写字母在小写字母之前。
注意:这里并没有完全按问题的要求做。问题要求找出邮件头中的地址,但这个程序找出了整个文件中的所有地址。为避免这一点,可在遇到空行后调用fileinput.close(),因为邮件头不可能包含空行。如果有多个文件,也可在遇到空行后调用fileinput.nextfile()来处理下一个文件。

(6)模板系统示例
模板(template)是一种文件,可在其中插入具体的值来得到最终的文本。
例如,可能有一个只需插入收件人姓名的邮件模板。Python提供了一种高级模板机制:字符串格式设置。使用正则表达式可让这个系统更加高级。假设要把所有的’[something]’(字段)都替换为将something作为Python表达式计算得到的结果。因此,下面的字符串:
‘The sum of 7 and 9 is [7 + 9].’ 表达式
应转换为:
‘The sum of 7 and 9 is 16.’
另外,你还希望能够在字段中进行赋值,使得下面的字符串:
‘[name=“Mr. Gumby”]Hello, [name]’ 语句
转换成:
‘Hello, Mr. Gumby’
这看似很复杂,我们来看看可供使用的工具。
可使用正则表达式来匹配字段并提取其内容。
可使用eval来计算表达式字符串,并提供包含作用域的字典。可在try/except语句中执行这种操作。如果出现SyntaxError异常,就说明你处理的可能是语句(如赋值语句)而不是表达式,应使用exec来执行它。
可使用exec来执行语句字符串(和其他语句),并将模板的作用域存储到字典中。
可使用re.sub将被处理的字符串替换为计算得到的结果。
import fileinput, re

与使用方括号括起的字段匹配

field_pat = re.compile(r’[(.+?)]’) #定义一个用于匹配字段的模式。

我们将把变量收集到这里:

scope = {} #创建一个用作模板作用域的字典。

用于调用re.sub:

def replacement(match):
code = match.group(1)
try:
# 如果字段为表达式,就返回其结果:
return str(eval(code, scope))
except SyntaxError:
# 否则在当前作用域内执行该赋值语句
# 并返回一个空字符串
return ‘’

获取所有文本并合并成一个字符串:

#(还可采用其他办法来完成这项任务,详情请参见第11章)
lines = []

使用fileinput读取所有的行,将它们放在一个列表中

for line in fileinput.input():
lines.append(line)

再将其合并成一个大型字符串。

text = ‘’.join(lines)

替换所有与字段模式匹配的内容:

调用re.sub来使用替换函数来替换所有与模式field_pat匹配的字段,并将结果打印出来。

print(field_pat.sub(replacement, text))
简而言之,这个程序做了如下事情。
定义一个替换函数,其功能如下。
从match中获取与编组1匹配的内容,并将其存储到变量code中。
将作用域字典作为命名空间,并尝试计算code,再将结果转换为字符串并返回它。如果成功,就说明这个字段是表达式,因此万事大吉;否则(即引发了SyntaxError异常),就进入下一步。
在对表达式进行求值时使用的命名空间(作用域字典)中执行这个字段,并返回一个空字符串(因为赋值语句没有结果)。
这就创建了一个强大的模板系统。通过使用标准库,Python的功能变得非常强大。为结束这个示例,下面来测试一下这个模板系统:尝试对代码清单10-12所示的简单文件运行它。
[x = 2]
[y = 3]
The sum of [x] and [y] is [x + y].
你应看到如下输出:
The sum of 2 and 3 is 5.
别急,还可以做得更好!由于使用了fileinput,因此可依次处理多个文件。这意味着可以使用一个文件来定义变量的值,并将另一个文件用作模板,以便在其中插入这些值。
例如,可能有一个包含定义的文件(magnus.txt,如代码清单10-13所示),还有一个模板文件(template.txt,如代码清单10-14所示)。
[name = ‘Magnus Lie Hetland’ ]
[email = ‘magnus@foo.bar’ ]
[language = ‘python’ ]
代码清单10-14 一个模板
[import time]
Dear [name],
I would like to learn how to program. I hear you
use the [language] language a lot – is it something I
should consider?
And, by the way, is [email] your correct email address?
Fooville, [time.asctime()]
Oscar Frozzbozz
import time并非赋值语句(而是用于做准备工作的语句),但由于程序没那么挑剔(使用了
一条简单的try/except语句),它支持任何可使用eval和exec进行处理的表达式和语句。可像下面
这样运行这个程序(假设是在UNIX命令行中):
$ python templates.py magnus.txt template.txt
你将看到类似于下面的输出:
Dear Magnus Lie Hetland,
I would like to learn how to program. I hear you use the python language a lot – is it something I
should consider?
And, by the way, is magnus@foo.bar your correct email address?
Fooville, Mon Jul 18 15:24:10 2016
Oscar Frozzbozz
虽然这个模板系统能够执行非常复杂的替换,但也存在一些缺陷。例如,如果能够以更灵活的方式编写定义文件就好了。如果使用execfile来执行它,就可使用普通Python语法了。这样还将修复输出开头包含空行的问题。
你还能想出其他改进这个程序的方式吗?对于这个程序使用的概念,你还能想到它们的其他用途吗?无论要精通哪种编程语言,最佳的方式都是尝试使用它——找出其局限性和长处。看看你能不能重写这个程序,让它做得更好,并满足你的需求。

(7)编译
当我们在Python中使用正则表达式时,re模块内部会做两件事情:
1.编译正则表达式,如果正则表达式的字符串本身不合法,就会报错。
2.用编译后的正则表达式匹配字符串。
如果一个正则表达式需要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,这样重复使用时就不需要编译这个步骤了,直接匹配即可,例如:
re_telephone = re.compile(r’^(\d{3})-(\d{3,8})$’)
print(re_telephone.match(‘010-12345’).groups())
print(re_telephone.match(‘010-8086’).groups())
执行结果如下:
(‘010’, ‘12345’)
(‘010’, ‘8086’)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值