本文主要参考:http://www.liujiangblog.com/course/python/73

1.正则概要

正则表达式(regular expression)是一种工具,一种广泛用于匹配字符串的工具。它用一个“字符串”来描述一个特征,然后去验证另一个“字符串”是否符合这个特征。比如 表达式“ab+”描述的特征是“一个'a'和任意个'b'”,那么'ab','abb','abbbbbbbbbb'都符合这个特征。

正则表达式本质上只做一件事,那就是编写一个表达式“字符串”,然后用这个字符串去匹配目标文本。核心的核心,都在编写这个“字符串”表达式上面。

学习正则表达式需要理解正则的功用及其使用场景,当然更重要的是要牢记它的基本语法和元字符。

在正式学习正则之前,需要知道几件事:

  • 正则表达式本质上是一门语言,它不从属与Python!Python只是将他人写好的正则引擎集成到了语言内部,大多数编程语言都是这么干的!
  • 正则表达式诞生的时间很长,应用非常广泛,是业界公认的字符串匹配工具。虽然有不同版本的内部引擎,但基本通用,也就是说,你在Python内写的正则表达式,可以移植到Linux的shell,Java语言等任何支持正则的场景中去。
  • 正则表达式默认从左往右匹配。
  • 正则表达式默认是贪婪模式。
  • 正则表达式默认在匹配到了内容后,则终止匹配,不继续匹配。
  • 对同一个问题,编写的正则表达式不是唯一的!

2.正则语法

正则表达式的语法是其最基础也是最重要的部分,需要牢记

2.1普通字符

字母、数字、汉字、下划线、以及没有特殊定义的符号,都是"普通字符"。正则表达式中的普通字符,在匹配的时候,只匹配与自身相同的一个字符。

例如:表达式c,在匹配字符串abcde时,匹配结果是:成功;匹配到的内容是c;匹配到的位置开始于2,结束于3。(注:下标从0开始还是从1开始,因当前编程语言的不同而可能不同)

2.2元字符

正则表达式中使用了很多元字符,用来表示一些特殊的含义或功能。

表达式               匹配
.            # 小数点可以匹配除了换行符\n以外的任意一个字符
|            # 逻辑或操作符
[]           # 匹配字符集中的一个字符
[^]          # 对字符集求反,也就是上面的反操作。尖号必须在方括号里的最前面
-            # 定义[]里的一个字符区间,例如[a-z]
\            # 对紧跟其后的一个字符进行转义
()           # 对表达式进行分组,将圆括号内的内容当做一个整体,并获得匹配的值

示例:

a.c匹配abc

(a|b)c匹配acbc

[abc]1匹配a1或者b1或者c1

使用方括号[]包含一系列字符,能够匹配其中任意一个字符。用[^]包含一系列字符,则能够匹配其中字符之外的任意一个字符。

[ab5@]匹配ab5@

[^abc]匹配a,b,c之外的任意一个字符

[f-k]匹配f~k 之间的任意一个字母

[^A-F0-3]匹配A~F以及0~3之外的任意一个字符

2.3转义字符

一些无法书写或者具有特殊功能的字符,采用在前面加斜杠"\"进行转义的方法。

表达式              匹配
\r, \n           # 匹配回车和换行符
\t               # 匹配制表符
\\               # 匹配斜杠\
\^               # 匹配^符号
\$               # 匹配$符号
\.               # 匹配小数点.
\?               # 匹配问号?
\*               # 匹配星号*

所有正则表达式中具有特殊含义的字符在匹配自身的时候,都要使用斜杠进行转义。这些转义字符的匹配方法与普通字符类似,也是匹配与之相同的一个字符。

例如表达式\$d,在匹配字符串"abc$de"时,匹配结果是:成功;匹配到的内容是$d;匹配到的位置开始于3,结束于5。

2.4预定义匹配字符集

