什么是正则表达式
-
官方解释:正则表达式的概念是使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。
-
简单来说,正则表达式就是通过一定的匹配规则,从一个字符串中提取出我们想要的数据,
-
虽然有时候会比较复杂,但无疑它是非常强大的。
-
在python中要使用正则表达式,首先需要先导入python内置的re模块。
正则表达式的常用方法:
findall()
sub
match
search
正则表达式的匹配规则:
普通字符
元字符字符集
概括字符集
量词
锚字符
组
贪婪与非贪婪
正则表达式的灵魂在于匹配规则的灵活使用,而匹配规则,我简单将之分为两大类,一类是普通字符,这类匹配意义往往不大,另一类是元字符,是我们需要重点掌握的内容。
1. 普通字符串的匹配
re.findall()
- findall是re库的一个重要方法,第一个参数是匹配规则,第二个参数是要匹配的目标字符串,还有第三个参数,我们之后讲,findall返回的结果是一个列表。
import re
target = 'life is short, I use python.'
result = re.findall('python', target) # 这行代码的意思是从target中匹配'python',如果匹配到就返回,没有匹配到就返回空列表。
result1 = re.findall('java', target)
匹配结果:
- 如果匹配规则是一个普通字符串的话,意义并不大,试想一下,一个网页上的内容都是变的,处处可能都不一样,我们想用一个固定的普通字符串去匹配到内容,显然是不太合适的。
- 就比如CSDN,我们想要获取每篇文章的发布时间,每篇的文章的发布时间都是不一样的,用一个固定的字符串显然匹配不出来我们想要的内容。
2. 元字符
第一类:字符集
- 接下来,就是我们的重头戏了,元字符,我粗略地将他们分为了7类,我们先来看第一类,字符集,用[]表示,中括号内可以写任意字符,各字符间是或的关系(不理解没关系,后面会有代码解释):
- 现在我们得到了一个这样的字符串 target = ‘abc acc aec agc adc aic’,我们有这样一个需求,需要找出这个字符串中中间是d或者e的单词,我们该怎么做呢?
- 很多童鞋第一反应就是for循环遍历,for循环当然可以写出来,for循环是非常厉害的,有兴趣的童鞋可以去尝试一下,但是用正则会简单很多,我们来看看怎么做:
import re
target = 'abc acc aec agc adc aic'
result = re.findall('a[de]c', target) # 这一行中的[de]表示这个位置上的字符是d或者是e都可以匹配出来
print(result)
匹配结果:
- 这只是字符集[]的一个最简单的应用,现在我们又有一个需求,需要找出这个字符串中中间是b-z之间的任意一个字符的单词,就可以这样写了,而不需要把b-z之间的字符都写出来。
import re
target = 'abc acc aec agc adc aic'
result = re.findall('a[b-z]c', target) # 这一行中的[b‐z]表示这个位置上的字符在b‐z范围内都可以匹配出来
print(result)
匹配结果:
- 插入符为[]内的第一个字符时,表示否定
import re
target = 'abc acc aec agc adc aic'
result = re.findall('a[^c-z]c', target) # 这一行中的[^c‐z]表示这个位置上的字符不在c‐z范围内都可以匹配出来,注意是不在
print(result)
匹配结果:
总结:
匹配规则(举例说明) | 释义 |
---|---|
[abf] | 表示该位置上的字符为a或者b或者f,即匹配成功 |
[a-z] | 表示该位置上的字符在a-z之间,即匹配成功 |
[â-z] | 表示该位置上的字符不在a-z之间,即匹配成功 |
第二类:概括字符集
匹配规则 | 释义 | 等价于 |
---|---|---|
\d | 表示该位置上的字符是数字,即匹配成功 | [0-9] |
\D | 表示该位置上的字符不是数字,即匹配成功 | [^0-9] |
\w | 表示该位置上的字符是字母或_,即匹配成功 | [A-Za-z_] |
\W | 表示该位置上的字符不是是字母或_,即匹配成功 | [Â-Za-z_] |
\s | 表示该位置上是不可见字符(空格、制表符\t、垂直制表符\v、回车符\r、换行符\n、换页符\f),即匹配成功 | [\f\n\t\r\v] |
\S | 表示该位置上不是不可见字符,即匹配成功 | [^\f\n\t\r\v] |
# \d的用法
import re
target = '点赞数:12'
result = re.findall('\d', target) # 这一行中的\d表示只要该位置上的字符是数字,就匹配成功,返回结果,一次只表示一个字符
print(result)
# \D的用法
import re
target = '点赞数:12'
result = re.findall('\D', target) # 这一行中的\D表示只要该位置上的字符不是数字,就匹配成功,返回结果,一次只表示一个字符
print(result)
# \w的用法
import re
target = 'I love python_'
result = re.findall('\w', target) # 这一行中的\w表示只要该位置上的字符是字母或者下划线,就匹配成功,返回结果,一次只表示一个字符
print(result)
# \W的用法
import re
target = 'I love python_'
result = re.findall('\W', target) # 这一行中的\W表示只要该位置上的字符不是字母或者下划线,就匹配成功,返回结果,一次只表示一个字符
print(result)
# \s的用法
import re
target = 'Life is short \n I love python'
result = re.findall('\s', target) # 这一行中的\s表示只要该位置上的字符是不可见字符,就匹配成功,返回结果,一次只表示一个字符
print(result)
# \S的用法
import re
target = 'Life is short \n I love python'
result = re.findall('\S', target) # 这一行中的\S表示只要该位置上的字符不是不可见字符,就匹配成功,返回结果,一次只表示一个字符
print(result)
第三类:量词
匹配规则(举例说明) | 释义 |
---|---|
{3} | 表示{3}前面的一个字符出现3次 |
{3,8} | 表示{3}前面的一个字符出现3-8次 |
? | 表示?前面的一个字符出现0次或1次 |
+ | 表示+前面的一个字符出现1次或无限多次 |
* | 表示*前面的一个字符出现0次或无限多次 |
案例一:
- 假设现在我们获取到了一本英文书的全部内容,想要判断这本书有多少字数,该怎么做呢?还是老规矩,我们通过代码来解释:
import re
content = "To be or not to be, that is a question."
result = re.findall('\w{1,30}', content) # 这一行中的\w表示一个字母或者_,{1,30}表示\w出现1次到30次之间,只要一个单词的长度在1‐30之间就能被匹配出来
print(result)
print(len(result))
- 上述代码还是存在某些缺陷,万一某个英文单词的长度超过30了怎么办?所以我们应该换种方式来写:
import re
content = "To be or not to be, that is a question."
result = re.findall('\w+', content) # +表示匹配一次或多次,只要一个单词的长度大于一就能被匹配出来
print(result)
print(len(result))
案例二:
- 提取文章的点赞数或者评论数
import re
content = '点赞数:12'
result = re.findall('\d{1,10}', content)# 这一行中的\d表示一个数字字符,{1,10}表示这个\d出现1‐10次都匹配成功,只要点赞数在0‐9999999999之间都可以匹配出来。
# 同样也可以这样写:
result = re.findall('\d+', content)
print(result)
至此,数量词我们已经讨论了{}和+,至于?和*,我们放在后面和贪婪与非贪婪一起讲,因为 ? 和 ** 还有 . 经常连在一起使用。
第四类:锚字符
匹配规则 | 释义 |
---|---|
^ | 表示只要是以^后面的字符开头的,即匹配成功 |
$ | 表示只要是以$前面的字符结尾的,即匹配成功 |
案例一
import re
content = 'https://www.zhihu.com'
content1 = '/question/62749917/answer/576934857'
result = re.findall('^http.*', content) # 这一行的^http表示匹配content的首部是http的内容,后面的.表示一个除换行符\n以外的所有字符,*表示.重复0次或无限多次,.*放在一起就是匹配除换行符以外的任意字符无限多次,这两个字符经常放在一起用,之后会单独讨论。
result1 = re.findall('^http.*', content1)
print('result的匹配结果:{}'.format(result))
print('result1的匹配结果:{}'.format(result1))
- 通过上述例子,我们能发现^其实就相当于我们之间讲的字符串的一个方法,叫做startswith。
- 同样的,有startswith,当然会有类似于endswith的匹配规则,我们用代码来看下:
import re
content = 'https://www.zhihu.com/shiyue.png'
content1 = '/question/62749917/answer/576934857'
result = re.findall('.*png$', content) # 这一行的.*和之前一样,表示png前面可以有除换行符之外的任意字符,png$表示匹配content以png结尾的内容。
result1 = re.findall('.*png$', content1)
print('result的匹配结果:{}'.format(result))
print('result1的匹配结果:{}'.format(result1))
第五类:组/()
- 我们为什么要有组这个概念呢,因为一个字符串中,我们有时候只想要其中某一连续的满足某个条件的字符串。
- 例如content = ‘发布于2018/12/23’,我们需要提取出其中的发布时间,用之间的\d是提取不出来的,因为\d提取不出/,这个时候就可以用到组的概念了。
- 现在,我们来看看什么是组:
匹配规则(举例说明) | 释义 |
---|---|
(\d+) | ()内的内容构成一个组,只要当前位置满足\d+就匹配成功,返回()内匹配成功的内容 |
案例一:提取文章的发布日期
import re
content = '发布于2018/12/23'
result = re.findall('.*?(\d.*\d)', content) # 这一行的.*表示匹配除换行符外的任意多个字符,?表示非贪婪匹配
# (\d.*\d)表示一个组,以数字开头,以数字结尾,.*表示中间可以是除换行符以外的任意多个字符
# 最终返回的结果就是括号内匹配到的结果。
print(result)
组的作用:只输出匹配组中条件的内容
不加括号的结果:
import re
content = '发布于2018/12/23'
result = re.findall('.*?\d.*\d', content)
print(result)
因为python默认会在正则表达式首尾各添加一个括号,第三行代码其实等价于
result = re.findall((’.?\d.\d’), content)
正则表达式中可以存在多个组,例如下面这种情况:
案例二:提取发布时间和发布人
import re
content = '发布于2018/12/23,发布人:九月'
result = re.findall('.*?(\d.*\d).*:(.*)', content)
print(result)
- 这里的前一部分和上面的代码是一样的意思,两个括号之间的内容.*:表示中间是除换行符以外的任意字符,直到遇见:才终止,进入第二个组。
- 所以上述正则表达式的意思是:以除换行符以外的任意字符开头,直到遇见第一个组,以数字开头,以数字结尾,这样就能匹配到发布时间2018/12/23,然后又是除换行符外的任意字符,直到遇见:进入第二个组,冒号后面所有的内容构成第二个组,匹配到发布人九月
- 会发现得到的结果好像和我们想的不一样呀,我们希望得到 (‘2018/12/23’, ‘九月’) ,虽然上述结果我们也方便获取我们想要的内容,但是能不能直接获取到类似 (‘2018/12/23’, ‘九月’) 呢?
- 这就需要用到re的另一个方法了,match方法。
re.macth()方法——从第一个字符开始匹配(默认在最前面带^)
案例三:提取发布时间和发布人
import re
content = '发布于2018/12/23,发布人:九月'
result = re.match('.*?(\d.*\d).*:(.*)', content) ## match方法的参数和findall是一样的,返回的结果是re.Match对象
print(result.group()) # 该方法默认是result.group(0)
- 前文说过re.match(’.?(\d.\d).:(.)’, content)等价于re.match(’(.?(\d.\d).:(.))’, content)
- result.group(0)获取的内容就是最外层的括号匹配的内容。
在用match方法的时候有一个需要注意的地方,很重要,非常容易导致出错,老规矩,用代码解释:
import re
content = '评论数:12'
result = re.match('\d', content)
print(result)
- 得到的结果是None,如果直接print(result.group())是会报错的。
- 原因在于match方法是从content第一个字符开始去匹配\d,如果未匹配到,直接就返回None。这里因为content第一个字符不是数字,所以直接返回None
第六类:贪婪与非贪婪
案例:提取发布时间
- 非贪婪模式
import re
content = '发布于2018/12/23'
result = re.findall('.*?(\d.*\d)', content) # 这里的?表示的就是非贪婪模式,第一个.*会尽可能少地去匹配内容,因为后面跟的是\d,所以碰见第一个数字就终止了。
print(result)
2. 贪婪模式
import re
content = '发布于2018/12/23'
result = re.findall('.*(\d.*\d)', content)
print(result)
这里的第一个 .* 后面没有添加问号,表示的就是贪婪模式,第一个 .* 会尽可能多地去匹配内容,后面跟的是\d,碰见第一个数字并不一定会终止,当它匹配到2018的2的时候,发现剩下的内容依然满足(\d.*\d),所以会一直匹配下去,直到匹配到12后面的/的时候,发现剩下的23依然满足(\d.*\d),但是如果.*再匹配下去,匹配到23的2的话,剩下的3就不满足(\d.*\d)了,所以第一个.*就会停止匹配,(\d.*\d)最终匹配到的结果就只剩下23了。
再来看下面这段代码:
import re
content = '发布于2018/12/23'
result = re.findall('.*(\d.*?\d)', content)
print(result)
得到的结果是[‘23’],原因在于第一个.*是贪婪模式,会一直匹配到12后面的/,这样结果就是[‘23’]
再来看下面这段代码:
import re
content = '发布于2018/12/23'
result = re.findall('.*?(\d.*?\d)', content)
print(result)
- 这里的第一个.*?表示非贪婪模式,匹配到2018前面的’于’之后就停止了
- 括号里的.*?也是表示非贪婪模式,括号里的内容从2018的2开始匹配,因为后面一个数字是0,那么也就满足了(\d.*?\d),所以就直接返回结果了,同样的,接下来的18也是这样,一直匹配到23才结束。
import re
content = '发布于2018/12/23'
result = re.match('.*?(\d.*?\d)', content)
print(result.group())
简单来说,贪婪模式就是尽可能多地去匹配字符,非贪婪模式就是尽可能少地去匹配字符,python默认采取的是贪婪模式。
贪婪与非贪婪在使用的要慎重,因为一不小心就容易出错,匹配到的结果并不是我们想要的。
re.search()方法
比较search和match方法的区别
import re
content = '点赞数:12'
result_match = re.match('\d', content)
result_search = re.search('\d', content)
- 可以看到,使用match方法,会从content的开头去匹配\d,没有匹配到就直接返回None了。
- 而search方法也是从头开始匹配,只要匹配到有一个字符符合\d,就直接返回了,不会继续往下匹配。
- search方法返回的也是一个re.Match对象,和match方法的取值是一样的,用group()。
re.sub(待匹配字符串, 待替换字符串, 操作对象[, count=0, flags=])方法
- 功能:匹配出结果并替换掉内容
- sub方法的第一个参数是正则表达式,第二个参数是替换之后的字符串,第三个参数是目标字符串
案例一:将content中的php全部换成python
import re
content = 'python php java c javascript java php'
result = re.sub('php', 'python', content)
print(result)
案例二:将content中的php全部替换成python,php不区分大小写
# 只替换小写部分
import re
content = 'python PHP java c javascript java php'
result = re.sub('php', 'python', content)
print(result)
# 替换大写和小写部分
import re
content = 'python PHP java c javascript java php'
result = re.sub('php', 'python', content, flags=re.I)
print(result)
-
flags=re.I表示的是匹配模式,re.I表示第一个参数不区分大小写
-
还有一种常用的匹配模式re.S,它表示“.”(不包含外侧双引号)的作用扩展到整个字符串,包括“\n”。(正常情况下 . 的扩展是按行进行的,也就是说遇到\n就停止匹配,然后从下一行重新开始匹配),加上re.S之后,就是把包括\n在内的文本作为一个整体来进行匹配
案例三:将第一个出现的php替换成python
import re
content = 'python PHP java c javascript java php'
result = re.sub('php', 'python', content, flags=re.I)
# re.sub的第四个参数是count,默认count=0,表示无论匹配到多少个php,都替换成python
result1 = re.sub('php', 'python', content, count=1, flags=re.I)
# 这里的count=1表示无论匹配到多少个php,最多只将第1个php替换成python,count=8的话就表示无论匹配到多少个php,最多只将前8个php替换成python
接下来,我们来看下sub方法设计的精妙之处,就是sub的第二个参数可以是一个函数。
精妙之处在哪呢,就在于当你拿到匹配结果的时候,不一定要将它替换成固定的字符串,你可以传递一个函数,在函数中对匹配结果进行逻辑处理,这样主动权就交到了用户手上,用户可以随便处理。
案例:将学生的分数替换成相应的等级,0-59分为不及格, 60-79分为中,80-89分为良,90-100分为优(这个功能用字符串的replace方法还是会比较复杂的。)
import re
def judge(value):
value = value.group() # 用group方法获取到匹配结果,以下逻辑是对value进行逻辑判断
if int(value) < 60:
return '不及格'
elif int(value) < 80:
return '中'
elif int(value) < 90:
return '良'
else:
return '优'
content = '小明:59 小红:66 小白:83 小绿:98 小王:100'
result = re.sub('\d+', judge, content)
# 这里sub的第二个参数就是一个函数judge,第一个参数匹配到的结果会作为value传递进judge函数中,从而在judge中可以对他进行判断,函数的返回值将会替换掉匹配结果。
print(result)
函数式编程
- 这个sub方法的使用并不难,可有一点值得我们去学习,那就是将函数作为一个参数传递到另一个函数中(而且返回值也可以是一个函数),这种编程思想叫做函数式编程。