正则表达式 For Python

Manarola

正则表达式有很多流派,也有很多的特性,不同的语言支持度也是不一样的。本篇文章是写Python中的正则表达式的用法的,介绍了一些可用特性,也指出了某些特性是不支持的。

本篇文章仅为学习笔记,不对正则表达式做深入研究,只想用最短的时间入门正则。

实践是检验真理的唯一标准。你以为的并不是你以为的,如有问题还是要写一个例子用多个工具验证下最好。

字符组 [ ]

在同一位置可能出现的各种字符,也可以称之为字符集。

写法有很多种。有原始写法,然后是范围表示法,然后是排除型字符组,然后是字符组简记法,最后还有一个字符组运算(Python不支持)。

[3645970128]  -->  [0123456789]  -->  [0-9]  -->  \d
[0-9A-Za-z_]  -->  [\dA-Za-z_]  -->  \w
"-"用来表示范围 范围的确定依靠码值来确定,一般据ASCⅡ表值。

排除型字符组由脱字符"^"来指定,"^"排在第一位,紧靠在"["之后,需要排除的按上面写法写在"^"之后。
[^0-9]    # 排除数字,也可以写成[^\d]
如果需要排除"-",则必须将"-"写在开头"^"之后,如果一定要写在后面,则必须用"\"转义。
字符组简记 说明
\d 数字(digit) [0-9]
\w 单词(word) [0-9A-Za-z_] 字母数字下划线
\s 空白(space) [ \t\r\n\f\v] 空格、制表符、回车、换行等显示空白字符

\D 、\W 、\S是与之对应的排除型字符组简记。

以上说的\d 、\w 、\s的匹配规则都是针对ASCⅡ编码规则而言的,也叫ASCⅡ匹配规则。在Unicode编码中,全角数字0、1、2之类的也算数字,也可以由\d匹配;中文字符也可以算是单词字符,由\w匹配;同样全角空格也由\s匹配。

Python3默认采用Unicode匹配规则,但也可以显示指定采用ASCⅡ匹配规则(表达式最开始用(?a)指定ASCⅡ模式)。

另外还有一个POSIX字符组(POSIX Character Class)。POSIX(Portable Operating System Interface for uniX),它是含有正则表达式规范的一系列规范。这种字符组只在java、php、ruby中支持,经过测试以及查阅资料并没有显示Python是支持的,不过也有可能是POSIX字符组需要变化一下才能使用,就像其他语言那样。

量词

以上说的都是匹配单个字符,如果多了怎么办。所以就出现了量词。

量词限定之前的元素的出现,这个元素可能是一个字符,也可能是一个字符组,还可以是一个表达式。

一般形式量词 说明
{n} 之前的元素必须出现n次
{m,n} 之前的元素最少出现m次,最多出现n次
{m,} 之前的元素最少出现m次,最高没有限制
{0,n} 之前的元素可以不出现,也可以出现,最多出现n次
常用量词 等价形式 说明
* {0,} 可能出现,也可能不出现,出现次数没有上限
+ {1,} 至少出现1次,最多没有限制
? {0,1} 可以不出现,也可以就出现1次

所有的量词默认都是贪婪量词,能匹配的,绝不停止匹配。它会优先尝试匹配,并记下这个状态,以便将来反悔。这个返回的过程称之为回溯。

量词也可以变成懒惰的,称之为懒惰量词,能不匹配就不匹配,如果之后的表达式匹配失败,则会回溯进行匹配。变成懒惰量词是在现有量词后加上?

还有一种量词称之为占有量词。它和贪婪量词很像,但是占有量词不会进行回溯,优点是速度快,但是失败也会很快。变成占有量词是在现有量词后加上+

分组 ( )

分组,顾名思义就是将某些东西分成一组,对外来看是一个整体。通过()将表达式括起来,此时表达式就是一个整体一个可整体操作的元素。括号内的表达式称之为“子表达式”。

多选结构

多选结构的形式是(···|···),在括号中以竖线|分隔开多个子表达式,这些子表达式也叫多选分支;在一个多选结构中,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素,只要某个子表达式能够匹配,整个多选结构的匹配就成功。