正则表达式中的一些表示方法,可以同时匹配某个预定义字符集中的任意一个字符。比如,表达式\d可以匹配任意一个数字。虽然可以匹配其中任意字符,但是只能是一个,不是多个。注意大小写:

表达式             匹配
\d             # 任意一个数字,0~9 中的任意一个
\w             # 任意一个字母或数字或下划线,也就是 A~Z,a~z,0~9,_ 中的任意一个
\s             # 空格、制表符、换页符等空白字符的其中任意一个
\D             # \d的反集,也就是非数字的任意一个字符,等同于[^\d]
\W             # \w的反集,也就是[^\w]
\S             # \s的反集,也就是[^\s]

例如表达式\d\d,在匹配abc123时,匹配的结果是:成功;匹配到的内容是12;匹配到的位置开始于3,结束于5。

2.5重复匹配

前面的表达式,无论是只能匹配一种字符的表达式,还是可以匹配多种字符其中任意一个的表达式,都只能匹配一次。

如果要匹配多次,就可以使用表达式再加上修饰匹配次数的特殊符号{},不用重复书写表达式就可以重复匹配。比如[abcd][abcd]可以写成[abcd]{2}

表达式                匹配
{n}            # 表达式重复n次,比如\d{2}相当于\d\d,a{3}相当于aaa
{m,n}          # 表达式至少重复m次,最多重复n次。比如ab{1,3}可以匹配ab或abb或abbb
{m,}           # 表达式至少重复m次,比如\w\d{2,}可以匹配a12,_1111,M123等等
?              # 匹配表达式0次或者1次,相当于{0,1},比如a[cd]?可以匹配a,ac,ad
+              # 表达式至少出现1次,相当于{1,},比如a+b可以匹配ab,aab,aaab等等
*              # 表达式出现0次到任意次,相当于{0,},比如\^*b可以匹配b,^^^b等等

注意!比如ab{1,3}中重复的是b而不是ab(ab){1,3}这样重复的才是ab。表达式\^*b中重复的是\^而不是^,要从左往右读正则表达式,转义符号有更高的优先级,需要和后面的字符整体认读。

表达式\d+\.?\d*在匹配It costs $12.5时,匹配的结果是:成功;匹配到的内容是12.5;匹配到的位置开始于10,结束于14。

表达式go{2,8}gle在匹配Ads by goooooogle时,匹配的结果是:成功;匹配到的内容是goooooogle;匹配到的位置开始于7,结束于17。

2.6位置匹配

有时候,我们对匹配出现的位置有要求,比如开头、结尾、单词之间等等。

表达式            匹配
^              # 在字符串开始的地方匹配,符号本身不匹配任何字符
$              # 在字符串结束的地方匹配,符号本身不匹配任何字符
\b             # 匹配一个单词边界,也就是单词和间隙之间的位置,符号本身不匹配任何字符
\B             # 匹配非单词边界,即左右两边都是\w范围或者左右两边都不是\w范围时的字符缝隙

示例:

表达式^aaa在匹配xxx aaa xxx时,匹配结果是:失败。因为^要求在字符串开始的地方匹配。

表达式aaa$在匹配xxx aaa xxx时,匹配结果是:失败。因为$要求在字符串结束的地方匹配。

表达式.\b.在匹配@@@abc时,匹配结果是:成功;匹配到的内容是@a;匹配到的位置开始于2,结束于4。

表达式\bend\b在匹配weekend,endfor,end时,匹配结果是:成功;匹配到的内容是end;匹配到的位置开始于15,结束于18。

2.7贪婪与非贪婪模式

在重复匹配时,正则表达式默认总是尽可能多的匹配,这被称为贪婪模式。比如,针对文本dxxxdxxxd,表达式(d)(\w+)(d)中的\w+将匹配第一个d和最后一个d之间的所有字符xxxdxxx。可见,\w+在匹配的时候,总是尽可能多的匹配符合它规则的字符。同理,带有?*{m,n}的重复匹配表达式都是尽可能地多匹配。

