17. 正则表达式
Hi,大家好。我是茶桁。
不知不觉中,咱们针对人工智能的 Python 课程已经过去了一半。相信大家这段时间也都有所进步了。
今天这节课呢,我给大家划一个重点。不仅仅是 Python,很多语言里都是通用的,而且非常的强大。这就是我们的正则表达式。
说起正则表达式,很多程序员其实对其都不是很重视,但是学好它,必定在处理数据的时候事倍功半。虽然内容看似不多,但是市面上有一本经典的「精通电子表达式」整本书还是非常厚的。当然,它比咱们今天要讲的内容详尽的多了。听完我这节课之后想继续研究正则的小伙伴,推荐这本书(唯一推荐)。
正则表达式是什么呢?其实就是使用字符、转义字符和特殊字符组成一个规则,使用这个规则对文本的内容完成一个搜索或匹配或替换的功能。
正则表达式的组成
正则表达式内,包含了普通字符,转义字符,特殊字符以及匹配模式:
接下来我们看几个栗子:
这样,我们就匹配了一个字符串。如果我们想匹配数字,那myReg=521
就能够匹配了。不过,现在我有一个新需求,我们重新定义一下myStr = 'iloveyou521to123simida'
, 我们可以看到,这个字符串十分的混乱,数字和字母都是混在一起的。现在,我就想把数字都单独的拎出来,又该怎么做呢?来,让我们试试看:
这样,我们使用\d
这个转义字符匹配到了字符串内相关的所有数字,返回了一个列表。
可是这还是不符合我们的要求,我们想要的是将其中的数字组合匹配出来,而不是单独的数字。接着继续改:
将原本一个\d
改为了三个叠在一起的\d\d\d
, 这样,我们匹配到了三位数字的组合。注意,我在原本的字符串内又加入了一个四位的数字组合7894
,但是也只匹配出了789
,那也就是说,这种数字匹配方式,有几个转义字符组合在一起,那就匹配出多少位; \d
这个转义字符就是代表单个的数字。
整个代码中,findall
就是正则中的相关函数,除了findall
之外,还有一些其他函数,我们一起来认识下:
re
模块的函数
match
与search
match
和search
经常是被放在一起来进行讨论的,因为这两个函数很像。具体它们有什么作用和区别呢?我们直接上代码,一点点讲:
可以看到,虽然第二段打印中,我输入的me
也可以从字符串中找到,但是因为不是从起始位置匹配的,所以返回了None
。
再来看看search
:
search
方法中,无论我们要匹配的字符是在起始位置还是结束位置,只要是能找到,都会返回其位置。
有小伙伴们可能会奇怪,我在成功返回的末尾都加了一个span()
, 是不是这个影响的原因?我们来看看这两个方法匹配成功后的返回值就明白了:
其完整的返回值应该是这样的,我后面加的span()
只是为了获取返回值中的span
信息。
所以对于这两个函数,我们可以稍微归纳一下:
re.match()
函数:
会从头开始进行匹配,如果第一个就符合要求,那么匹配成功
如果第一个不符合规则,则返回
None
匹配成功后返回
Match
对象成功后可以使用
group()
和span()
方法获取数据和下标区间
re.search()
函数:
从字符串的开头开始进行搜索式的匹配
匹配成功则返回
Match
对象,匹配失败者返回None
成功后可以使用
group()
和span()
方法获取数据和下标区间
两者的区别:
match
方法是从字符串的开头进行匹配,如果开始就不符合正则的要求,则匹配失败,返回 None。search
方法是从字符串的开始位置一直搜索到字符串的最后,如果在整个字符串中都没有匹配到,则失败,返回 None
在看完match
和search
之后,我们再来看看re
模块的其他函数:
re.findall()
这个函数在文章开头我们就用到了,但是并未给大家进行详解。现在我们就来认识一下:
可以看到,和一开头我们所写的不同,这次返回的参数列表内出现了两个love
, 原因就是我对myStr
这个变量又重新定义了一下,在接近尾部的地方多加了一个love
。
从这我们也能看出来了,findall
这个函数是按照正则表达式的规则在字符中匹配所有符合规则的元素,结果返回一个列表,如果没有找到的情况下,会返回一个空列表。
re.finditer()
从返回的结果中看到,这个函数返回的是一个迭代器。那让我们利用迭代器规则来试试看:
在第三个next
方法的时候报错了,那和我们使用findall
结果是一致的,返回了两个love
。并且,finditer
方法会返回每一个匹配值的下标范围。使用span()
来获取到这个范围。
re.sub()
这个函数方法和之前介绍的方法有些不太一样了,以上我们所使用的可以说都是搜索、查找。那这个函数就是修改了。其功能是按照正则表达式的规则,在字符串中找到需要被替换的字符串,完成一个替换。主要参数有三个:
pattern
: 正则表示的规则,匹配需要被替换的字符串repl
: 替换后的字符串string
: 被替换的原始字符串
这样,我们对整个myStr
就完成了特定字符串的替换,将其中的love
全部替换成了live
。
re.split()
这个方法会按照指定的正则规则,进行数据切割。
在这段代码中,我们将原字符串以空格
来进行风格,将整个字符串分割成了一个列表。其原理和我们将字符串时讲到的基本一致,这里就不详细讲解这个函数了。
compile()
这个函数可以直接将正则表达式定义为「正则对象」, 使用正则对象直接操作。
我们现在来看一个示例:
假如说,我有下面这样一个列表:
我现在想要从中找到所有的数字,那么使用之前所学的内容,当然我们想到的一定是for
循环。
可以看到,我们确实正确的拿到了arr
中的相关数字。
这里,我们稍微讲解一下正则表达式中的\d{n}
, 在正则中,\d
是转义字符我们之前学过了,但是其实,我们并不需要写三个\d
来去匹配三个数字,那如果真是这样的话,我们要匹配几十个数字的时候怎么办呢?这个时候我们可以用到\d{n}
这样的写法,大括号中的n
表示的就是前面这个\d
的匹配连续匹配 n 次。那么,我们原本的newReg='\d\d\d'
就可以改为newRge='\d{3}'
。
让我们回过头来继续,刚才我们使用了for
循环来完成了依次取值对吧。这个时候,让我们深入search()
这个方法的源码去看看,其中是长这样的:
那么根据这个return
的结构来看,我们是不是可以这样来写:
确实,我们获得了我们想要的结果。
那上下两种写法的区别在于哪里呢?
其实,我们第一种写法里,我们定义了一个正则规则,然后传给search(myReg, i)
之后,search
方法在其内部先是调用了一下_compile
, 生成了一个正则对象,在这之后,才又传给了search
方法,最后得到结果。
而我们在第二种写法中,直接用compile
函数将规则定义成了一个对象,使用search
直接得到了结果。
你们注意看结构,是不是我第二个写法里,for
循环里的myReg
实际上就是search
方法内的_compile(pattern, flags)
。
那我们这样写有什么意义呢?呃,实际上,从性能上来说虽然是快了一些,但是也不见得快多少。更多的是想让大家养成一个去方法源码中探究逻辑的好习惯。
那么接下来,才是这节课的重点。大家集中注意力,我们开始。
正则表达式的规则
在本文的最开头,我们就先给到了正则表达式的基本规则,我们拿下来再复习一下:
那其实,我们之前介绍相关函数方法的时候,所使用到的基本都是普通字符,其中也用到了\d
这个转义字符,明白\d
就是去匹配数字。
普通字符
普通字符实际上就是最简单的匹配方式,你写什么就是什么。可以理解为,我在全文中去搜索一个单词或者数字。而且我们之前也使用过多次了,所以这部分我们就不再继续向西介绍了。
转义字符
转义字符包括:\w, \W, \d, \D, \s, \S ...
什么都是从代码里去理解最直接,让我们先来定义一个字符串待用:
然后我们一个一个来看:
\w
, 这个转义字符匹配的内容是单个字母、数字、下划线
我们看,在最后的结果中,我们匹配到了这个字符串中所有的字母,数字和下划线。其中的特殊字符和制表符都被过滤掉了。
\W
, 注意,现在这个W
是大写的。那这个转义字符规则会去匹配单个「非」字母、数字,下划线。啥意思?简单,看代码:
看到区别了吧?和\w
(小写)完全相反,之前匹配到的字母、数字、下划线一个没匹配到,而之前没被匹配到的特殊字符和制表符,则被匹配后组成了一个列表。
\d
,这个转义字符规则会匹配单个的数字。这个我们之前用过了,这里就不演示了。
\D
, 这个转义字符实际也非常简单,就是匹配非数字。注意到了吧?所有的转义字符里,小写字母大写之后,其匹配的内容都是相反的。
结果也是,除了数字其他的内容都被匹配到了。
\s
, 这个转义字符规则是匹配单个的空格符或制表符。
结果却是如此,唯一的制表符被匹配了出来。(初学时,我一度长期混乱的认为\s
是匹配所有字符串,大家别犯我一样的错误。)
\S
,那这个大写字母的匹配规则不用说,一定是匹配空格或制表符之外的所有内容:
打印结果验证了我们的猜想。
\w{4}\d
, 基本转义字符我们都介绍完了,这里我们来看看组合在一起会是什么样。其中的\w{4}
大家也应该明白其含义,就是\w\w\w\w
。
可以看到,这样组合之后匹配出来的就是 4 个字母、数字、下划线+一个数字。那是不是这次匹配我们也可以写成\w{5}
呢?反正最后匹配出来的love5
不也就是 5 个w
的组合么?
那我们试试看就知道了:
没想道结果是这样的对吧?这是因为,\w{5}
中,最后一位可以是字母,数字或者下划线都可以,而我们用\w{4}\d
则是最后一位必须是数字,不能是其他的。
特殊字符
特殊字符包括:. * ? + ^ $ [] {} ()
这回,让我们再重新定义一段合适的字符串
然后,我们还是一个一个的来看:
.
,表示匹配单个的任意字符,当然也有例外,就是除了换行符。
字符串内的所有内容,我们都匹配了出来,然后组成了一个列表。
*
, 这个特殊字符需要和其他的匹配进行组合使用,它表示的是任意次数。具体什么意思呢?我们还是从代码里去看看是如何表现的:
这段代码我们应该是比较熟悉了,\w
和search
方法我们都已经学过了。那这个其实就是在字符串中从开头去匹配\w
对吧?
我们再来继续往下看:
我们看结果,这次search
在匹配到第一个\w
之后,又继续向后匹配了,直到遇到空格才停了下来。就是因为\w
后面加了一个*
, 所以就会一直匹配任意次数,直到\w
不再匹配了才结束。
这里有一个概念,就是*
代表的匹配任意次数,为什么我要强调这个呢?就是任意次数其实是包含0
次的。也就是说,我们只要使用了*
这个特殊字符,那么就算没有符合匹配项,一样是酸是匹配成功了,只是返回的是空而已。我们来看看:
我们在之前的字符串前面加了一个空格,按道理说是不符合\w
匹配的。那么match
从字符串的开头开始进行搜索式的匹配,没有就返回None
对吧?可是这次,并没有返回None
,我们看到match=''
, 也就是说,它匹配成功了,只是成功了 0 次。所以按照*
匹配任意次的规则,它不会返回None
。这里比较绕,大家好好理解一下。我们继续。
+
, 这又是一个和其他规则配合使用的特殊字符,和*
一样,它也表示匹配次数,但是这个表示的是至少要求匹配一次。正好,我们之前改造的字符串最前面多加了一个空格,让我们来看看:
按search
从字符串的开头开始进行搜索式的匹配,是不是这里我们应该返回l
了?然而并没有,却返回了like
,这是为什么呢?
原因就在于+
这个特殊字符,它也并不是只拿到第一个就罢休了,和\w*
一样,一直往后匹配,直到第二个空格的时候,才宣告罢休。所以\+
是「至少」匹配一次,0
次不干,而1
之后如果可以连续,那就继续匹配。
?
, 这个特殊字符的作用是「拒绝贪婪」,看着很特殊是吧?其实就是,在?
之前的规则,只要达成即可。
看,我们之前使用\w+
的时候,+
这孩子遇到第一个还不满足,非要向后继续拿。可是这个时候我多加了一个家长?
,勒令+
既然目的已经达到了,就不要再继续了。这就是?
字符「拒绝贪婪」的作用。
{}
, 这个特殊字符咱们用过了,应该大家也都知道它的含义。就是重复多少次。
从结果我们可以看到,因为前面的like
只有四位,并不符合连续匹配五次的标准,所以最后被匹配出来的是chahe
。
{}
这个特殊字符其实还有一种用法,就是可以给定范围。
我们知道,下标 1 到下标 4 这个范围内,正好是like
。
‘[]’
, 这个特殊字符代表字符的范围。用于匹配范围内说包含的字符。使用[]
我们可以更精准的筛选出我们需要匹配的内容。
可以看到,不同的组合匹配出了不同的范围内的单个字符。
[A-Za-z0-9_]
这个组合等价于\w
。
()
,这个特殊字符代表的子组,括号中的表达式首先作为整个正则的一部分,另外会把符合小阔中的内容单独提取一份。我们先看一段代码:
这个组合我们匹配到了_2ial2345LoveYou
, 有且只有这一个组合了。不过这不是我想要的,我想要的是什么呢?是这段匹配出的字符串,并且,我还想要这段字符串中那段数字作为单独的匹配出来。那这个时候我们怎么做呢?
我们将用()
将前中后包裹了起来,希望得到一个子组,而中间的部分,就是我们想要得到的 4 个数字的组合。
^
和$
这两个特殊字符实际上属于「定位符」,^
是匹配输入字符串开始的位置。$
是匹配输入字符串结尾的位置。
那我们从一个案例中来了解一下这两个定位符的使用:
返回了None
, 这又是为什么呢?原因就在于,我们用^
限制了必须是 1 开始,而用了$
来限制了到结尾必须是后面有十位数字,而我们数一下,我们给定了 12 位数字,超出了一位,才会匹配不上。
如果我们去掉限制结尾$
再来看看:
可以看到,我们遵循了开头位 1 和后面跟十位数字的规则,但是原字符串中多出来的一位数字被直接过滤掉了。那我们并不知道,用户输入的数字中到底是哪个位置多输入了一个数对吧?
再来看,我们把结尾限制加上,但是开头限制改一个数字:
因为开头匹配不对,所以返回了None
。
所以,争取的匹配方式至少有三个条件:
1 开头, 当然,如果能把运营商的所有开头数字都拿到,那我们能够匹配的条件就变多了。
必须全部是数字
必须是 11 位
这样,我们可以加上开头限制和结尾限制,正好能满足一个简易的手机号匹配规则:
正则的模式
在正则的模式中,包含了一下几个模式:
re.I
: 不区分大小写
re.L
: 作本地化识别匹配
re.M
: 多行匹配,会影响到^
个$
re.S
:使用.
匹配包括换行在哪的所有字符
re.U
:根据Unicode
字符集解析字符。这个标志影响\w, \W, \b, \B
re.X
: 改标志通过给予更灵活的格式以便将正则表达式写的更易于理解。
实际上,我们虽然列出了这么多模式,真正常用的,也就是re.I
这个模式:
可以看到,当我设定了不区分大小写的情况下。字符串中的所有英文字母都被匹配了出来。
练习:
这次,我们和以往不同,将练习和课程放在了一起。至于为什么嘛,只是因为我课程写完之后发现还有时间。^_^
这次我们作这样一个练习:
验证邮箱
首先让我们来看,邮箱的格式基本包含以下内容:
123456@qq.com
纯数字chaheng@qq.com
纯字母chaheng75@126.com
数字加字母cha_heng@163.com
混合型chaheng@vip.163.com
多级域名chaheng@hivan.me
企业邮箱(企业域名)cha.heng@gmail.com
包含特殊字符.
好,让我们来看,我们以@
来前后区分,那么我们先看左边,会包含的内容就是数字,字母,下划线,特殊字符
,让我们先来写一下规则试一下:
[a-zA-Z0-9]+([_\.][a-zA-Z0-9])*
那么右边的部分呢?
@(\w)+\.[a-z]{2,6}
让我们结合其实试试看:
感觉是 OK。那让我们多测试下试试:
多级域名的测试没有通过,在函数内打印出了None
。我们回过头来看看:
既然是多级域名出现了问题,那问题肯定出现在多出的那一个点上,我们这样改:
注意,我们加上了^
和$
符号。
然后我们再重新试试看,这次呢,我不想一个个实验了,让我们来定义一个数组来批量测试:
这一下子我们就将刚才想到的格式都测试完了,最后我们还特意加了一个不符合的格式来测试下康康是否被过滤了出来。结果说明我们写的正则没有问题。
手机号码
就像我们上面写验证手机号正则提到过的,我们可以定义一个所有运营商可能的开头来做头部验证:
实际上,我们还可以分的更细一点,区分运营商。不过在这个练习中,没必要分的这么细了。
来,我们实现一下:
正确的辨认并打印了出来,不正确的也辨认了出来。
匹配 IP 地址
我们来看看,一个正确的 IP 地址(IPv4),是由四个三位数来组成的,包含:
既然是这样一个格式,我们来思考一下,
首先,我们需要匹配0 ~ 199
的范围,也就是 0 或者 1 开头,这个比较简单:
这一段匹配中:
[0-1]?
表示匹配 0 或 1 一次或者零次\d
就是要匹配任意单个数字。0~9 都可以{1,2}
, 给定范围 1 ~ 2, 表示前面的\d
出现 1 次或者 2 次。
然后我们需要来匹配200 ~ 255
范围, 这个范围内比较复杂,包含了两种情况,一种是201 ~250
的情况,一种是251 ~ 255
的情况
这一段匹配,一开表示数字2
开头
然后 2 之后用异或限定了开头可以是5
或者可以是0-4
之间的任意数字。
是5
的话,后面需要匹配 0-5,是
4的话,后面需要匹配
0-9`。
既然一次的匹配已经有了,那剩下的和第一次也都一样,就好些了,我们再多加一个.
的匹配:
让我们来试试:
从结果中可以看到,到底还是有漏网之鱼。第三个和最后两个判断都失误了。这是为什么呢?
似乎最后一位被截断判断了,也就是说,它并没有判断三位数。
哦,我大概猜到了。让我们把头部匹配和结尾匹配加上再试试:
这回没问题了。我们的正则匹配算是完成了。
那这节课下来之后,大家要多去理解,多去练习。这节课对于打算玩数据的人正的很重要。
好了,那我们下节课再见吧。
版权声明: 本文为 InfoQ 作者【茶桁】的原创文章。
原文链接:【http://xie.infoq.cn/article/a21189f8ad02dee07e5b84bae】。文章转载请联系作者。
评论