现实中,经常要做的一项任务,是在文本中检索某种模式。所谓模式,就是满足一定规则的字符串的总称,例如只由大写字母构成的单词就是一种模式,所有电子邮件的地址,也是一种模式。我们检索这样符合一定规则的字符串,按照之前字符串的知识,当然能设计出相关的函数,完成这些任务,可是当要检索的模式非常复杂时,设计这样的函数显然费时费力,那就需要一种高级的,便捷的模式检索的方法,帮助我们解决这个问题。
这也就引出了今天的主题——正则表达式。直观上讲,正则表达式就是一种特殊的字符串,它能够表达某种字符串组成的规则。从而根据这个规则,检索出符合规则的字符串。
我们先不关心正则表达式是如何在Python中运作的,先看如何把我们要表达的规则写成一个正确的正则表达式。那就一定要明白各种特殊字符的含义。
仅有字母和数字构成的普通文本。
只能匹配他们本身。比如python
只匹配python
;hao123
只匹配hao123
管道符号|
匹配多个正则表达式。
比如abc|def
能匹配的是字符串abc
或者 def
,而 123|asd|12f
能匹配字符串 123, asd, 12f
中的任意字符串。这里需要注意的是,管道符号|
连接的也能是其他的正则表达式,不一定是只有文本和数字构成的普通文本。实际上,就是我们非常熟悉的“或”操作。
句点 .
用来匹配任何单个字符(换行符除外)。
比如 .a
可以匹配任意字符和a连接的字符串("ba", "ca", "+a", "2a"...
都可以);同理,也可以用 ..
匹配任意两个字符构成的字符串。那么,现在有个问题,就是如何匹配句点本身呢?比如现在要匹配字符串 "ab."
,那么只需要用转义字符 \
对句点转义即可,相应的正则表达式写为 ab\.
^
和 $
分别用来匹配字符串的开头和结尾。
比如 ^I
匹配所有以"I"
开头的字符串;.com$
匹配所有以 .com
结尾的字符串。同样的,如果想匹配的是这两个字符中的任意一个,要用 \
转义,比如,想匹配以$为结尾的字符串,可以这样设计正则表达式:.*\$$
,其中 *
表示前面的模式重复任意0次或多次。这个我们下面还要再说。
[]
用来创建字符类。
用处和上面说的句点类似,可以匹配方括号里面出现的任何字符。比如 c[abe]t
就能匹配 "cat, cbt, cet"
3个字符,[ab][12]
可以匹配 "a1", "a2", "b1", "b2"
4种字符。
指定范围 -
和否定 ^
。
这是关于 []
操作符的两个特殊的操作。我们可以在 []
中指定范围,例如, [A-Za-z]
表示匹配任意大写或小写的英文字符;而在 []
左括号后面第一个字符如果是 ^
则表示匹配除了 []
中的模式之外的情况,比如 [^aeiou]
匹配非元音。
闭包操作符 *, +, ?, {}
用来实现多次匹配。
我们一个个来看这4个符号。*
上面已经说过,表示匹配前面出现的正则表达式0次或多次;+
与 *
类似,匹配前面出现的正则表达式1次或多次;?
匹配前面出现的正则表达式0次或1次;最后,{}
处理的问题是匹配前面出现的正则表达式给定次数的情况,比如 {N}
匹配前面的模式出现N次的情况,一个例子是 [0-9]{18}
可以用来匹配身份证号,而 {M, N}
处理的是前面的模式出现M次到N次的情况。
()
创建组。
()
的存在将正则表达式合成一个分组,比如,(abc){2}
匹配 abcabc
,(\w+) (\w)
匹配 ab 2
这样的形式
除了上面说的特殊符号,还有一些特殊的字符。
\d
匹配任何数字,等同于[0-9]
,\D
表示取反,即所有非数字。正则表达式中,很多情况都是将小写改为大写就是取反了。
\w
匹配任何数字或字母
\s
匹配任何空白符
\b
匹配单词边界. 这个多说两句,其实功能和前面的 ^
和 $
是类似的。比如 \band
匹配以"and"
开头的字符串,而 \Band
匹配包含 "and"
却不以 "and"
开头的字符串。
\A
匹配字符串开头,相当于^
\Z
匹配字符串结尾,相当于$
好了,到此,基本的正则表达式的语法就算是学习完毕了。可能还有些地方我没说全,不过好在这些语法讲的很多,随便一本教材或者直接百度都能搜到大量的结果。另外,在这我给一个在线的正则表达式测试的网站:http://tool.oschina.net/regex/,遇到自己拿不准的情况可以自行先去测试一下。
其实上面的语法规则对于初学者来说还是相当复杂,纯靠记忆是行不通的,唯一的办法就是多练习。下面我们看看几个相对常用的正则表达式。
[A-Za-z_]\w*
"www."
开头,以".com"
结尾的Web域名。^www\..+\.com$
\d{3}-\d{8}|\d{4}-\{7,8}
[email protected]
的电子邮件地址。\w+@\w+\.com
了解正则表达式的语法当然不是学习的最终目的,还要看看放到具体问题中怎么应用。Python为我们提供了一个完整的正则表达式引擎re模块。
引入re模块,就可以自己设计正则表达式进行匹配了。下面给出了一个简单的用re模块中 match()
进行匹配的实例。
(1) match(pattern, string)
import re
address = "xxxx@bupt.edu.cn"
address2 = "wdcever@qq.com"
print(re.match("^\w+@bupt\.edu\.cn$", address)) # >>> <_sre.SRE_Match object; ...>
print(re.match("^\w+@bupt\.edu\.cn$", address2)) # >>> None
match()
函数由2个参数组成,第一个是模式,我们写入正则表达式,第二个是要匹配的字符串。在这个例子中,想要匹配的是北邮的校园邮箱地址,于是,写入相应的正则表达式,如果匹配成功,则返回匹配的对象,如果失败,则返回None. 如上面的代码所示。
(2) group(num=0)
和 groups()
当然,上面的做法只是返回了匹配对象,但是更多时候,我们需要返回的是所有匹配的字符串本身。那就需要用到group(num=0)
和 groups()
import re
myStr = "hello world"
matchObj = re.match("(\w+) (\w+)", myStr)
if matchObj:
print(matchObj.group(1)) # >>> hello
print(matchObj.group(1, 2)) # >>> ('hello', 'world')
print(matchObj.groups()) # >>> ('hello', 'world')
其实,从上面这个例子,基本可以看出group(num=0)
和 groups()
的区别:group(num)
返回的是第num个匹配的分组;而 groups()
匹配的是全部分组构成的元组。而如果正则表达式中没有分组的话,就返回空元组。
(3) compile()
预编译提高效率
现在说一个re模块中特殊的函数 compile()
,它的作用是对正则表达式做预编译。其实,当我们在上面直接使用 match()
函数的时候,本身就是对正则表达式先进行预编译,编译成一个regex对象,再做匹配计算,只不过为了简单起见,这个编译过程从代码来看,是被省略了(其实没有)。所以,是否预编译,对我们使用正则表达式是没有什么影响的。但是,当一个项目中有着大量的、重复的正则表达式运算时,预编译能够在一定程度上提高程序运行的效率。
用法如下:
import re
myStr = "123"
matchObj = re.compile("\d+")
if matchObj:
print(matchObj.match(myStr))
效果跟下面的代码是一致的:
print(re.match("\d+", myStr))
上面说的 match()
函数是从一个字符串的头开始匹配整个字符串。但是更多时候,可能我们要匹配的模式是在一个大的字符串当中的,比如下面这种情况:
import re
myStr = "qws123wd"
print(re.match("\d+", myStr)) # >>> None
我们要查找的是一个只含有数字的模式,这个模式在myStr中间也确实出现了,但是 match()
从头匹配的机制导致返回的是None.
(1) search()
所以,自然能想到,我们需要一种能匹配字符串中第一个出现模式的方法。这个函数就是 search
import re
myStr = "qws123wd"
print(re.search("\d+", myStr).group()) # >>> 123
当然,返回的只是第一次匹配时的匹配字符串,比如还是上面的例子,令 myStr = "qws123wd"
,那么输出的还是 "123"
接下来,我将给出一些例子,顺带算是把上面的正则语法再回顾一下。
\w, +, $, |
的基本应用import re
myStr1 = "www.baidu.com"
myStr2 = "blog.csdn.net"
myStr3 = "a.net.cn"
pat = "\w+\.\w+\.com$|\w+\.\w+\.net$"
matObj1 = re.match(pat, myStr1)
matObj2 = re.match(pat, myStr2)
if matObj1:
print(matObj1.group()) # >>> www.baidu.com
if matObj2:
print(matObj2.group()) # >>> blog.csdn.net
# myStr3不能与正则表达式匹配
print(re.match(pat, myStr3)) # >>> None
match()
与 search()
import re
myStr = "a.net.cn"
pat = "\.net"
print(re.search(pat, myStr).group()) # >>> .net
print(re.match(pat, myStr)) # >>> None
我们通过电子邮件地址的例子熟悉一下这一部分内容。
匹配简单的形如 "[email protected]"
的电子邮件,其中,"XXX", "YYY"
都是字母或数字:
"\w+@\w+\.com"
进一步,允许出现不止一个子域名:所谓子域名就是前面的 "YYY"
部分
"\w+@(\w+\.)*\w+\.com"
其中,"*"
表示q前面的分组,也就是 "(\w+\.)"
可以出现0次或多次。
这里的分组很重要,加或者不加,加在哪里,都有着不同的意义。因为每个正则语法中的特殊字符,它只能管紧邻着它的,左边的第一个正则表达式。如下所示:
import re
myStr1 = "aa1@bb2_23.xxx.edu.com"
myStr2 = "edu.com"
patt1 = "\w+@(\w+\.)*\w+\.com"
patt2 = "(\w+@\w+\.)*\w+\.com"
print(re.search(patt1, myStr1).group()) # >>> aa1@bb2_23.xxx.edu.com
print(re.search(patt2, myStr2).group()) # >>> edu.com
print(re.search(patt1, myStr2).group()) # >>> AttributeError
可见,patt1表示的是前面 "(\w+\.)"
出现0次或多次的情况;patt2表示的是 "(\w+@\w+\.)"
出现一次或多次的1情况。其实,基本上在这里分组操作符 "()"
的作用有点类似于四则运算中()的作用。
(2) findall()
除了 search()
,findall()
也用来实现搜索功能,而且功能更为强大。它在字符串中搜索所有匹配的对象,并返回匹配对象的列表,用法如下:
import re
myStr = "www.sina.com, www.baidu.com, www.mi.com"
patt = "www\.\w+\.com"
print(re.findall(patt, myStr)) # >>> ['www.sina.com', 'www.baidu.com', 'www.mi.com']
(1) 替换
替换功能,re模块也提供了相应的函数 sub()
和 sunb()
,后者与前者的作用是一样的,都是事先查找,再替换,返回替换后的结果。只不过后者还返回了替换次数。用法如下:
sub(pattern, repl, string, max=0)
表示将string中符合pattern规则的部分替换成repl,max表示替换的最大次数,若没有给出,则全部替换。
给出实际的例子:
import re
myStr = "@123 Road, @2r Road"
patt = "Road"
print(re.sub(patt, "Street", myStr)) # >>> @123 Street, @2r Street
print(re.subn(patt, "Street", myStr)) # >>> ('@123 Street, @2r Street', 2)
(2) 分割
re模块中的 split()
函数,功能上与我们之前了解的字符串的 split()
函数是类似的,但是更加强大,因为后者只能根据给出的固定字符串分割,而前者却是根据给出的正则表达式所代表的模式分割。
比如下面这个例子中,一行字符串被三种分隔符分割,分别是 ,.;
。我们的目的是对只要出现这三种分隔符的地方,就进行分割。
"C;Python,C++;Java.JS,CSS"
如果用之前的字符串函数 split()
,那这件事就不太容易了,但是re模块的 split()
函数却提供了一种非常方便的接口。
import re
myStr = "C;Python,C++;Java.JS,CSS"
patt = "[,.;]"
print(re.split(patt, myStr)) # >>> ['C', 'Python', 'C++', 'Java', 'JS', 'CSS']
到此,基本上关于正则表达式的内容已经讲完了大部分,但是还有一个小问题需要说说,那就是“贪婪”匹配的问题。
先看一个例子,字符串 "asdfg123-74384-23"
,我们现在需要将后面的,由横线 "-"
相连的数字部分检索出来,学过正则表达式后,我们想到的办法当然是用 search()
函数,匹配模式与字符串。正则表达式可以这样写:"\w+(\d+-\d+-\d+-)"
分组的目的是,因为主要匹配后面的数字部分,分组能帮助我们提取最想要的信息。写出代码,如下:
import re
myStr = "edwewme31273681-83928-29839"
patt = "\w+(\d+-\d+-\d+)"
print(re.search(patt, myStr).group(1)) # >>> 1-83928-29839
可以看到,最后的结果不是我们想要的。因为正则表达式前面的 "\w+"
部分在最大可能地匹配它所能匹配的字符串。这就是正则表达式的贪心性质。也就是说,当出现通配符(特殊字符、符号)时,程序会从左至右匹配所能匹配的最长的字符串。
显然,在一些应用场景下,这种贪心的性质并不能满足我们的需求,所以,我们一般采用“非贪婪”操作符 ?
,将 ?
操作符放在重复操作符 *, +, ?
的后面,作用是要求正则表达式尽可能少的匹配。比如还是上面的例子
import re
myStr = "edwewme31273681-83928-29839"
patt = "\w+?(\d+-\d+-\d+)"
print(re.search(patt, myStr).group(1)) # >>> 31273681-83928-29839
这回就对了。
有关正则表达式的东西非常琐碎,其实大量的内容还需要在实践中自己感受,自己去查找相关的解决方法。我在此算是介绍个大概吧。