15ch
15.1引言
核心笔记:当我们讨论与字符串中模式有关的正则表达式时,我们会用术语“匹配”matching,指的是模式匹配pattern-mathcing,在Python专门术语中,有两种主要方式完成模式匹配:搜索searching和匹配matching。搜索指的是在字符串任意部分中搜索匹配的模式,匹配指的是判断一个字符串能否从起始处全部或部分匹配某个模式。搜索通过search()函数或方法来实现,而匹配是以调用match()函数或方法来实现。
15.2正则表达式使用的特殊符号和字符
首先介绍最常用的元字符(metacharacter)---特殊字符和符号。
记号 | 说明 | 正则表达式样例 |
literal | 匹配字符串的值 | foo |
re1|re2 | 匹配字符串re1或re2 | foo|bar |
. | 匹配任何字符(换行符除外 | b.b |
^ | 匹配字符串的开始 | ^Dear |
$ | 匹配字符串的结尾 | /bin/*sh$ |
* | 匹配前面出现的正则表达式零次或多次 | [A-Za-z0-9]* |
+ | 匹配前面出现的正则表达式一次或多次 | [a-z]+\.com |
? | 匹配前面出现的正则表达式零次或一次 | goo? |
{N} | 匹配前面重复出现N次的正则表达式 | [0-9]{3} |
{M,N} | 匹配前面重复出现M到N次的正则表达式 | [0-9]{5,9} |
[...] | 匹配字符组里出现的任意一个字符 | [aeiou] |
[..x-y..] | 匹配从字符x到y中的任意一个字符 | [0-9],[A-Za-z] |
[^...] | 不匹配此字符集中出现的任何一个字符,或某一范围内的字符 | [^aeiou],[^A-Za-z0-9] |
(*|+|?|{})? | 用于将之前的符号改成?版本的 | .*?[a-z] |
(...) | 匹配封闭括号中正则表达式,并保存为子组 | ([0-9]{3})?,f(oo|u)bar |
\d | 匹配任何数字,和[0-9]一样(\D是\d的反义,匹配任何非数字字符) | data\d+.txt |
\w | 匹配任何数字字母字符,和[A-Za-z0-9]相同(\W是\w的反义 | [A-Za-z]\w+ |
\s | 匹配任何空白符,和[\n\t\r\v\f]相同(\S是\s的反义 | of\sthe |
\b | 匹配单词边界(\B是\b的反义 | \bThe\b |
\nn | 匹配已保存的子组(参考(...)) | price:\16 |
\c | 逐一匹配特殊字符c(即,消除它的特殊含义,按字面匹配 | \., \\, \* |
\A(\Z) | 匹配字符串的起始 | \ADear |
15.2.1 匹配多个正则表达式
使用| 来表示“或”操作,有时也称之为联合(union)或者逻辑或(OR)
15.2.2 匹配任意单一字符
句点符号可以匹配除了换行符(NEWLINE)之外的任意一个单个字符(Python的正则表达式有一个编译标识可以去除这一限制,让句点符号可以匹配换行符)
PS:使用\. 可以匹配句点本身
15.2.3 从字符串边界开始匹配
脱字符号(^,caret)或\A用来匹配以特定字符串开头的字符串,$和\Z用来匹配特定字符串结尾
正则表达式模式 | 匹配的字符串 |
^From | 匹配任何以From开始的字符串 |
/bin/tcsh$ | 匹配任何以/bin/tcsh结束的字符串 |
^Subject:hi$ | 匹配仅由Subject:hi组成的字符串 |
特别说明,如果你想匹配这两个字符中的任何一个(或全部),就必须用反斜线进行转义。例如,如果你想匹配任何以美元符号$结尾的字符串,一个可行的方法是使用正则表达式".*\$$"。
\b匹配的是单词(匹配单词与匹配字符串的不同之处便是\b\b与^$的不同之处)开头,\B是\b的反义
正则表达式模式 | 匹配的单词 |
the | 任何包含有"the"的单词 |
\bthe | 任何以"the"开头的单词 |
\bthe\b | 仅仅匹配单词the |
\Bthe | 任意包含"the"但不以“the"开头的单词 |
15.2.4 创建字符类[]
[cr][23]用以匹配c2,c3,r2,r3,当[]中只有一个单词时,效果和|是一样的,比如[a][b]和a|b是等价的。
15.2.5指定范围(-)和否定(^)
正则表达式模式 | 匹配的字符 |
z.[0-9] | 字符z+任意一个字符+一个数字 |
[r-u][env-x][us] | "r","s","t","u"中一个字符+“e”,"n","v","w","x"中一个字符+"u"或"s" |
[^aeiou] | 一个非元音字符 |
[^\t\n] | 除TAB制表符和换行符以外的一个字符 |
["-a] | 在ASCII字符集中顺序在“到a之间的一个字符 |
15.2.6使用闭包操作符(*,+,?,{})实现多次出现/重复匹配
零次或零次以上的情况(在计算机语言和编译器原理里,此操作符被叫做 Kleene 闭包操作符)。加号(+)操作符匹配它左边那个正则表达式模式至少出现一次的情况(它也被称为正闭包操作符),而问号操作符( ? )匹配它左边那个正则表达式模式出现零次或一次的情况。 还有花括号操作符({ }), 花括号里可以是单个的值,也可以是由逗号分开的一对值。如果是一个值,如,{N},则表示匹配 N 次出现;如果是一对值,即,{M, N},就表示匹配 M 次到 N 次出现。
问号有两种含义:1.单独使用时表示匹配出现零次或一次的情况,2.紧跟在表示重复的元字符后面时,表示要求搜索引擎匹配的字符串越短越好。
正则表达式的模式 | 匹配的字符 |
[dn]ot? | 字符"d"或"n", 后面是一个"o", 最后是最多一个字符"t",即,do, no, dot,not |
0?[1-9] | 从1到9中的任意一位数字,前面可能还有一个"0". 例如:可以把它看 成一月到九月的数字表示形式,不管是一位数字还是两位数字的表示形 式。 |
[0-9]{15,16} | 15 或 16 位数字表示,例如:信用卡号码 |
</?[^>]+> | 匹配所有合法(和无效的)HTML 标签的字符串 |
[KQRBNP][a-h][1-8]-[a-h][1-8] | 在“长代数”记谱法中,表示的国际象棋合法的棋盘。 即, “K,” “Q,” “R,” “B,” “N,” 或 “P” 等 字母后面加上两个用连字符连在一起的"a1"到"h8"之间 的棋盘坐标。前面的编号表示从哪里开始走棋,后面的 编号代表走到哪个位置(棋格)去。 |
15.2.7特殊字符表示,字符集
正则表达式的模式 | 匹配的字符 |
\w+-\d+ | 一个由字母或数字组成的字符串,和至少一个数字,两部分中间由连 字符连接 |
[A-Za-z]\w* | 第一个字符是字母,其余字符(如果存在的话),是字母或数字(它几乎 等价于 Python 语言中合法的标识符[见参考练习] |
\d{3}-\d{3}-\d{4} | (美国)电话号码,前面带区号前缀,例如 800-555-1212 |
\w+@\w+\.com | 简单的 XXX@YYY.com 格式的电子邮件地址 |
15.2.8用圆括号创建组
一对圆括号(()) 和正则表达式一起使用时可以实现以下任意一个(或两个)功能: 1.对正则表达式进行分组 2.匹配子组
正则表达式模式 | 匹配的字符 |
\d+(\.\d*)? | 表示简单的浮点数,即, 任意个十进制数字,后面跟一个可选的小数点,然后再接零或多个十进制数字。例如:“0.004,” “2,” “75.”,等等。 |
(Mr?s?\. )?[A-Z][a-z]* [A-Za-z-]+ | 名字和姓氏,对名字的限制(首字母大写,其它字 母(如果存在)小写), 全名前有可选的称谓(“Mr.,” “Mrs.,” “Ms.,” 或 “M.,”),姓氏没有什么限制,允许有多个单词、横线、大写字母。 |
15.3正则表达式和Python语言
5.3.1 re模块:核心函数和方法
compile(pattern,flags=0) 对正则表达式模式pattern 进行编译,flags 是可选标志符,并返回一个regex 对象
match(pattern,string, flags=0) 尝试用正则表达式模式pattern 匹配字符串string,
flags 是可选标志符,如果匹配成功,则返回一个匹配对象;否则返回None
search(pattern,string, flags=0) 在字符串string 中查找正则表达式模式pattern 的第
一次出现,flags 是可选标志符,如果匹配成功,则返回
一个匹配对象;否则返回None
findall(pattern,string[,flags]) 在字符串string 中查找正则表达式模式pattern 的所有
(非重复)出现;返回一个匹配对象的列表
finditer(pattern,string[, flags]) 和findall()相同,但返回的不是列表而是迭代器;对
于每个匹配,该迭代器返回一个匹配对象
split(pattern,string, max=0) 根据正则表达式pattern 中的分隔符把字符string 分割
为一个列表,返回成功匹配的列表,最多分割max 次(默
认是分割所有匹配的地方)。
sub(pattern, repl, string, max=0) 把字符串string 中所有匹配正则表达式pattern 的地
方替换成字符串repl,如果max 的值没有给出,则对所有
匹配的地方进行替换(另外,请参考subn(),它还会返回
一个表示替换次数的数值)。
group(num=0) 返回全部匹配对象(或指定编号是num 的子组)
groups() 返回一个包含全部匹配的子组的元组
15.3.2使用compile()编译正则表达式
大多数re模块函数都可以作为regex对象的方法。编译rex对象时给出一些可选标识符,可以得到特殊的编译对象,详情参阅相关文档。这些标识符也可以传给模块版本的match()和search()进行特定模式的匹配。
15.3.3 匹配对象和group(), groups()方法
在处理正则表达式时,除regex对象外,还有另一种对象类型---匹配对象。这些对象是在match()或search()被成功调用之后所返回的结果。匹配对象有两个主要方法:group()和groups()。
group()方法或者返回所有匹配对象或是根据要求返回某个特定子组。groups()则很简单,它返回一个包含唯一或所有子组的元组。如果正则表达式中没有子组的话, groups() 将返回一个空元组,而group()仍会返回全部匹配对象。
15.3.4用match()匹配字符串
>>>m = re.match('foo','bar')
>>>if m is not None:m.group()
匹配失败,返回None
如果使用>>>re.match('foo', 'bar on the table').group()
则会引发一个AttributeError,因为match返回的None没有group属性
15.3.5 search()在一个字符串中查找一个模式(搜索和匹配的比较)
>>>m = re.match('foo','seafood')
>>>if m is not None:m.group()
会导致失败,这时改用re.search可以成功匹配到foo
15.3.6匹配多个字符串()
>>>bt = 'bat|bet|bit'
>>>m = re.match(bt, 'bat')
>>>if m is not None:m.group()
'bat'
15.3.7匹配任意单个字符
>>> patt314 = '3.14' # RE dot #正则表达式点号
>>> pi_patt = '3\.14' # literal dot (dec. point) #浮点(小数点)
>>> m = re.match(pi_patt, '3.14') # exact match #完全匹配
>>> if m is not None: m.group()
15.3.8 创建字符集合( [ ] )
>>> m = re.match('[cr][23][dp][o2]', 'c2do')# matches 'c2do' #匹配'c2do'
>>> if m is not None: m.group()
...
'c2do'
>>> m = re.match('r2d2|c3po', 'c2do')# does not match 'c2do' #不匹配'c2do'
>>> if m is not None: m.group()
...
15.3.9重复、特殊字符和子组
我们通过括号划分子组,在之前匹配邮箱地址的正则表达式中,我们可以稍作改变让其支持完整的域名
>>> patt = '\w+@(\w+\.)?\w+\.com'
>>> re.match(patt, 'nobody@xxx.com').group()
'nobody@xxx.com'
>>> re.match(patt, 'nobody@www.xxx.com').group()
'nobody@www.xxx.com'
或改成匹配任意数量的子域名
>>> patt = '\w+@(\w+\.)*\w+\.com'
>>> re.match(patt, 'nobody@www.xxx.yyy.zzz.com').group()
'nobody@www.xxx.yyy.zzz.com'
用括号匹配并保存子组的好处是你可以简单的提取各个部分
>>> m = re.match('(\w\w\w)-(\d\d\d)', 'abc-123')
>>> m.group() # entire match 所有匹配部分
'abc-123'
>>> m.group(1) # subgroup 1 匹配的子组 1
'abc'
>>> m.group(2) # subgroup 2 匹配的子组 2
'123'
>>> m.groups() # all subgroups 所有匹配子组
('abc', '123')
groups显示的是子组,所以没有使用括号时,groups会返回一个空的元组
>>> m = re.match('ab', 'ab') # no subgroups #无子组
>>> m.group() # entire match
'ab'
>>> m.groups() # all subgroups #所有匹配的子组
()
>>>
>>> m = re.match('(ab)', 'ab') # one subgroup #一个子组
>>> m.group() # entire match #所有匹配
'ab'
>>> m.group(1) # subgroup 1 #匹配的子组 1
'ab'
>>> m.groups() # all subgroups #所有匹配子组
('ab')
>>>
>>> m = re.match('(a)(b)', 'ab') # two subgroups #两个子组
>>> m.group() # entire match
'ab'
>>> m.group(1) # subgroup 1 匹配的子组 1
'a'
>>> m.group(2) # subgroup 2 匹配的子组 2
'b'
>>> m.groups() # all subgroups 所有匹配子组的元组
('a', 'b')
>>>
>>> m = re.match('(a(b))', 'ab') # two subgroups #两个子组
>>> m.group() # entire match #所有匹配部分
'ab'
>>> m.group(1) # subgroup 1 #匹配的子组 1
'ab'
>>> m.group(2) # subgroup 2 #匹配的子组 2
'b'
>>> m.groups() # all subgroups #所有匹配的子组的元组
('ab', 'b')
15.3.10从字符串的开头或结尾匹配及在单词边界上匹配
下面的例子强调了锚点性(positioncal)正则表达式操作符。这些锚点性正则表达式操作符主要被用于搜索而
不是匹配,因为 match()总是从字符串的开头进行匹配的。
>>> m = re.search('^The', 'The end.') # match #在字符串开头
>>> if m is not None: m.group()
...
'The'
>>> m = re.search('^The', 'end. The') # not at beginning #不在字符串开头
>>> if m is not None: m.group()
...
>>> m = re.search(r'\bthe', 'bite the dog') # at a boundary #在单词开头
>>> if m is not None: m.group()
...
'the'
>>> m = re.search(r'\bthe', 'bitethe dog') # no boundary #不在单词开头
>>> if m is not None: m.group()
...
>>> m = re.search(r'\Bthe', 'bitethe dog') # no boundary #不在单词开头
>>> if m is not None: m.group()
...
'the'
你可能在这里注意到了原始字符串(raw strings)的出现。在本章末尾的核心笔记中,有关于
它的说明。通常,在正则表达式中使用原始字符串是个好主意。
你还应该了解另外四个 re 模块函数和 regex 对象方法: findall(), sub(), subn() 和 split().
15.3.11用findall()找到每个出现的匹配部分
findall()用于非重叠地查找某字符串中一个正则表达式模式出
现的情况。findall()和 search()相似之处在于二者都执行字符串搜索,但 findall()和 match()与
search()不同之处是,findall()总返回一个列表。如果 findall()没有找到匹配的部分,会返回空
列表;如果成功找到匹配部分,则返回所有匹配部分的列表(按从左到右出现的顺序排列)。
>>> re.findall('car', 'car')
['car']
>>> re.findall('car', 'scary')
['car']
>>> re.findall('car', 'carry the barcardi to the car')
['car', 'car', 'car']
包含子组的搜索会返回更复杂的一个列表,这样做是有意义的,因为子组是允许你从单个正则
表达式中抽取特定模式的一种机制,比如,匹配一个完整电话号码中的一部分(例如,区号),或完
整电子邮件地址的一部分(例如,登录名)。
正则表达式仅有一个子组时,findall()返回子组匹配的字符串组成的列表;如果表达式有多个
子组,返回的结果是一个元组的列表,元组中每个元素都是一个子组的匹配内容,像这样的元组(每
一个成功的匹配对应一个元组)构成了返回列表中的元素。
15.3.12用sub()(和subn())进行搜索和替换
有两种函数/方法用于完成搜索和代替的功能: sub()和 subn(). 二者几乎是一样的,都是将某
字符串中所有匹配正则表达式模式的部分进行替换。用来替换的部分通常是一个字符串,但也可能
是一个函数,该函数返回一个用来替换的字符串。subn()和 sub()一样,但它还返回一个表示替换次
数的数字,替换后的字符串和表示替换次数的数字作为一个元组的元素返回。
>>> re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
'attn: Mr. Smith\012\012Dear Mr. Smith,\012'
>>>
>>> re.subn('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
('attn: Mr. Smith\012\012Dear Mr. Smith,\012', 2)
>>>
>>> print re.sub('X', 'Mr. Smith', 'attn: X\n\nDear X,\n')
attn: Mr. Smith
Dear Mr. Smith,
>>> re.sub('[ae]', 'X', 'abcdef')
'XbcdXf'
>>> re.subn('[ae]', 'X', 'abcdef')
('XbcdXf', 2)
15.3.13 用 split()分割(分隔模式)
re的split与string的相似,但是前者可根据正则表达式模式分割,还可以通过设定参数来指定分割的最大次数。当没使用匹配模式的时候re.split()和string.split()的执行过程是一样的,见以下例子:
>>> re.split(':'. 'str1:str2:str3')
['str1', 'str2', 'str3']
另一个例子是处理Unix下who命令输出的已登录用户的信息。(略)
核心笔记:Python原始字符串(raw strings)的用法
'\b'在ASCII字符中和正则表达式中有不同的含义,我们不得不在字符串中使用'\\b',所以使用正则表达式时我们通常都使用原始字符串。
15.4正则表达式示例

1 from random import randint, choice 2 from string import lowercase 3 from sys import maxint 4 from time import ctime 5 6 doms = ('com', 'edu', 'net', 'org', 'gov') 7 8 for i in range(randint(5,10)): 9 dtint = randint(0, maxint - 1) 10 dtstr = ctime(dtint) 11 12 shorter = randint(4, 7) 13 em = '' 14 for j in range(shorter): 15 em += choice(lowercase) 16 17 longer = randint(shorter, 12) 18 dn = '' 19 for j in range(longer): 20 dn += choice(lowercase) 21 22 print '%s::%s%s.%s::%d-%d-%d' %(dtstr, em, dn, choice(doms), 23 dtint, shorter, longer)
15.4.1匹配一个字符串
^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)来匹配日期开头,也可以使用'^(\w{3})',书中提到的'^(\w){3}'在2.7.9环境下并无差异。
15.4.2 搜索与匹配的比较, “贪婪”匹配
在我们写正则表达式前,先明确这些整数数据项是在字符串数据的末尾。这意味着我们有两种选择:搜索(search)或匹配(match)。使用搜索更合适,因为我们确切地知道要查找的数据是什么(三个整数的集合),它不在字符串的开头,也不是字符串的全部内容。如果我们用匹配(match)的方法,就不得不写一个正则表达式来匹配整行内容,并用子组保存我们感兴趣的那部分数据。为说明它们之间的区别,我们先用搜索查找,再尝试用匹配来做,向你证明搜索查找更适合。
>>> patt = '\d+-\d+-\d+'
>>> re.search(patt, data).group() # entire match
'1171590364-6-8'
尝试用这个正则表达式来匹配数据会失败,这是为什么呢?因为匹配从字符串的起始位置开始进行的,而我们要找的数值字符串在末尾。我们只能再写一个匹配全部字符串的正则表达式。还有一个偷懒的办法,就是用“.+”来表示任意个字符集,后面再接上我们真正感兴趣的数据:
patt = '.+\d+-\d+-\d+'
>>> re.match(patt, data).group() # entire match #全部匹配部分
'Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::1171590364-6-8'
这个方法不错,可是我们只想获得每行末尾数字的字段,而不是整个字符串,所以需要用圆括号将我们感兴趣的那部分数据分成一组:
>>> patt = '.+(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1 #子组 1
'4-6-8'
到底怎么回事呢? 我们本应该得到数据“1171590364-6-8”,而不应该是“4-6-8”啊。第一个整数字段的前半部分到哪里去了呢? 原因是:正则表达式本身默认是贪心匹配的。也就是说,如果正则表达式模式中使用到通配字,那它在按照从左到右的顺序求值时,会尽量“抓取”满足匹配的最长字符串。在我们上面的例子里,“.+”会从字符串的起始处抓取满足模式的最长字符,其中包括我们想得到的第一个整数字段的中的大部分。“\d+”只需一位数字就可以匹配,所以它匹配了数字“4”,而“.+” 则匹配了从字符串起始到这个第一位数字“4”之间的所有字符:
“Thu Feb 15 17:46:04 2007::uzifzf@dpyivihw.gov::117159036”。
一个解决办法是用“非贪婪”操作符,“?”. 这个操作符可以用在 “*”, “+”, 或 “?” 的后面。它的作用是要求正则表达式引擎匹配的字符越少越好。因此,如果我们把“?”放在“.+”的后面,我们就得到了想要的结果.
>>> patt = '.+?(\d+-\d+-\d+)'
>>> re.match(patt, data).group(1) # subgroup 1 # 子组 1
'1171590364-6-8'
另一种办法,更简单,注意运用 “::”做字段分隔符号。你可以用一般字符串的 strip('::') 方法,得到全部字符,然后用 strip('-')得到你要找的三个整数字段。我们现在不采用这种方法,因为我们的脚本 gendata.py 正是通过这种方法把字符组合到一起的。
最后一个例子:假设我们只想抽取三个整数字段里中间的那个整数部分。我们是这么做的(用搜索,这样就不必匹配整个字符了):“-(\d+)-”。用这个模式“-(\d+)-”,我们得到:
>>> patt = '-(\d+)-'
>>> m = re.search(patt, data)
>>> m.group() # entire match #整个匹配
'-6-'
>>> m.group(1) # subgroup 1 #子组 1
'6'
在本章中,有很多正则表达式的强大功能我们未能涉及,由于篇幅所限,我们无法详细介绍它们。但我们希望所提供的信息和技巧对你的编程实践有所帮助。我们建议你参阅有关文档以获得更多在 Python语言中使用正则表达式的知识。要精通正则表达式,我们建议你阅读Jeffrey E. F. Friedl 所编写的 Mastering Regular Expressions 一书.