一般而言ab|cd依旧是多选结构,等价于(ab|cd),建议使用后者,更加明确,也能够避免一些错误。例如^ab|cd$其实是(^ab|cd$)而不是^(ab|cd)$,因为竖线|的优先级很低。

多选结构的匹配顺序问题。比如表达式(aaa|aaabbb)去匹配aaabbb,匹配结果到底是aaa还是aaabbb呢?Python中多选结构都会优先选择最左侧的分支,所以匹配出来是aaa。不过在实际使用中请避免这种情况,分支中存在重复会大大增加回溯的计算量。

引用分组

分组之后,正则表达式会保存每一个分组真正匹配的文本(不是正则表达式可匹配的文本),匹配完成后,可通过编号进行引用当时捕获的内容。

如果出现多个嵌套括号,那编号的计数规则是怎样的呢?首先整个正则表达式默认存在一个的编号为0的分组,其他分组的编号是依据从左到右开括号出现的顺序决定的,第几个括号就是第几组。

  1. 反向引用

    在正则表达式内部引用之前的捕获分组匹配的文本,之前捕获分组中锚点表示的位置信息不会被保留下来。

    引用形式:\num,其中num表示所引用分组的编号。

  2. sub替换中引用

    re.sub(pattern,replacement,string),在replacement中使用引用分组。引用形式同样是:\num

  3. 匹配结果中group引用

    对匹配完成后,可对匹配结果对象调用group(num)获取分组中匹配的数据。

\num引用的二义性问题:\10是第10个引用还是第1个引用后再加一个0呢?

Python中将其解释为第10个引用,可通过\g<num>,消除二义性。

注:反向引用只引用之前的捕获分组匹配的文本,之前捕获分组中的锚点表示的位置信息不会被保留下来。

命名分组

鉴于数字编号不够直观,并且括号多了难免混淆,所以就有了命名分组的出现。Python中用(?P<name>...)来分组,其中name是分组的名称。

反向引用必须使用(?P=name)来引用;替换则需要写作\g<name>;匹配结果中则使用group(name)

非捕获分组

无论是否需要分组,只要出现了括号,正则表达式再匹配的时候就会把括号内的子表达式存储起来,提供引用。如果并不需要引用,保存这些信息无疑会影响正则表达式的性能。

正则表达式提供了非捕获分组(?:...),这样的括号叫做非捕获型括号,他只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按照开括号出现的顺序从左到右计数,非捕获分组会略过。

原子分组

另一种非捕获分组就是原子分组(?>...)。这种分组可以将回溯操作关闭,但它只针对原子分组内的部分,而不针对整个正则表达式。经过测试发现这个表达式?>是不识别的。但是一篇文章英文原版)给出了一个等价方案:

