正则表达式是每个程序员都绕不过的一道坎,也是最容易被忽视的一门艺术,相信大多数程序员跟我之前一样在工作中使用正则的时候,都是Google一下,然后复制过来改一改。正则给程序员的感觉就是一门很难掌握和利用好的一门工具。最近,在爬虫项目中大量用到了正则,为此,查询了好多文献资料,系统的学习了一下正则表达式,做了一些总结分享给大家。
正则表达式是利用单个字符来描述、匹配一系列符合某个句法规则字符串的技术。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。在编程项目中,正则也常被用于文本字符串的查找、替换、切分和提取。
几乎所有的编程语言都支持正则表达式,但不同的编程语言对正则的规则和定义有所差别,本文对于正则的讨论是基于python语言的
一、五类元字符
元字符就是指那些在正则表达式中具有特殊意义的专用字符,元字符是构成正则表达式的基本元件。正则就是由一系列的元字符组成的,比如\d可以表示0-9之间的任意数字,\w可以表示字母、数字、下划线中任意字符。正则中的元字符非常多,可分为以下几类:
1、特殊单字符:英文的.表示换行以外的任意字符,\d表示任意单个数字,\w表示字母、数字、下划线的任意字符,\s表示任意的空白字符,相应地\D,\W,\S分别是对对应小写的取反。
2、空白符:除了特殊单字符外,你在处理文本的时候肯定还会遇到空格、换行等空白符。其实在写代码的时候也会经常用到,换行符 \n,TAB 制表符 \t
\s可以表示以上所有空白字符,包括空格
3、量词:特殊单字符和空白符都只能匹配单个字符,但在实际匹配过程中会有出现几次、至少出现几次、最多出现几次等规则,这时候就需要用到量词,正则中的量词元字符主要有六种:*表示0次或多次,+表示1次或多次,?表示0次或1次,{m}m次,{m,}至少m次,{m,n}m到n次
4、范围:之前所有元字符都只能匹配单个字符或者单字符的重复,有时需要对完整字符串(比如匹配good或者well等表示好的单词)进行匹配或者多个不同单字符(比如匹配元音aeiou),这时候就用到了范围元字符,范围元字符主要四种:| 或运算符,可匹配前后两个字符串中的任意一个可用于字符串的匹配,比如ab|bc 可匹配文本中的ab和bc,[....]中括号 [] 代表多选一,可以表示里面的任意单个字符,所以任意元音字母可以用 [aeiou] 来表示,中括号中的中划线可表示范围[a-z]可表示a到z的任意字母,如果中括号第一个是脱字符(^),那么就表示非,表达的是不能是里面的任何单个元素。
5、断言:断言又称锚点,用来限定字符出现的位置,比如要替换tom字符串为jerry,如果不用断言那么tomorrow的前三个字符也会被替换,这显然是错误的。断言主要分为三类
替换前:tom asked me if I would go fishing with him tomorrow. 替换后:jerry asked me if I would go fishing with him jerryorrow.
5.1单词边界:\b 来表示单词的边界,
利用断言来就可以完成以上替换案例案例了
str =" tom asked me if I would go fishing with him tomorrow." re.sub(r"\btom\b","jerry",str) 替换后:jerry asked me if I would go fishing with him tomorrow.
5.2行的开始/结束:行的开头和结尾有两套元字符^和$,\A和\Z,两者的区别是当匹配为多行模型【后面会有介绍】时,前者匹配的是每一行的开头和结尾,而后者一直匹配整个字符串的开头和结尾。
5.3 环视又称零款断言:在一些场景下需要对要匹配的字符串左右做限定,这就用到了环视。比如我们需要对11位的电话号码做匹配时,12位数字的前11位也会被匹配上,22位数也会被匹配前11位,此时就需要用到环视,即左右都不能是数字。
对于环视可能上边的表有点复杂,其实本质就是左尖括号代表看左边,没有尖括号是看右边,感叹号是非的意思
二、两种量词匹配方式
上一个模块已经介绍了六种量词元字符,其实{m,n}这种形式完全可以替代*,+,?,其中+和*因为有无穷多的属性,需要引进贪婪匹配和非贪婪匹配,先来看一个例子
+号的匹配比较好理解,*的匹配会匹配上四个空,是因为*是匹配0次或者多次。针对无穷次匹配属性就引入了贪婪模型:尽可能长的匹配,正则默认就是贪婪模式和非贪婪模式,尽可能少的去匹配,非贪婪模式就是在+或者*后面加个?就可以了。
三、四种匹配模式
所谓匹配模式就是改变元字符本身匹配行为的方式,具体分为四类:
1、忽略大小写模式,(?i)比如 不区分 大小写匹配a,python中提供了re.IGNORECASE参数也可以实现忽略大小写功能
2、.点号通配模式,也称为单行模式(?s).号本身是匹配换行以外的任意字符的,利用此模式可以使.号匹配任意字符功能相当于[\W\w]
3、多行匹配模式,通常情况下,^ 匹配整个字符串的开头,$ 匹配整个字符串的结尾。多行匹配模式改变的就是 ^ 和 $ 的匹配行为,这在之前断言中介绍过,(?m)实现多行模式,多行模式匹配每一行的开头结尾,这在日志分析中识别每条以时间开始的日志行非常有用
4、注释模式,即为正则表达式添加注释,便于后期维护与复盘(?#comment)
四、小括号的作用
()小括号可以说是正则中使用频率最高,功能最强大的一类符号,那正则中的效果好具体有哪些功能呢?
除了分组引用以外,所有内容前面均已讲过了,这里主要介绍分组引用功能。
1、分组与编号
括号在正则中可以用于分组,被括号括起来的部分 “子表达式” 会被保存成一个子组。那分组和编号的规则是怎样的呢?其实很简单,用一句话来说就是,第几个括号就是第几个分组。这么说可能不好理解,我们来举一个例子看一下。这里有个时间格式 2020-05-10 20:23:05。假设我们想要使用正则提取出里面的日期和时间。
2、不保存分组
在括号里面的会保存成子组,但有些情况下,你可能只想用括号将某些部分看成一个整体,后续不用再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用?: 不保存子组。如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以不保存子组可以提高正则的性能。除此之外呢,这么做还有一些好处,由于子组变少了,正则性能会更好,在子组计数时也更不容易出错。那到底啥是不保存子组呢?我们可以理解成,括号只用于归组,把某个部分当成 “单个元素”,不分配编号,后面不会再进行这部分的引用
3、括号嵌套
前面讲完了子组和编号,但有些情况会比较复杂,比如在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组怎么办?不要担心,其实方法很简单,我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组。
4、命名分组
前面我们讲了分组编号,但由于编号得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,还可能导致编号发生变化,因此一些编程语言提供了命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。命名分组的格式为 (?P < 分组名> 正则)。
5、后向引用
在知道了分组引用的编号 (number)后,python中,我们就可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引用
五、四类正则转义场景
1、转义字符:\反斜杠是python中的转义字符,\后的字符就会改变字符本来的意思
2、字符串转义和正则转义
可能所有的编程教程中都讲过在正则中要表示反斜杠需要用四个反斜杠,估计有很多程序员可能都不知道底层的原理。从输入字符串到 最终的正则表达式经历了两次转义过程。其中可以使用r避免字符串转义,即用原生字符串进行匹配
3、正则中的元字符转义
如果现在我们要查找比如星号(*)、加号(+)、问号(?)本身,而不是元字符的功能,这时候就需要对其进行转义,直接在前面加上反斜杠就可以了
4、括号的转义
在正则中方括号 [] 和 花括号 {} 只需转义开括号,但圆括号 () 两个都要转义
>>> import re>>> re.findall('\(\)\[]\{}', '()[]{}')['()[]{}']>>> re.findall('\(\)\[\]\{\}', '()[]{}') # 方括号和花括号都转义也可以['()[]{}']
5、字符组中的转义
以上描述了元字符的转义,字符组中的转义有三种情况
5.1脱字符在中括号中,且在第一个位置需要转义
>>> import re>>> re.findall(r'[^ab]', '^ab') # 转义前代表"非"['^']>>> re.findall(r'[\^ab]', '^ab') # 转义后代表普通字符['^', 'a', 'b']
5.2中划线在中括号中,且不在首尾位置
>>> import re>>> re.findall(r'[a-c]', 'abc-') # 中划线在中间,代表"范围"['a', 'b', 'c']>>> re.findall(r'[a\-c]', 'abc-') # 中划线在中间,转义后的['a', 'c', '-']>>> re.findall(r'[-ac]', 'abc-') # 在开头,不需要转义['a', 'c', '-']>>> re.findall(r'[ac-]', 'abc-') # 在结尾,不需要转义['a', 'c', '-']
5.3右括号在中括号中,且不在首位
>>> import re>>> re.findall(r'[]ab]', ']ab') # 右括号不转义,在首位[']', 'a', 'b']>>> re.findall(r'[a]b]', ']ab') # 右括号不转义,不在首位[] # 匹配不上,因为含义是 a后面跟上b]>>> re.findall(r'[a\]b]', ']ab') # 转义后代表普通字符[']', 'a', 'b']
6、字符组中其他的元字符
一般来说如果我们要想将元字符(.*+?() 之类)表示成它字面上本来的意思,是需要对其进行转义的,但如果它们出现在字符组中括号里,可以不转义。这种情况,一般都是单个长度的元字符,比如点号(.)、星号(*)、加号(+)、问号(?)、左右圆括号等。它们都不再具有特殊含义,而是代表字符本身。但如果在中括号中出现 \d 或 \w 等符号时,他们还是元字符本身的含义。
>>> import re>>> re.findall(r'[.*+?()]', '[.*+?()]') # 单个长度的元字符 ['.', '*', '+', '?', '(', ')']>>> re.findall(r'[\d]', 'd12\\') # \w,\d等在中括号中还是元字符的功能['1', '2'] # 匹配上了数字,而不是反斜杠\和字母d
六、一个方法论
正则表达式的一个方法论:某个位置上可能有多个字符的话,就⽤字符组。某个位置上有多个字符串的话,就⽤多选结构。出现的次数不确定的话,就⽤量词。对出现的位置有要求的话,就⽤锚点锁定位置。