Python--正则表达式

现实中,经常要做的一项任务,是在文本中检索某种模式。所谓模式,就是满足一定规则的字符串的总称,例如只由大写字母构成的单词就是一种模式,所有电子邮件的地址,也是一种模式。我们检索这样符合一定规则的字符串,按照之前字符串的知识,当然能设计出相关的函数,完成这些任务,可是当要检索的模式非常复杂时,设计这样的函数显然费时费力,那就需要一种高级的,便捷的模式检索的方法,帮助我们解决这个问题。

这也就引出了今天的主题——正则表达式。直观上讲,正则表达式就是一种特殊的字符串,它能够表达某种字符串组成的规则。从而根据这个规则,检索出符合规则的字符串。

正则表达式使用的特殊符号和字符

我们先不关心正则表达式是如何在Python中运作的,先看如何把我们要表达的规则写成一个正确的正则表达式。那就一定要明白各种特殊字符的含义。

特殊符号

  • 仅有字母和数字构成的普通文本。
    只能匹配他们本身。比如python 只匹配pythonhao123 只匹配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/,遇到自己拿不准的情况可以自行先去测试一下。

例子

其实上面的语法规则对于初学者来说还是相当复杂,纯靠记忆是行不通的,唯一的办法就是多练习。下面我们看看几个相对常用的正则表达式。

  1. 匹配合法的Python标识符。[A-Za-z_]\w*
  2. 匹配以"www." 开头,以".com" 结尾的Web域名。^www\..+\.com$
  3. 匹配国内电话号码。\d{3}-\d{8}|\d{4}-\{7,8}
  4. 匹配简单的,形如 [email protected] 的电子邮件地址。\w+@\w+\.com

re模块的基本应用

了解正则表达式的语法当然不是学习的最终目的,还要看看放到具体问题中怎么应用。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

这回就对了。

有关正则表达式的东西非常琐碎,其实大量的内容还需要在实践中自己感受,自己去查找相关的解决方法。我在此算是介绍个大概吧。

你可能感兴趣的:(Python,Python--基础)