可以通过使用零宽度先行assert((?= RE))来模拟它,它从当前点匹配相同的语义想要,将一个命名的组((?P RE))放在lookahead中,然后使用一个命名的反引用((?P = name)恰恰是零宽度断言匹配.组合在一起,这给了你相同的语义,代价是创建一个额外的匹配组和很多语法.

匹配模式

所谓匹配模式,指的是匹配时使用的规则。设定特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有4种:不区分大小写模式单行模式多行模式注释模式。通常有两种指定方式:以模式修饰符指定(?modifier),或者以预定义的常量作为特殊参数传入来指定。

Python中模式修饰符只要出现,无论在什么位置,都对整个正则表达式生效。Python中不支持失效修饰符(?-modifier)

不区分大小写模式

不区分大小写模式的修饰符是i (case Insensitive),则(?i)it可以匹配的有it It iT IT

单行模式

修饰符是s (Single line)。

这种模式下所有的文本只在一行里,换行符只是其中一个普通字符。单行模式影响的是点号.的匹配规则。

多行模式

修饰符是m (Multiline)。

多行模式影响的是^$的匹配规则。

注释模式

修饰符是x (eXtended mode 扩展模式)。

有时正则表达式可能非常复杂,不但难于编写和阅读,也难以维护。python中支持使用(?#comment)的记法添加注释;还有一种是使用注释模式,此时正则表达式对应的字符串可以跨越多行,注释则以#comment的形式添加在正则表达式内部,每一条注释从#开始,到行末结束,同时还可以使用缩进表示层级结构更加方便阅读和维护。

断言

常见的断言有三类:单词边界行起始/结束位置环视。断言并不真正的匹配文本,他匹配的是位置,他只负责判断在某个位置左/右侧的文本是否符合要求。

单词边界

单词边界记为\b,他匹配的是“单词边界”位置,而不是字符。也就是说\b能够匹配这样的位置:一边是单词字符,另一边不是单词字符(包括没有字符)。不同的模式下\w不同,所以\b也就不同。

另外还有\B非单词边界。

行起始/结束为位置

^匹配字符串开始的位置。

$匹配字符串结束的位置。

前面已经说了:多行模式影响的是^$的匹配规则。

在不指定多行模式的情况下:^匹配的是整个字符串的开始位置,$匹配的是整个字符串的结束位置。

当在多行模式下:^匹配的是字符串的起始位置以及换行之后行终止符之后的位置,如果字符串末尾出现了行终止符,则依旧匹配行终止符之后的那个位置;$匹配的是字符串的结束位置,但是这个位置是在字符结束之后行终止符之前的位置。

python中还有一个特殊标记\Z,它和$类似,但是他不受多行模式的影响,\Z不管行终止符,他匹配的是整个字符串的结束位置,在行终止符之后的位置。

^$的另一个特点是在进行正则表达式替换的时候并不会被替换。也就是说,在起始和结束位置进行替换,只会在起始和结束位置添加一些字符,位置本身依然存在。

环视

环视是对匹配字符周围字符的验证,他匹配的并不是字符,而是位置。

环视大致分为以下四种:

名称 又称 说明
肯定顺序环视 正前瞻 ...(?=...)
否定顺序环视 反前瞻 ...(?!...)
肯定逆序环视 正后顾 (?<=...)...
否定逆序环视 反后顾 (?<!...)...

肯定环视和否定环视的区别就是:肯定环视要想判断成功,字符串中必须有字符由环视结构中的表达式匹配;而否定环视要判断成功却有两种情况1.字符串的字符不能匹配2.根本就没有字符,这个位置可能是起始或结束位置。

环视的组合:

  1. 包含

    环视中包含环视,这个和环视的并列还不太一样,这个外层环视是限定的位置,但是这个内层环视是限定外层环视的。(?=...(?!...))

  2. 多环视并列

    环视的并列要求每一个环视对当前位置的匹配都必须成功,而各个环视之间是没有任何联系的,他们都将作用于该位置。

  3. 环视作为多选分支

    将若干个环视作为多选分支排列在多选结构中,只要有一个环视分支成立,整个多选就成立。

分组的编号只与捕获型括号相关,而不受其他任何括号类型的影响,所以环视结构的括号并不影响分组。但是环视结构中出现了捕获型括号,则会影响分组。

环视结构中的捕获型括号一旦匹配完成,就不能回溯。e.g.(?<=(\d+))\w+\1无法在123a12中找到匹配。

Python支持顺序环视,但对于逆序环视,Python只支持匹配固定长度文本表达式。(?<=dogs?)(?<=(dog|cats))都是不合法的,遇到这样的可与选择多选结构来改造,但是如果出现*和+之类的量词,就不能用多选了。

至此,整个正则表达式部分就介绍完毕,以下部分Python正则API。


Python中的正则表达式模块是re模块,通过import re 导入re模块来使用正则表达式。

在正则表达式中,大多数的函数是类似re.search(pattern,string)的形式,其中pattern通常是正则表达式的字符串,这可以视为“静态方法”。其实也可以将RegexObject对象作为pattern参数传入,或者直接对RegexObject对象调用相同名字的成员方法,结果相同,但是后面这两个方法可以显示的缓存RegexObject对象。

除了正则表达式对象RegexObject,还有一个MatchObject对象,MatchObject对象提供了一些方法和属性,通过他们可以获取匹配的信息。

方法 描述 注释
start(n) 返回编号为n的捕获分组匹配的开始位置,如果对应分组不存在或者未匹配,则返回-1 如果没有设定n,则返回整个表达式匹配的开始位置,此时n等于0
pos 返回整个表达式匹配的开始位置
end(n) 返回编号为n的捕获分组匹配的结束位置,如果对应分组不存在或未匹配,则返回-1 如果没有设定n,则返回整个表达式匹配的结束位置,此时n等价于0
endpos 返回整个表达式匹配的结束位置 等价于end(0)
group(n) 返回编号为n的捕获分组匹配的文本 如果没有设定n,则返回整个表达式匹配的文本,此时n等价于0
groups() 返回各捕获分组匹配的文本构成的tuple 如果表达式中不存在捕获分组,则返回空tuple
span(n) 返回编号为n的捕获分组匹配的开始-结束位置 如果没有设定n,则返回整个表达式匹配的开始-结束位置,此时n等价于0
re 返回匹配时所使用的正则表达式对象
lastindex 返回匹配成功的编号最大的捕获分组的编号 如果没有,则返回None
string 返回用来尝试用正则表达式来匹配的字符串
expand(str) 返回一个字符串,可以在其中引用捕获分组匹配的文本
  • re.compile(regex [, flags])

    这个方法用于显示“编译”正则表达式对象。如果需要多次使用同一个正则表达式,那么每次使用编译好的正则表达式对象比每次临时编译表达式对象要快得多。另外compile()还有一个功能,那就是添加第二个参数re.DEBUG来观察整个正则表达式的详细信息。

  • re.search(pattern, string [, flags])

    这个方法用来测试正则表达式pattern能否在string中找到匹配,flags是可选参数是匹配模式(aiLmsux)的按位OR结果。如果能找到匹配,则返回MatchObject,否则返回None。

  • re.match(pattern, string [, flags])

    re.match()和re.search()非常相似,参数相同,返回值也行通,非常容易混淆,唯一的区别在于:如果re.match()匹配成功,那么pattern匹配的字符串必须开始于string的最左端。也就是说,re.match()只会在字符串的开始的位置尝试匹配,re.search()则没有这个限制。MULTILINE 多行模式下,match也只匹配string的开头部分。

  • re.findall(pattern, string [, flags])

    这个函数会一次性的找出正则表达式pattern在string中的所有匹配,返回一个list。

    如果pattern中存在捕获分组,则返回list的元素并非整个表达式所匹配的文本,而是各个捕获分组所匹配文本构成的tuple。返回的tuple中是没有默认的分组编号0所匹配的文本元素的,如果需要返回可以在整个正则表达式外再加一个括号。tuple中文本的顺序就是分组编号的顺序。

  • re.finditer(pattern, string [, flags])

    如果文本内容很多,使用re.findall()一次性找到所有匹配的成本可能很高,使用re.finditer()可以返回一个迭代器(iterator),他可以按照从左到右的顺序逐个遍历每次匹配的MatchObject对象,依次进行处理或检验。

  • re.split(pattern, string [, maxsplit=0, flags=0])

    这个函数用正则表达式pattern能够匹配的文本切分字符串string。如果正则表达式在开头或者结尾位置找到匹配,那么结果的开头或结尾会出现空字符串。也可以明确设定maxsplit。在Python中这个参数表示切分的次数。也就是说,返回数组一般会包含maxsplit+1个元素。如果maxsplit设定负数,则表示不做任何切分;若设定0,则等于没有设定;若设定的值为正数,且小于实际可切分的次数,则按设定的值进行切分,若大于实际可切分的次数,则以实际可切分的次数为准。

  • re.sub(patern ,repl, string [, count=0, flags=0])

    这个函数用来进行正则表达式替换,它将字符串string中的正则表达式pattern能够匹配的文本替换为repl指定的文本。如果要在replacement字符串中引用之前的某个捕获分组匹配的文本,则应该使用\num。Python中也提供了跟清晰的方式在repl中引用分组,写作\g<num>,这种记法也可以使用命名分组\g<name>。repl也可以是一个函数,他接受一个MatchObject对象,返回一个String对象。也可以设定count参数,他指定替换操作最多能发生的次数。

  • re.subn(patern ,repl, string [, count=0, flags=0])

    与sub()相同,但返回一个元祖,其中包含新字符串和替换次数。

  • re.escape(string)

    返回一个字符串,其中所有的非字母数字字符都带有反斜杠。

推荐阅读更多精彩内容