正则表达式是一种通用的工具,并不只属于Python语言,基本大部分语言都封装好了这个工具。
正则表达式(Regular Expression)是一种用于做字符串匹配的工具,它能够非常方便地从一段文本中找到/匹配出符合一定要求/规律的目标字符串。
但是我们什么情况下要做字符串匹配呢?而且为什么要用正则表达式做呢,直接用一对一的去对不行吗?举一个简单的例子来回答上面的问题。
比如说,我们有如下一段文本,假设这是你写的一段日记:
我昨天认识了一个女孩A,她给了我她的邮箱girlA@163.com,和她道别后,又遇到另一个女孩B,她也给了我她的邮箱girlB@qq.com,没想到,我之后又遇见了第三个女孩C......
那么,现在,你肯定想做的事情就是给这些遇见的女孩子们群发约会的邀请信息,但是想偷懒的你不想手动一个个地去从上述文本里肉眼查找,然后复制粘贴每个女孩的邮箱(数量少时,你手动做肯定没问题)。于是,你想能不能用一个工具自动提取出所有日记里面的邮箱地址出来。
然后你也想到,首先每个人邮箱的名字都不一样,可能是各种数字与字母的组合,其次邮箱所属的机构名(163, sina, qq, gmail, outlook)也可能不一样,域名(.com, .net, .cn)也可能不一样,那么这可怎么匹配?
但是,聪明的你总结出了一条规律:邮箱不过就是【若干个字符(邮箱名)+@+若干个字符(机构名)+.+若干个字符(域名)】,如果程序能够懂这个模式就能挑选出字符串了!
现在告诉你,正则表达式就可以做到,它就可以按照【若干个字符(邮箱名)+@符号+若干个字符(机构名)+.+若干个字符(域名)】的模式去从文本里把所有符合这个模式的字符串全部找出来。
用python3具体做法如下:
import re
pattern=re.compile(r'[0-9a-zA-Z_]{0,19}@[0-9a-zA-Z]{1,13}\.[com,cn,net]{1,3}')
text='''我昨天认识了一个女孩A,她给了我她的邮箱girlA@163.com,
和她道别后,又遇到另一个女孩B,她也给了我她的邮箱girlB@qq.com,
没想到,我之后又遇见了第三个女孩C......'''
match = pattern.findall(text)
for email in match:print(email)
# girlA@163.com
# girlB@qq.com
搞定!
目前你看不懂上面的代码没关系,下面我们来一一讲解。
要想用好正则表达式,首先要学习正则表达式的使用语法/使用规则。
图来自Python正则表达式指南
我们按照图里面的顺序来分别讲解不同正则表达式不同部分的语法:
如”a”,”b”,”g”,”4”,”,”等这种比较常用的字符,在正则表达式中都是一对一地匹配和自身相同的字符,没什么特别。
import re
text='abcdefg'
match=re.search('cd',text)
if match:print(match.group())
# cd 匹配到cd
.(一个点)用于匹配任意除了换行符”\n”以外的字符。
import re
text='ab3defg'
match=re.search('b..',text)
if match:print(match.group())
# b3d 匹配到b及其后紧跟的两个任意字符
反斜杠表示转义字符,它将使得紧跟在它后面的字符转变成特殊的含义,或者消除特殊字符本身的特殊含义。(注意,后面的\d 和\w等正则本身包含的元字符中的反斜杠不属于此类转义作用,而是正则中规定好的组合,元字符里的反斜杠就是普通的反斜杠字符。这一点目前看不懂没关系,结合后面的注意事项部分理解)
比如当\与.(点)结合在一起,点就不再是匹配任意单个字符,而是确实地匹配一个点(消除特殊含义)。
中括号中放上任意多的字符,则这些字符会构成一个字符集合,这个模式将会在遇到集合中的任意一个字符时都认定为匹配。
import re
text='abc345def'
match=re.search('[345]',text)
if match:print(match.group())
# bc3 由于匹配到了(3,4,5)中的3,不再继续向后匹配。match=re.search('[b5aef]',text)
if match:print(match.group())
# a 由于匹配到了(b,5,a,e,f)中的a,不再继续向后匹配
d是decimal(十进位的)首字母,代表数字。w
\d用来匹配任意单个数字(0到9),而\D则是匹配任意的非数字(大写就是反过来,比较好记)
import re
text='abc345def'
match=re.search('\d\d',text)
if match:print(match.group())
# 34 匹配到连续的两个数字match=re.search('\D\D',text)
if match:print(match.group())
# ab 匹配到连续的两个非数字字符
s代表space(空格,空白),可以匹配空格,\t制表符,\r,\n换行符,\f,\v这几个不会在屏幕上输出可见字符的字符。
import re
text='abc3 45def'
match=re.search('\d\s\d',text)
if match:print(match.group())
# 3 4 匹配到数字+空格+数字的三个连续字符的组合
w代表word(单词),能构成word的字符包括大小写的字母(a-z和A-Z),也包括数字(0-9),还包括下划线_。
import re
text='_a3%sdef'
match=re.search('\w\w\w\w',text)
if match:print(match.group())
# sdef 匹配到连续的四个单词字符,但不能匹配"_a3%",因为%是非单词字符
首先明白,数量词不能单独使用,是配合前面的字符使用,用以描述对前面的字符进行什么数量的匹配。
星号*表示对前面的字符串进行0次或多次匹配(多次即大于等于1次)。
import re
text='_a34534%1sdef'
match=re.search('a\d*',text)
if match:print(match.group())
# a34534 匹配到a与其后的多个数字,直到遇到第一个非数字的%号停止匹配。match=re.search('a\s*\d*',text)
if match:print(match.group())
# a34534 依然是匹配到a与其后的多个数字。虽然在字符a与数字之间没有任何空白字符,但是由于星号允许匹配0个(即没有),所以也算匹配。
简而言之,星号是有则匹配,没有也不要紧,继续往后走。
加号相对星号就“真的很严格~~”,因为它要求前面的字符至少匹配一次,多次也ok。
承接上面星号中的例子,只是把”\s*”改成了”\s+”:
import re
text='_a34534%1sdef'
match=re.search('a\s+\d*',text)
if match:print(match.group())
输出为空白,因为字符a和后面的数字之间根本没有空白字符,于是匹配不成功,返回空。
import re
text='_a34534%1sdef'
match=re.search('a\d+',text)
if match:print(match.group())
# a34534 这样就还是能匹配到字符a及后面的数字。
问号就是确认一下有木有,有或没有都行,但是你有的话,就只能有一个,不准多了,多了它不负责匹配。
import re
text='_a34534%1sdef'
match=re.search('a\d?',text)
if match:print(match.group())
# a3 匹配到a及后面紧接的一个数字,不再继续向后匹配。match=re.search('a\d*!?',text)
if match:print(match.group())
# a34534 匹配到a及后面的数字,又尝试匹配数字后的感叹号,发现没有(实际是百分号),于是停止匹配。
这个就比较简单,就是匹配前一个字符m次。
import re
text='_a34534%1sdef'
match=re.search('\d{3}',text)
if match:print(match.group())
# 345 匹配数字字符三次。
这个也很简单,就是匹配n到m次,即大于等于n,小于等于m的任一次数都可以。不过会尽量往多的次数匹配。
import re
text='_a34534%1sdef'
match=re.search('\d{1,4}',text)
if match:print(match.group())
# 3453 匹配数字1-4次,因为尽量往多的匹配,匹配了4个数字match=re.search('\d{1,3}s',text)
if match:print(match.group())
# 1s 匹配数字1-3次及后面的s字符
注意,这里的问号不是前面讲的“匹配字符0或1次”的那个问号的作用,而是用于声明使用懒惰模式(或者叫非贪婪模式)来进行匹配。
什么叫贪婪模式?即默认情况下,凡是涉及到数量词的匹配,正则表达式的匹配工具都是尽可能地往次数多的去匹配的(例子见上面讲{m,n}时的第一个示例代码),这就是贪婪模式(尽可能多就是很“贪婪”嘛~)。而非贪婪模式/懒惰模式就是相反地做尽可能少的匹配。
两种情况下,星号、加号、问号、{m,n}实际上就分别成了以下作用:
符号 | 贪婪模式 | 非贪婪模式 |
---|---|---|
* | 多次(无限次) | 0次 |
+ | 多次(无限次) | 1次 |
? | 1次 | 0次 |
{m,n} | n次 | m次 |
举个简单的例子:
import re
text='_a34534%1sdef'
match=re.search('\d{2,3}',text)
if match:print(match.group())
# 345 匹配到连续的三个数字(贪婪模式)match=re.search('\d{2,3}?',text)
if match:print(match.group())
# 34 匹配到连续的两个数字(非贪婪模式)
^代表匹配字符串的开头(在多行模式中匹配每一行的开头)。$代表匹配字符串的末尾(在多行模式中匹配每一行的末尾)。
这两个符号主要是针对只想匹配文本的开头或结尾部分的情况使用的。而当一个正则表达式同时使用两个符号”^XXXXXX$”时,意思就是对字符串进行从头到尾的整体匹配。
比如我们在任意网站注册账号时要求填入邮箱信息,那么网站的后台一般会对邮箱做合法性检验,那么就会对我们输入的邮箱地址做从头到尾的正则匹配,看是否符合要求。
import re
text='%a1234'
match=re.search('a\d{4}',text)
if match:print(match.group())
# a1234 匹配到a和连续的4个数字match=re.search('^a\d{4}',text)
if match:print(match.group())
# 输出空白,因为从头开始匹配,而开头是百分号,不是a,匹配失败。
\A和^的作用相同,\Z和$的作用相同。
圆括号会将包含在里面的所有内容作为一个分组的整体,我们可以在分组加数量词来匹配分组若干次。
import re
text='_a34a534b123%1sdef'
match=re.search('(a\d{2,3})+',text)
if match:print(match.group())
# a34a534 匹配分组[字符a+2-3个数字]一次或多次
竖线本身会将正则表达式切分成左右两个部分,而竖线的作用就是:优先匹配左边的部分,如果匹配到了,就完成匹配;如果未匹配到,再匹配右边的部分,如果匹配到了,就完成匹配,否则,匹配失败。
import re
text='_a34a524b1234%1sdef'
match=re.search('[a-z]\d{5}|[a-z]\d{4}',text)
if match:print(match.group())
# b1234 竖线左边小写字母和5个数字的匹配未成功,转而匹配右边小写字母和4个数字的匹配成功。
常用的正则语法也就是上面这些,其它的大家可以再需要用到时再回来看和理解,这里就先不详细讲解了。
r表示raw(原生的,未加工的),放在字符串前,表示这个字符串为原生字符串,而原生字符串指的是区别于正则表达式的一般字符串。
强调原生字符串这种概念的主要原因是针对反斜杠(\)的问题。因为反斜杠在一般编程语言的字符串里的含义是转义符,转义符的作用在前面也有提到过,就是转换或消除后面紧跟的一个字符的特殊含义。即单个的反斜杠出现时,就会被认为是在对紧跟在它后面的一个字符进行转义操作。
但是问题是,有时候我们可能想匹配的文本就是一个包含有反斜杠字符的字符串,那么我们在正则里面就需要把反斜杠的转义的作用给消除掉,而表达出我们就是要匹配一个反斜杠字符的意思。
首先,我们要明白的是,如果我们想在一个普通字符串里面表达一个反斜杠字符,而非转义符时,应该这么做:
string='abc\ndef'
print(string)
# abc
# def
# 会分两行输出abc和def,因为\n中的反斜杠被认为成转义符,和n结合时就成了换行符。# 第一种方法:反斜杠+反斜杠
string='abc\\ndef'
print(string)
# abc\ndef
# 只输出一行,因为\\n中的第一个反斜杠作为转义符,将后面的第二个反斜杠给转义了,使其成为了普通的反斜杠字符(消除特殊含义)。# 第二种方法:声明原生字符串
string=r'abc\ndef'
print(string)
# abc\ndef
# 只输出一行,因为被声明成原生字符串的字符串,其里面所有的字符都将被消除特殊含义,于是单个的反斜杠也不是转义符了,而成了普通的反斜杠字符。
好,回到我们的正则表达式,正则表达式里如果我们想写一个能匹配反斜杠的表达式,怎么写呢?
import re
text=r'%abc\ndef'
match=re.search('\\\\',text)
if match:print(match.group())
# \
对,没有错,你得写四个反斜杠才行!!!原因是,一般的字符串里你想表达一个反斜杠字符得用两个反斜杠(\),但是在正则表达式里规定了:表达一个普通字符串里的反斜杠字符需要用两个反斜杠字符(很绕很奇怪我知道- -!),于是这代表着,你需要组合两个反斜杠字符(\),即四个反斜杠一起,才能在正则表达式里面表达出一个反斜杠字符的含义。
于是,为了简化这个问题,防止在正则表达式里面写反斜杠写到晕厥,我们可以用原生字符串的方法来减少麻烦:
import re
text=r'%abc\ndef'
match=re.search(r'\\',text) # 不是转义
if match:print(match.group())
# \
即,当你把正则表达式声明为原生字符串时,里面所有的反斜杠也不再具备转义含义,而是纯粹的反斜杠字符,于是只用写两个就可以表示普通字符串里面的一个反斜杠字符了。(所以追根溯源,这个坑就是因为正则里面规定了表达式里的两个反斜杠字符才能算普通字符串里的一个反斜杠字符)。
总结:一般情况下,我们也提倡在正则表达式的前面加个r声明这是原生字符串,因为可以减少匹配反斜杠字符时候的麻烦(写两个反斜杠就够了),也不影响其它正则元字符的正常使用(\d,\s,\w等),该咋写咋写。
这个部分比较绕,不过实际情况中也并没有很多需要做反斜杠字符匹配的地方,所以不太理解此部分问题也不大,能够顺畅使用正则表达式里的上述其它语法也已经可以应对绝大多数匹配问题了。
另外,字符串前面有时还有带一个u字母的,意思是表示该字符串是一个unicode编码的字符串。详情参见python字符串前缀 u和r的区别。