但是有时候,这种模式不是我们想要的结果,比如最常见的HTML标签匹配。假设有如下的字符串:

<table>
    <tr>
        <td>苹果</td>
        <td>桃子</td>
        <td>香蕉</td>
    </tr>
</table>

我们的意图是获取每个<td></td>标签中的元素内容,那么如果你将正则表达式写成<td>(.*)</td>的话,你得到的是<td>苹果</td><td>桃子</td><td>香蕉</td>这么个东西,而不是“苹果”、“桃子”、“香蕉”。

这就需要使用非贪婪模式。

在修饰匹配次数的特殊符号后再加上一个?问号,则可以使匹配次数不定的表达式尽可能少的匹配,使可匹配可不匹配的表达式,尽可能的"不匹配"。如果少匹配就会导致整个表达式匹配失败的时候,与贪婪模式类似,非贪婪模式会最小限度的再多匹配一些,以使整个表达式匹配成功。

表达式<td>(.*?)</td>匹配上面的字符串时,将只得到<td>苹果</td>,再次匹配下一个时,可以得到<td>桃子</td>,以此类推。

针对文本"dxxxdxxxd"举例:

表达式(d)(\w+?)中的\w+?将尽可能少的匹配第一个d之后的字符,结果是只匹配了一个"x",整体只匹配了dx

表达式(d)(\w+?)(d)为了让整个表达式匹配成功,\w+?不得不匹配xxx才可以让后边的d匹配,从而使整个表达式匹配成功。因此,结果是\w+?匹配了xxx,整体匹配了dxxx

2.8反向引用

表达式在匹配时,表达式引擎会将小括号()包含的表达式所匹配到的字符串记录下来。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。这是一个非常有用也非常重要的特性。在实际应用场合中,当用某种边界来查找,而所要获取的内容又不包含边界时,必须使用小括号来指定所要的范围。比如前面的 <td>(.*?)</td>"

其实,"小括号包含的表达式所匹配到的字符串"不仅是在匹配结束后才可以使用,在匹配过程中也可以使用。表达式后边的部分,可以引用前面"括号内的子匹配已经匹配到的字符串"。引用方法是\加上一个数字。\1引用第1对括号内匹配到的字符串,\2 引用第2对括号内匹配到的字符串……以此类推,如果一对括号内包含另一对括号,则外层的括号先排序号。换句话说,哪一对的左括号"("在前,那这一对就先排序号。举例如下:

