想要学好一门技能,不仅仅要勤读书,还要勤练习。作为网络工程师的我来讲,为了适应网络变革浪潮不断的逼自己学习python,在公司内部也不断的推动网络向自动化的演进车轮。前进的道路总是曲折的,最近要分析防火墙20多G的log文件。想从中提取有用信息,无奈学艺不精写不出准确的正则表达式,不得不通过大量的if语句来判断,即吃力不讨好,又劳民伤财。故趁机系统学习一下python 正则表达式。这个东西我们经常是当工具来使用,也就是在需要的时候再去查一查,不用就抛之脑后。对于大多数人来讲这样并么有什么坏处,但是要想快速准确的匹配出自己想要的内容,那就得老老实实的掌握它的语法。以下进入主题部分。
此处抄用一下度娘的话吧:
正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
试想一下如果要判断010-1314520
是否为电话号码,那么通过python代码实现逻辑大概如下(伪代码):
if 是否是10个数字组成:
if 是否有“-”:
if 是否第4位是“-”
print(“这是一个电话号码结构”)
通过上面的伪代码,我们发现至少需要进行三次判断才可以确定该字符串据具有电话号码的结构。那如果用正则表达式又需要多少步呢?
import re
if re.compile(r"\d{3}-\d{7}").search("010-1314520"):
print("这是一个电话号码结构")
从行数上来看使用正则表达式也用了3行,但不同的是这是一个完整的代码,可以直接使用。相对于if判断语句利用正则表达式极大的简化了代码结构和运行效率更体现了程序的逼格。如果需要判断更多字符串那么正则表达式的优势将更加明显。这就是要学习正则表达式的主要原因:快速、准确、代码精简。
在python中要使用正则表达式,需要有三步:
import re
导入正则表达式模块,search(),findall(),sub()
等,本文只介绍这三个方法。group()
方法返回匹配的文本内容。import re #导入正则表达式库
a = re.compile(r"\d{3}-\d{7}") #创建regex对象
b = a.search("010-1314520") #通过regex对象的search方法来查找,如果查找成功返回match对象,否则返回None
print("电话号码是:%s" %b.group()) #通过group()方法读出文本内容
运行结果如下:
电话号码是:010-1314520
看到这里可能大家要问,搜索了有啥用? 莫慌先让我们看看这几个方法的返回值
regex()方法 | 返回值 | 备注 |
---|---|---|
search() | search()方法查找传入的字符串, 寻找该正则表达式的所有匹配。如果字符串中没有找到该正则表达式模式, search()方法将返回 None。如果找到了该模式,search()方法将返回一个 Match 对象。以字符串形式返回第一个 命中的内容 |
group(),groups()是search()的方法 |
findall() | 以列表形式返回所有命中的字符串,未命中则返回空列表,如果存在分组,则分组以元组形式出现 | |
sub() | sub(参数1,参数2),用参数1替换参数2中被匹配的字符 |
回到前面的问题,来回答正则表达式在编程中到底能干什么,我这里想到的是除了得到对应的返回值外我们可以根据返回值实现条件判断、赋值、文本替换等功能。还有什么高端骚操作请请速速打脸。
上面对正则表达式运行体系进行了介绍,下面将分别对每个组件进行解释说明:
regex对象这里就一句话带过了,不详细讲了,因为创建regex对象的目的是制订自己的正则表达式语法,而语法部分作为重中之重将在后面讲,请先稍安勿躁。现在先将几个常用的方法给大家介绍了着。
a = re.compile(r"\d{3}-\d{7}")
re.compile()
就是创建regex对象,r"\d{3}-\d{7}"
就是正则表达式语法,r表示元字符,不需要转义。
search()方法的参数是需要查找的字符串,它将在传入的字符串中进行匹配,而且只匹配第一次
命中的字符串。如果字符串中没有找到该正则表达式模式, search()方法将返回 None
。如果匹配成功则search()方法将返回一个 Match 对象。 Match 对象有一个 group()方法,它返回被查找字符串中实际匹配的文本(稍后我会解释分组)。示例如下,将之前的例子修改下,让其打印search()
的返回值和search().group()
的返回值
import re
a = re.compile(r"\d{3}-\d{7}") #该语法的意思是匹配“三个数字-七个数字”这样的结构的字符串
b = a.search("123-123456700000000000")
print(b)
print(type(b)) #查看search()返回值的类型
print(b.group())
以下运行结果证明search()
返回值是一个match对象,而match通过调用group()
方法将返回匹配的文本内容。
123-1234567
运行结果第一样显示未match对象;第二行判断b的类型的确为match类型;第三行显示了匹配的文本内容。
match对象也一言以蔽之,本质就是search()
方法的返回值,(想要看栗子还是上面那个栗子)如果search()
命中了就返回match对象,否则返回None
。match对象有group()
和groups()
两个方法,他们的作用是返回匹配的文本内容,区别在于在有分组(什么是分组?返回什么类型? 莫慌,后续道来。)情况下,返回的类型不一样。group()
返回的是字符串,而groups()
返回的是元组。
在了解了正则表达式组成部分后,现在可以开始语法部分了。好好学习,天天向上。Let go!!
用一个灵魂三问开启新篇章:何为分组?怎么实现分组?什么场景需要分组?我们按照这三个问题来一一解答。
分组就是用括号将正则表达式分成不同的小组,每个小组用括号来隔开(如果需要匹配括号需要转义" \( "或者" \) "
)。
plain ="123-123456700000000000" #假设这是一个被加了00000000000的电话号码
regex1 = re.compile(r"\d\d\d-\d\d\d\d\d\d\d") #参照组
regex2 = re.compile(r"(\d\d\d)-(\d\d\d\d\d\d\d)") #通过括号将语法分为两个组,组号从左到右,编号从1开始
result1 = regex1.search(plain).group()
result2 = regex2.search(plain).group()
print(result1)
print(result2)
\d
表示匹配所有数字。上面的语法表示匹配“xxx-xxxxxxx”这样的内容(x表示数字)。那么以下是上面的运行结果:
123-1234567
123-1234567
分组和不分组的group()结果都是一样的,那为什么还要分组呢? 好,让哥来告诉你,假设我现在只要电话号码的区号那怎么搞?
那我们就将电话号码的每一部分给他标个号,这个号就是组的序号,此处敲黑板通过括号将语法分为任意个组,组号从左到右,编号从1开始
也就是上面的两个括号,栗子还是那个栗子,此时我们使用带参数的group()
方法,尼玛,还有参数,复杂不? 负责的告诉你,不复杂,因为她的参数就是你想要的组的序号。比如上面我想要第一组 也就是区号, 那就group(1)
,想要电话号码那就group(2)
,简单不。 要是还不明白,建议默默点击右上角XX。来嘛,来嘛,照顾下看不懂的人嘛,顺便再演示下groups()
方法,上栗子;
plain ="123-123456700000000000"
regex1 = re.compile(r"\d\d\d-\d\d\d\d\d\d\d")
regex2 = re.compile(r"(\d\d\d)-(\d\d\d\d\d\d\d)")
result1 = regex1.search(plain).group()
result2 = regex2.search(plain).group()
result3 = regex2.search(plain).group(1) #待参数 组号1
result4 = regex2.search(plain).group(2) #待参数 组号2
result5 = regex2.search(plain).groups() #注意是group的复数形式,将以元组形式返回所有分组内容
print(result1)
print(result2)
print(result3)
print(result4)
print(result5)
运行结果如下:
123-1234567
123-1234567
123
1234567
(‘123’, ‘1234567’)
注意最后一行,这就是groups()
以元组方式返回了所有的分组内容,明白不,这些懂分组,group()
,groups()
了波?应该懂了吧。
介绍完分组,后面来说说“|”管道符,管道符在正则表达式语法中定义多个对象,这对象之间是或的关系,只要命中就返回,如果存在多个可能被命中的字符,那么只匹配第一个
。问题在于怎么判断第一个
呢? 继续吃栗子:
注意 plain和plain2的内容顺序不一样哦
plain ="""
xiaoming
xiaohua
xiaohong
"""
plain2 ="""
xiaohua
xiaoming
xiaohong
"""
regex1 = re.compile(r"xiaoming|xiaohua") #定义匹配xiaoming和xiaohua中的一个即可,注意"|"两侧的内容和regex2位置不一样
regex2 = re.compile(r"xiaohua|xiaoming") #
print(regex1.search(plain).group()) #在plain中匹配
print(regex2.search(plain).group())
print(regex1.search(plain2).group()) #在plain2中匹配
print(regex2.search(plain2).group())
运行结果如下:
xiaoming
xiaoming
xiaohua
xiaohua
可以看到前两个运行结果是一样的,后两个运行的结果是一样的。 那么为什么返回的值会不一样呢? 这里要敲黑板了,前面说到如果存在多个可以匹配的值,则返回第一个匹配的值,怎么判断第一个? 不是通过判断语法中正则表达式内容的顺序,而是文本中第一个出现的负责正则表达式语法的文本。 很明显plain首先出现xiaoming ; plain2首先出现xiaohua
什么意思呢? 就是有些字符可以匹配,也可以不匹配。 场景就是类似“xxx-xxxxxxx”的电话号码结构中可以匹配“-”,也可以不匹配“-”。因为你不能保证所有的电话号码格式都是通过“-”来分隔的。在吃栗子前,先提一下findall()方法,findall()是regex的方法,用于以列表形式返回所有符合条件的文本,该栗子将加入findall()调味:
import re
a = re.compile(r"\d{3}-?\d{7}") #问号前面的"-"可以匹配,也可以不匹配
print(a.findall("123-1234567 0101234567")) #使用findall()返回一切皆有可能的文本
运行结果如下:
[‘123-1234567’, ‘0101234567’]
明白了波?? 中间的“-”有了问号的加持,显得可有可无了。
在正则表达式中*号 是神一样的存在,表示匹配前一个字符或分组0次或多次。甜点如下:
plain="neeeeeeeeero" #9个e
print(re.compile(r"ne*ro").search(plain).group()) #将匹配以上文本
print(re.compile(r"nx*e*ro").search(plain).group()) #多加了一个x,但是在后面跟了星号,表示这个x可以出现,也可以不出现。故该语句能正确匹配以上文本
print(re.compile(r"n(ee)*ro").search(plain).group()) #将报错,因为这里是对(ee)分组进行多个,但是9个e不满足(ee)的倍数,所以search()会返回None,而None是没有任何方法的,故调用group()方法会报错
neeeeeeeeero
neeeeeeeeero
Traceback (most recent call last):
File “111.py”, line 38, in
print(re.compile(r"n(ee)*ro").search(plain).group())
AttributeError: ‘NoneType’ object has no attribute ‘group’
是不是对*的用法了如指掌了???
同上面的*号,区别在于+必须至少匹配一次。
plain="neeeeeeeeero" #9个e
print(re.compile(r"ne+ro").search(plain).group()) #将匹配以上文本
print(re.compile(r"nx+e+ro").search(plain).group()) #将至少匹配一次x,但是文本中没有x,所以匹配报错
+
和*
要对比着来记忆
顾名思义,用{}
控制前面的分组匹配的次数,这里还有个知识点就是贪心模式和非贪心模式,因为{x,y}有两个参数,分别代表最少
和最多
,那么可能出现多种匹配结果,正则表达式怎么去匹配,规定默认按照最多去匹配。 也就是默认是贪心模式。 那么怎么让他按照最短匹配呢? 直接在{}后面加?即可。直接上栗子:
plain="hahahahahaha" #5个ha
print(re.compile(r"(ha){1,}?").search(plain).group()) #将(ha)匹配一次或多次
print(re.compile(r"(ha){,1}").search(plain).group()) #将(ha)匹配0次或1次
print(re.compile(r"(ha){3,5}").search(plain).group()) #将(ha)匹配3次到5次,默认贪心模式,将匹配最多次数
print(re.compile(r"(ha){3,5}?").search(plain).group()) #将(ha)匹配3次到5次,按最短匹配
运行结果:
ha
ha
hahahahaha
hahaha
在前面有提到findall()是regex的方法和search()属于同一level,故他们之间是二选一的方法,findall()用于以列表形式返回所有符合条件的文本。但是需要注意的是如果正则表达式中有分组出现,那么列表中将嵌套元组,元组中每个元素为一个分组。栗子:
plain="tel 010-123-1234567 mobile 123-123-1234567"
print(re.compile(r"(\d{3})-(\d{3})-(\d{7})").findall(plain)) #正则表达式中有分组
print(re.compile(r"\d{3}-\d{3}-\d{7}").findall(plain)) #正则表达式中无分组
运行结果:
[(‘010’, ‘123’, ‘1234567’), (‘123’, ‘123’, ‘1234567’)]
[‘010-123-1234567’, ‘123-123-1234567’]
很明显第一行的列表中显示了嵌套了两个元组,每个元组中的内容均为正则表达式的一个分组。
字符分类是通过[]
或者特定的字符来缩短正则表达式长度的一种方式。那么要掌握这种方法需要涉及以下两方面的知识:
[]
的应用:[0-9]
表示(0|1|2|3|4|5|6|7|8|9)
,类似的[a-zA-Z]
表示匹配所有字母,[0-9a-zA-Z]
表示匹配所有数字和字母缩写字符分类 | 描述 |
---|---|
\d | 0 到 9 的任何数字 |
\D | 除 0 到 9 的数字以外的任何字符 |
\w | 任何字母、数字或下划线字符(可以认为是匹配“单词”字符) |
\W | 除字母、数字和下划线以外的任何字符 |
\s | 空格、制表符或换行符(可以认为是匹配“空白”字符) |
\S | 除空格、制表符和换行符以外的任何字符 |
xmasRegex = re.compile(r'\d+\s\w+')
print(xmasRegex.findall('12 drummers, 11 pipers, 10 lords, 9 ladies, 8 maids, 7 swans, 6 geese, 5 rings, 4 birds, 3 hens, 2 doves, 1 partridge'))
正则表达式\d+\s\w+
匹配的文本有一个或多个数字(\d+)
, 接下来是一个空白字符(\s)
, 接下来是一个或多个字母/数字/下划线字符(\w+)
。 findall()方法将返回所有匹配该正则表达式的字符串, 放在一个列表中。
[‘12 drummers’, ‘11 pipers’, ‘10 lords’, ‘9 ladies’, ‘8 maids’, ‘7 swans’, ‘6 geese’, ‘5 rings’, ‘4 birds’, ‘3 hens’, ‘2 doves’, ‘1 partridge’]
这时候可以建立自己的字符分类了,比如只匹配元音字符,re.compile(r"[aeiouAEIOU]")
这里还有个知识点,就是若在[]前面加上^
表示匹配除了[]这里面的字符,如re.compile(r"^[aeiouAEIOU]")
表示匹配非元音字符。特别注意:[]里面的字符不需要转义。
可以在正则表达式的开始处使用插入符号^
,表明匹配必须发生在被查找文本开始处
。类似地,可以再正则表达式的末尾加上美元符号$
,表示该字符串必须以这个正则表达式的模式结束。可以同时使用^
和$
,表明整个字符串必须匹配该模式,也就是说,只匹配该字符串的某个子集是不够的。来来继续吃栗子:
plain = "you are a bad boy"
plain1 = "I said: you are a bad boy"
regex = re.compile(r"^y.*y$") #匹配以y开始以y结束的字符串
print(regex.findall(plain)) #匹配成功
print(regex.findall(plain1)) #匹配失败,因为不是以y开始
运行结果如下:
[‘you are a bad boy’]
[]
特别注意:^
表示从需要查找的文本的开始出进行匹配,而不是文本中间某处符合正则表达式即可。
在正则表达式中 句点 “.”
表示匹配除换行符之外的所有字符。所以如果想匹配任何字符串可以用人r".*"
来实现:
plain = "you are a bad boy"
plain1 = "I said: you are a bad boy\n????"
regex = re.compile(r".*")
print(regex.search(plain).group())
print(regex.search(plain1).group())
运行结果如下:
you are a bad boy
I said: you are a bad boy
运行结果显示匹配出了plain的全部字符,但是plain1的\n???并没有被匹配,因为“\n”表示换行,通匹配是不能匹配的,如果要匹配怎么办呢?需要使用re.DOTALL
参数。
plain1 = "I said: you are a bad boy\n????"
regex = re.compile(r".*",re.DOTALL)
print(regex.search(plain1).group())
运行结果:
I said: you are a bad boy
???
很明显,全部命中。
正则表达式参数总结:
re.I
:表示匹配的时候不区分大小写;re.VERBOSE
:忽略正则表达式字符串中的空白符和注释.,如果要将正则表达式写成多行,需要三引号。re.DOTALL
:匹配字符串中的换行符。正则表达式不仅能找到文本模式, 而且能够用新的文本替换掉这些模式。 Regex对象的 sub()方法需要传入两个参数。第一个参数是一个字符串, 用于取代发现的匹配。第二个参数是一个字符串,即正则表达式。 sub()方法返回替换完成后的字符串。例如, 在交互式环境中输入以下代码
plain1 = "I said: you are a bad boy\n????"
regex = re.compile(r"you are",re.DOTALL)
print(regex.sub("he is",plain1))
该代码使用“he is” 替换字符串中的 “you are”,下面是运行结果:
I said: he is a bad boy
???
到此为止,正则表达式常用方法就这么多了,正则表达式理论简单,但要用得好,还得用的多。
- 《Python学习手册》
- 《Python编程快速上手-让繁琐工作自动化》