表达式('|")(.*?)(\1)在匹配'Hello', "World"时,匹配结果是:成功;匹配到的内容是'Hello'。再次匹配下一个时,可以匹配到 "World"。这里的(\1),动态的引用了('|")匹配到的结果。

表达式(\w)\1{4,}在匹配aa bbbb abcdefg ccccc 111121111 999999999时,匹配结果是:成功;匹配到的内容是ccccc。再次匹配下一个时,将得到999999999。这个表达式要求\w范围的字符至少重复5次,注意与\w{5,}之间的区别。

表达式<(\w+)\s*(\w+(=('|").*?\4)?\s*)*>.*?</\1>在匹配<td id='td1' style="bgcolor:white"></td>时,匹配结果是成功。如果<td></td>不配对,则会匹配失败;如果改成其他配对,也可以匹配成功。这就是常用的HTML标签匹配方法。

3. re模块

3.1概述

上面是正则表达式本身的语法知识,并未涉及实际使用的方法。下面将介绍在Python语言中如何使用正则表达式。

在Python中,通过内置的re模块提供对正则表达式的支持。正则表达式会被编译成一系列的字节码,然后由通过C编写的正则表达式引擎进行执行。

反斜杠的困扰:\

与大多数编程语言相同,正则表达式里使用\作为转义字符,这可能造成反斜杠困扰。假如需要匹配文本中的字符\,那么使用编程语言表示的正则表达式里将需要4个反斜杠\\\\。前两个和后两个分别用于在编程语言里转义成反斜杠,转换成两个反斜杠后再在正则表达式里转义成一个反斜杠。为了方便我们使用个,Python提供了原生字符串的功能,很好地解决了这个问题,这个例子中的正则表达式可以使用r"\\"表示。同样,匹配一个数字的"\\d"可以直接写成r"\d"。有了原生字符串,你再也不用担心是不是漏写了反斜杠,写出来的表达式也更直观。

3.2方法

re提供了下面的方法进行字符串的查找、替换和分割等各种处理操作。

方法                      描述                                      返回值
compile()          # 根据包含正则表达式的字符串创建模式对象            re对象
search()           # 在字符串中查找                                第一个匹配到的对象或者None
match()            # 在字符串的开始处匹配模式                       在字符串开头匹配到的对象或者None
findall()          # 列出字符串中模式的所有匹配项                    所有匹配到的字符串列表
split()            # 根据模式的匹配项来分割字符串                    分割后的字符串列表
sub()              # 将字符串中所有的pat的匹配项用repl替换           完成替换后的新字符串
finditer()         # 将所有匹配到的项生成一个迭代器                  所有匹配到的字符串组合成的迭代器
subn()             # 在替换字符串后,同时报告替换的次数               完成替换后的新字符串及替换次数
escape()           # 将字符串中所有特殊正则表达式字符串转义            转义后的字符串
purge()            # 清空正则表达式
template()         # 编译一个匹配模板                              模式对象
fullmatch()        # match方法的全字符串匹配版本                    类似match的返回值

3.2.1 compile(pattern, flags=0)

这个方法是re模块的工厂方法,用于将字符串形式的正则表达式编译为Pattern模式对象,可以实现更高效率的匹配。第二个参数flag是匹配模式。

使用compile()完成一次转换后,再次使用该匹配模式的时候就不用进行转换了。经过compile()转换的正则表达式对象也能使用普通的re方法。

经过compile()方法编译过后的返回值是个re对象,它可以调用match()、search()、findall()等其他方法,但其他方法不能调用compile()方法。实际上,match()和search()等方法在使用前,Python内部帮你进行了compile的步骤。

那么是使用compile()还是直接使用re.match()呢?看场景!如果你只是简单的匹配一下后就不用了,那么re.match()这种简便的调用方式无疑来得更简单快捷。如果你有个模式需要进行大量次数的匹配,那么先compile编译一下再匹配的方式,效率会高很多。

3.2.2 match(pattern, string, flags=0)

match()方法会在给定字符串的开头进行匹配,如果匹配不成功则返回None,匹配成功返回一个匹配对象,这个对象有个group()方法,可以将匹配到的字符串给出。

>>> ret = re.match(r"abc","ab1c123")
>>> print(ret)
None
>>> re.match(r"abc","abc123")
<_sre.SRE_Match object; span=(0, 3), match='abc'>
>>> obj = re.match(r"abc","abc123")
>>> obj.group()
'abc'

3.2.3 search(pattern, string, flags=0)

在文本内查找,返回第一个匹配到的字符串。它的返回值类型和使用方法与match()是一样的,唯一的区别就是查找的位置不用固定在文本的开头。

3.2.4 findall(pattern, string, flags=0)

作为re模块的三大搜索函数之一,findall()和match()、search()的不同之处在于,前两者都是单值匹配,找到一个就忽略后面,直接返回不再查找了。而findall是全文查找,它的返回值是一个匹配到的字符串的列表。这个列表没有group()方法,没有start、end、span,更不是一个匹配对象,仅仅是个列表!如果一项都没有匹配到那么返回一个空列表。

>>> obj = re.findall(r"abc","123abc456abc789")
>>> obj
['abc', 'abc']
>>> obj.group()
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    obj.group()
AttributeError: 'list' object has no attribute 'group'
>>> obj = re.findall(r"ABC","123abc456abc789")
>>> print(obj)
[]

3.2.5 split(pattern, string, maxsplit=0, flags=0)

re模块的split()方法和字符串的split()方法很相似,都是利用特定的字符去分割字符串。但是re模块的split()可以使用正则表达式,因此更灵活,更强大,而且还有“杀手锏”。看下面这个例子,匹配模式是加减乘除四个运算符中的任何一种,通过split()将字符串分割成一个一个的数字:

>>> s = "8+7*5+6/3"
>>> import re
>>> a_list = re.split(r"[\+\-\*\/]",s)
>>> a_list
['8', '7', '5', '6', '3']


# split有个参数maxsplit,用于指定分割的次数:
>>> a_list = re.split(r"[\+\-\*\/]",s,maxsplit= 2)
>>> a_list
['8', '7', '5+6/3']

利用分组的概念,re.split()方法还可以保存被匹配到的分隔符,这个功能非常重要!为什么呢?比如,你要计算8+7,是不是要同时获得8,+,7三个字符?就如同下面的例子,字符串s = "8+7*5+6/3",想要计算字符串内的表达式的值,你必须获得其中加减乘除的符号

>>> a_list = re.split(r“([\+\-\*\/])”,s)    # 注意这里添加了括号!
>>> a_list
['8', '+', '7', '*', '5', '+', '6', '/', '3']

3.2.6 sub(pattern, repl, string, count=0, flags=0)

sub()方法类似字符串的replace()方法,用指定的内容替换匹配到的字符,可以指定替换次数。

>>> s = "i am jack! i am nine years old ! i like swiming!"
>>> import re
>>> s = re.sub(r"i","I",s)
>>> s
'I am jack! I am nIne years old ! I lIke swImIng!' 

4.flag匹配模式

Python的re模块提供了一些可选的标志修饰符来控制匹配的模式。可以同时指定多种模式,通过与符号|来设置多种模式共存。如re.I | re.M被设置成IM模式。

匹配模式                   描述
re.A              # ASCII字符模式
re.l              # 使匹配对大小写不敏感,也就是不区分大小写的模式
re.L              # 做本地化识别(locale-aware)匹配
re.M              # 多行匹配,影响 ^ 和 $
re.S              # 使 . 这个通配符能够匹配包括换行在内的所有字符,针对多行匹配
re.U              # 根据Unicode字符集解析字符。这个标志影响 \w, \W, \b, \B
re.X              # 该标志通过给予你更灵活的格式以便你将正则表达式写得更易于理解

5.分组功能

Python的re模块有一个分组功能。所谓的分组就是去已经匹配到的内容里面再筛选出需要的内容,相当于二次过滤。实现分组靠圆括号(),而获取分组的内容靠的是group()、groups()和groupdict()方法,其实前面我们已经展示过。re模块里的几个重要方法在分组上,有不同的表现形式,需要区别对待。

以下内容是一些分组示例。

例一:match方法,不分组时的情况:

import re

origin = "hasdfi123123safd"
# 不分组时的情况
r = re.match("h\w+", origin)
print(r.group())         # 获取匹配到的整体结果
print(r.groups())        # 获取模型中匹配到的分组结果元组
print(r.groupdict())     # 获取模型中匹配到的分组中所有key的字典

 # 结果:
hasdfi123123safd
()
{}

例二:match()方法,有分组的情况(注意圆括号!)

import re

origin = "hasdfi123123safd123"
# 有分组
r = re.match("h(\w+).*(?P<name>\d)$", origin)
print(r.group())      # 获取匹配到的整体结果
print(r.group(1))     # 获取匹配到的分组1的结果
print(r.group(2))     # 获取匹配到的分组2的结果
print(r.groups())     # 获取模型中匹配到的分组结果元组
print(r.groupdict())  # 获取模型中匹配到的分组中所有key的字典


# 执行结果:
hasdfi123123safd123
asdfi123123safd12
3
('asdfi123123safd12', '3')
{'name': '3'}

分析一下上面的代码,正则表达式h(\w+).*(?P<name>\d)$中有2个小括号,表示它分了2个小组,在匹配的时候是拿整体的表达式去匹配的,而不是拿小组去匹配的。(\w+)表示这个小组内是1到多个字母数字字符,(?P<name>\d)?P<name>是个正则表达式的特殊语法,表示给这个小组取了个叫name的名字,?P<xxxx>是固定写法。在获取分组值的时候,group()group(0)是对等的,都表示整个匹配到的字符串,从group(1)开始,分别是从左往右的小组序号,按位置顺序来。

有时候括号会存在嵌套情况,那怎么确定组的顺序1,2,3?要么用取名字的方法,要么就数左括号,第几个左括号就是第几个分组,例如(1(2,(3)),(4)),0表示表达式本身,不参加数左括号的动作。

例三,search()方法,有分组的情况:

import re

origin = "sdfi1ha23123safd123"      # 注意这里对匹配对象做了下调整
# 有分组
r = re.search("h(\w+).*(?P<name>\d)$", origin)
print(r.group())
print(r.group(0))
print(r.group(1))
print(r.group(2))
print(r.groups())
print(r.groupdict())

# 执行结果:
ha23123safd123
ha23123safd123
a23123safd12
3
('a23123safd12', '3')
{'name': '3'}

表现得和match()方法基本一样。

例四,findall()方法,没有分组的情况:

import re

origin = "has something have do"
# 无分组
r = re.findall("h\w+", origin)
print(r)

# 执行结果:
['has', 'hing', 'have']
# 一切看起来没什么不一样

注意到了没有?这里根本没有调用group相关的方法,因为findall()的返回值是个列表,根本就没有group()、groups()、groupdict()的概念!

例五,findall()方法,有一个分组的情况:

import re

origin = "has something have do"
# 一个分组
r = re.findall("h(\w+)", origin)
print(r)

# 执行结果:
['as', 'ing', 'ave']

相比较前面未分组的例子,有没有发现什么?那就是没有圈在分组内的内容被抛弃了,比如这里的字符'h'。

例六,findall()方法,有两个以上分组的情况:

import re

origin = "hasabcd something haveabcd do"    # 字符串调整了一下
# 两个分组
r = re.findall("h(\w+)a(bc)d", origin)
print(r)

# 运行结果:
[('as', 'bc'), ('ave', 'bc')]

返回的是元组组成的列表!

例七,sub()方法,有分组的情况:

import re

origin = "hasabcd something haveabcd do"
# 有分组
r = re.sub("h(\w+)", "haha",origin)
print(r)

# 运行结果:

haha somethaha haha do

看到没有?sub()没有分组的概念!这是因为sub()方法是用正则表达式整体去匹配,然后又整体的去替换,分不分组对它没有意义。这里一定要注意了!

例八,split()方法,有一个分组的情况:

origin = "has abcd something abcd do"
# 有一个分组
r = re.split("(abcd)", origin)
print(r)

# 运行结果:
['has ', 'abcd', ' something ', 'abcd', ' do']

例九,split()方法,有两个分组,并且嵌套:

import re

origin = "has abcd something abcd do"
# 有两个分组
r = re.split("(a(bc)d)", origin)
print(r)

# 运行结果:
['has ', 'abcd', 'bc', ' something ', 'abcd', 'bc', ' do']

例十,split()方法,有多个分组,并且嵌套:

import re

origin = "has abcd something abcd do"
# 有一个分组
r = re.split("(a(b)c(d))", origin)
print(r)

# 运行结果:
['has ', 'abcd', 'b', 'd', ' something ', 'abcd', 'b', 'd', ' do']
02-13 16:03