学习Python正则表达式

介绍Python中的正则表达式 Modified: 2023-07-04 20:24:23 Created: 2021-08-22 09:50:08 Tags: #Python # Regex

正则表达式(regular expression),用于字符串匹配、替换,是字符串的重要处理方式。其中正则是有规则,有规律的意思。正则表达式有三要素,本文主要介绍 Python 中的这三要素:

  • 要处理的字符(串),需要注意转义序列
  • 用于处理的程序,即正则引擎:Python 自带re模块
  • 处理规则:正则表达式语法

1 Python 字符串

1.1 转义序列

Python 字符串可以包含两部分:

  • 普通字符组成的字符串
  • 转义序列组成的字符串

转义序列(Escape Squence)转义字符(Escape Character)和后续字符组成,转义字符放在字符序列前面时,它将对它后续的几个字符进行替代并解释。转义字符是元字符(Meta Character)的一种特殊情况。Python 中转义字符是反斜杠(\)。Python 中所有转义序列如下表:

转义序列 意义
\ (行尾) 续行符
\a alert
\b backspace
\n new line
\r carriage return
\t table
\v vertical table
\' single qoute
\" double qoute
\\ back slash
\xyy hex
\0zz oct, start with number 0
\000 null

通常也把转移序列成为转义字符。转义字符拥有两个含义:

  • 转义序列标志,如 Python 中的反斜杠
  • 转义序列,由转义序列标志符和后续字符组成,整体的有特殊意义

后面的描述中,转义字符特指转义序列。

处理引号也可以用转义字符:Python 并不明确区分单双引号,只要要匹配使用就行;如要表示一个字面意义上的引号,需用另外一种引号来嵌套,或者转义字符:

"'"  # use another kind of qoute
"\""  # use escape character

输入这些符号时需要多次敲键盘,但 Python 都把它们当成长度为1的字符来处理:

In [1]: len("\017")
Out[1]: 1

In [2]: len("\n")
Out[2]: 1

In [3]: len("\\")
Out[3]: 1

1.2 Python 中转义序列标志符

反斜杠是 Python 中转义序列标志符,它有如下特性需要注意:

  1. 表示一个字面意义的\需要转义序列标志符\
  2. Python 中显示一个字面意义上\则是\\
In [1]: len("")
Out[1]: 0

In [2]: len("\")
  File "<ipython-input-2-553b6950c0a1>", line 1
    len("\")
            ^
SyntaxError: EOL while scanning string literal

In [3]: len("\\")
Out[3]: 1

In [4]: "\\"[0]
Out[4]: "\\"

转义序列标志只有在特定的字符组合前才有效果,如果后面的字符组合不能组合为转义,则只会被当成简单的字符。 比如\n是特殊含义,而\m没有,\m只会被看成是一个字面上的\m组成的长度为 2 的字符串。

1.3 原始字符串

在某些特定情况下希望字符是所见即所得。一个\n组合在一起就是表示两个字符,而不是换行符。此时可以用 Python 的原始字符串(Raw String),在要处理的字符前面加上r

In [1]: len(r"\n")
Out[1]: 2

In [2]: len(r"\")
  File "<ipython-input-6-aa225c032aa6>", line 1
    len(r"\")
             ^
SyntaxError: EOL while scanning string literal

In [3]: len(r"\\")
Out[3]: 2

原始字符串并非万能:单独的反斜杠\没法用原始字符串表示,2 中把最后的引号给转义掉,3 是一个原始字符串,但表示两个反斜杠。无法用原始字符串表是转义序列

2 Python 正则引擎

用于匹配的程序叫正则表达式引擎,Python 自带re的模块。当然还有些第三方库正则引擎,但并不是主要介绍的对象。需要提前导入re模块:

In [1]: import re

同时,关于匹配的详细细节也不是本文介绍的重点。本文主要介绍正则引擎常用函数的使用,常见函数有两种用法:

re.func(REG, string, flag)  # call the func with re directly
re.compile(REG, flag).func(string)  # call the compiled object

通常认为先 compile(compilere模块的函数),后运算可以提升效率。但实际上并不特别关注效率问题:

  1. 某些高效的正则表达式可能难以理解
  2. Python 本身运行就想对慢一些
  3. 本文主要介绍使用,并不过分关注原理

函数参数意义如下:

  1. REG(regular expression)就是正则表达式,第三部分会详细介绍。
  2. string 是要匹配的字符串,Python 字符串。
  3. flag 是正则运算时候的参数,会在介绍具体函数时候介绍。

常见的func是:

  • match 匹配字符串的开始,返回一个re.Match(匹配上)或None(没匹配上)
  • search 找到一个匹配就返回,即使有多个也只返回第一个,返回一个re.Match(匹配上)或者None(没匹配上)
  • findall 找到所有匹配,返回一个列表(第四部分详细介绍这个列表)
  • finditer 找到所有匹配,返回一个迭代器
  • split 对字符串按照一定规则切分,返回一个列表
  • sub 对字符串进行替换,返回替换后的字符串
  • subn 对字符串进行替换,返回 tuple,前面是替换后的字符,后面是总共替换次数

所谓re.Match,有如下方法:

re.match.span()  # span, tuple
re.match.start()  # start index
re.match.end()  # end index
re.match.group()  # match group
re.match.group_dict()  # match group

其中group表示匹配的组, 会在后面捕获组一节中详细介绍。这里可以通过一个例子就re.Match进行简单说明。由于使用例子要用到正则表达式,这里先给出正则表达式的一条规则:大部分字符都匹配自身

In [1]:x = re.search("abc", 'zyxabcdre')

In [2]: x
Out[2]: <re.Match object; span=(3, 6), match='abc'>

In [3]: x.start()
Out[3]: 3

In [4]: x.end()
Out[4]: 6

In [5]: x.span()
Out[5]: (3, 6)

In [6]: x.group()
Out[6]: 'abc'

In [7]: x.group(0)
Out[7]: 'abc'

In [8]: x.groupdict()
Out[8]: {}

In [9]: re.match("abc", 'zyxabcdre')

match匹配的是开始,如果开始没有匹配到,就返回None;而search可以匹配字符串的中间部分,匹配到一个就返回。

flag可以给匹配添加更多选项,常见的flag如下:

flag 作用
re.IGNORECASE 忽略大小写
re.DOTALL 点可以匹配换行符
re.MULTILINE 多行模式

3 正则表达式语法

这也是通常说的正则表达式,正则表达式用 Python 字符串实现,它们属于 Python 字符串,正则引擎处理正则表达式时候有额外的语法

从第二部分知道,大部分字符都匹配它们自己,少数字符可以匹配其它字符串,它们需要被重点关注:

.
[]
^
$
-
*
+
?
{}
\
()
|

3.1 匹配单个字符

3.1.1 大部分字符匹配自己

匹配大小写敏感,可添加re.IGNORECASE通配大小写

In [1]: re.search('a', 'ABCD')

In [2]: re.search('a', 'ABCD', re.IGNORECASE)
Out[2]: <re.Match object; span=(0, 1),  match='A'>

转义序列也包含其中,但不包含反斜杠

In [1]: re.search('\n', 'ab\ncd')
Out[1]: <re.Match object; span=(2, 3),  match='\n'>

对反斜杠的特殊处理

确实存在匹配单个字面意义上的反斜杠的情况,如匹配如下$LaTeX$:

\section{back slash}

若按普通转义序列一样处理反斜杠,即用\\作为正则表达式,则会报错:

In [1]: re.search("\\", "\\x")
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-2-ace9affb99aa> in <module>
----> 1 re.search("\\", "\\x")

/usr/local/lib/python3.10/re.py in search(pattern, string, flags)
    199     """Scan through string looking for a match to the pattern, returning
    200     a Match object, or None if no match was found."""
--> 201     return _compile(pattern, flags).search(string)
    202
    203 def sub(pattern, repl, string, count=0, flags=0):

/usr/local/lib/python3.10/re.py in _compile(pattern, flags)
    302     if not sre_compile.isstring(pattern):
    303         raise TypeError("first argument must be string or compiled pattern")
--> 304     p = sre_compile.compile(pattern, flags)
    305     if not (flags & DEBUG):
    306         if len(_cache) >= _MAXCACHE:

/usr/local/lib/python3.10/sre_compile.py in compile(p, flags)
    762     if isstring(p):
    763         pattern = p
--> 764         p = sre_parse.parse(p, flags)
    765     else:
    766         pattern = None

/usr/local/lib/python3.10/sre_parse.py in parse(str, flags, state)
    940     # parse 're' pattern into list of (opcode, argument) tuples
    941
--> 942     source = Tokenizer(str)
    943
    944     if state is None:

/usr/local/lib/python3.10/sre_parse.py in __init__(self, string)
    230         self.index = 0
    231         self.next = None
--> 232         self.__next()
    233     def __next(self):
    234         index = self.index

/usr/local/lib/python3.10/sre_parse.py in __next(self)
    243                 char += self.decoded_string[index]
    244             except IndexError:
--> 245                 raise error("bad escape (end of pattern)",
    246                             self.string, len(self.string) - 1) from None
    247         self.index = index + 1

error: bad escape (end of pattern) at position 0

原因如下:正则引擎首先对正则表达式处理,把\\(两个连续反斜杠)翻译为\(一个反斜杠),此时正则表达式变成\"(一个反斜杠结合后面的引号),正则表达式缺少字符串结束标志符--引号",报错。(正则引擎对正则表达式还会有其它处理,后面会介绍。)正确姿势是用四个反斜杠

In [1]: re.search("\\\\", "\\subgraph")
Out[1]: <re.Match object; span=(0, 1), match='\\'>

好家伙!为匹配一个字面意义上的反斜杠,需要在正则表达式中用四个连续反斜杠。这也是所谓的反斜杠灾难。为了避免复杂的写法,正则表达式中也有原始字符串(Raw String),作用是:正则引擎处理正则表达式时候,不把连续两个反斜杠\\翻译为一个反斜杠\

In [1]: re.search(r"\\", "\\x")
Out[1]: <re.Match object; span=(0, 1), match='\\'>

为了让正则表达式和 Python 字符串更加接近,一条推荐的规则是:不管多么简单的规则,都采用原始字符串的方式进行匹配。

3.1.2 点.匹配所有字符

.匹配所有字符,这个和通配符中星号*表示所有字符一样。通常点并不能匹配换行符,加入re.DOTALL就可以了。

需要注意的是这里说的是单个字符。

3.1.3 []表示集合

中括号([])表示范围,匹配中括号中任意一个元素。中间是或的关系,元素之间不需要隔开。

In [1]: re.search(r'[1234]','843')
Out[1]: <re.Match object; span=(1, 2), match='4'>

中括号中的元素可以重复,但是并没有特殊意义:

In [1]: re.search(r'[ddxs]','d')
Out[1]: <re.Match object; span=(0, 1), match='d'>

中括号中大部分字符(包含转移序列)都只表示原本意思,除了特殊字符:点(.)可以匹配任意字符,但在中括号([])中却只匹配字面上的点(.):

In [1]: re.search(r'.', ',')
Out[1]: <re.Match object; span=(0, 1), match=','>

In [2]: re.search(r'[.]', ',')

In [3]: re.search(r'[\n]', 'x\n')
Out[3]: <re.Match object; span=(1, 2), match='\n'>

对某些需要转义表示的字符,也可以放到中括号中来避免使用转义符号,比如反斜杠:

In [1]: re.search(r"[\\]", '\\x')
Out[1]: <re.Match object; span=(0, 1), match='\\'>

需要特殊关注的字符^-[,规则如下:

  1. ^符号表示取反,但是^一定要添加在最开始
In [1]: re.search(r'[^1234]','1843')
Out[1]: <re.Match object; span=(1, 2), match='8'>

如果^在中间,则只会被当成是一个普通字符

In [1]: re.search(r'[7^1234]','1843')
Out[1]: <re.Match object; span=(2, 3), match='1'>
  1. 中括号中使用-表示范围,实现对表达式的精简
r"[0-9]" # equal  r"[0123456789]" in regular expression
r"[a-z]" # equal r"[abcdefghijklmnopqrstuvwxyz]"in regular expression

需注意若-前后不是范围,则只会匹配普通的-符号:

In [1]: re.search("[-a]", "b-")
Out[1]: <re.Match object; span=(1, 2), match='-'>
  1. 中括号开始标志[ 如果非要表示字面意义上左中括号([),则要使用\[
In [1]: re.search(r"[]]", ']')
Out[1]: <re.Match object; span=(0, 1), match=']'>

In [2]: re.search(r"[[]", ']')
<ipython-input-44-863737df3ef5>:1: FutureWarning: Possible nested set at position 1
  re.search(r"[[]", ']')

当要处理(字面意义)中括号时候,最好通通用转义序列

3.1.4 正则表达式转义序列

它们虽然是反斜杠(\)和某个字符的组合,但不是Python 转义序列:只有正则引擎会对它们特殊处理,而在其它情况下只会被看成是普通字符串。它们也可看成对中括号([])的扩展。如下正则表达式含义比较清楚:

r1 = "[0-9]"
r2 = "[a-zA-Z_]"

r1表示一位十进制数,r2表示任意字母数字或下划线。可以用正则表达式转义序列进一步简写:

r1 = "\d"
r2 = "\w"

更多的正则表达式转义序列如下:

简写方式 意义
\d digital,一位十进制数字符
\D 一位非十进制数字符
\w word,大小写都是可以的
\W 非字母
\s space,空格
\S 非空格
  • 一个规律是大写为小写取反

正则表达式中的转义序列并不会被 Python 字符特殊对待。此时反斜杠只会被看成字面意义上的反斜杠。

In [1]: len("\d")
Out[1]: 2

前面介绍正则表达式原始字符串时候提到,正则引擎会对首先正则表达式分析,所谓的正则表达式转义字符就是被正则引擎进行处理。

3.2 字符串匹配

实际中更有用的是字符串的匹配。如同字符串对字符的扩展方式,正则表达式并列排布,则匹配并列排布的字符和字符串:

  • 若正则表达式regAregB分别匹配字符chaAchaB,则正则表达式regAregB匹配字符串chaAchaB
  • 若正则表达式regAregB分别匹配字符串strAstrB,则正则表达式regAregB匹配字符串strAstrB
reg1 = r"\d"
reg2 = r"\d\d"
reg3 = r"\d\w"
re.search(reg1, "56")  # 5
re.search(reg2, "56")  # 55
re.search(reg3, "5x")  # 5x

3.2.1 小括号界定正则表达式范围

并列排布的运算并不都是从左往右计算,而是依据运算的优先级从高到低进行运算。为了说明优先级会影响匹配的结果,这里引入一个优先级极低的运算符号|,它表示或的关系。

In [1]: reg1, reg2 = r'a', r'b|c'

In [2]: re.match(r'ab|c', 'ac')

In [3]: re.match(r'ab|c', 'c')
Out[3]: <re.Match object; span=(0, 1), match='c'>

In [4]: re.match(r'ab|c', 'ab')
Out[4]: <re.Match object; span=(0, 2), match='ab'>

直接把reg1reg2组合到一起,并不能匹配ac,只能匹配ab或者c了。实际上匹配的优先级被修改。字符组合优先级高于或(|)运算。此时如果要实现匹配ac,可以添加小括号(实现:

In [1]: re.match(r'a(b|c)', 'ac')
Out[1]: <re.Match object; span=(0, 2), match='ac'>

小括号界定正则表达式的范围,改变了匹配优先级。添加非字面意思的小括号并不是匹配小括号的意思。

In [1]: re.search(r'x|y|z', "z")
Out[1]: <re.Match object; span=(0, 1), match='z'>

In [2]: re.search(r'(x)|(y)|(z)', "z")
Out[2]: <re.Match object; span=(0, 1), match='z'>

同时,小括号可以把一个正则表达式界定为一个组(group)。后续可以对这个组进行操作。下一节中将对组进行说明。

3.2.2 重复匹配

根据前面的规则,下面的正则表达式用于匹配多位十进制数:

\d
\d\d
\d\d\d
\d\d\d\d

如果要匹配位数更多的数,则需要加长正则表达式。显然这样并不方便,于是就有了如下简写:

(REG){m,n} # 至少m次,至多n次
(REG){,n} # 至少0次,至多n次
(REG){m,} # 至少m次
(REG){m} # 只能是m次

特殊情况下可继续简写:

(REG){0,1} -> (REG)?
(REG){1,} -> (REG)+
(REG){0,} -> (REG)*

上面检索都是贪婪的:会尽可能长地匹配。在正则表达式后面加上?实现非贪婪搜索:

In [1]: re.search(r'm{2,3}', "mmm")
Out[1]: <re.Match object; span=(0, 3), match='mmm'>

In [2]: re.search(r'm{2,3}?', "mmm")
Out[2]: <re.Match object; span=(0, 2), match='mm'>

除了对单个字符这样操作,也可以把用括号,把多个正则表达式作为一组进行操作:

In [1]: re.search(r'(ab)+', 'abababxxx')
Out[1]: <re.Match object; span=(0, 6), match='ababab'>

In [2]: re.search(r'ab+', 'abababxxx')
Out[2]: <re.Match object; span=(0, 2), match='ab'>

1 把ab看成一个整体,进行重复匹配;2 是匹配a和多个b。这里括号让一系列正则表达式组成一个组,然后对整个组进行操作。

3.3 捕获组、非捕获组与命名组

3.3.1 小括号与捕获组

总结一下小括号作用:

  1. 界定正则表达式范围
  2. 改变匹配的优先级
  3. 把一系列正则表达式匹配的结果对应到组(group)中

REG是一个合法的正则表达式:

(REG)

此时REG匹配到的结果就会存到一个 group 中,可以通过group函数进行访问。

In [1]: x = re.search(r'((\d)\w(\d))', "a1b3x")

In [2]: x.group(0)
Out[2]: '1b3'

In [3]: x.group(1)
Out[3]: '1b3'

In [4]: x.group(2)
Out[5]: '1'

In [5]: x.group(3)
Out[5]: '3'

In [6]: x.group(4)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-29-b8b09168b4e1> in <module>
----> 1 x.group(4)

IndexError: no such group

In [7]: x.group(1, 3)
Out[7]: ('1b3', '3')

以正则表达式最左边为1,从左往右开始计数,(非转义)左括号(()排列的顺序就是 group 函数访问时候的参数,此时返回当前左括号和对应右括号中的正则表达式匹配的结果。group()group(0)等价,都表示整个正则表达是匹配的结果。

In [1]: x = re.search(r'\d', '5')

In [2]: x.group(0)
Out[2]: '5'

In [3]: x.group()
Out[3]: '5'

这种用小括号包围正则表达式得到的组就是捕获组(Capture Group)。捕获组的结果会存储到内存中,可以通过 group 的 index 进行访问。

3.3.2 非捕获组

对于组而言,存储匹配结果并且访问并非一定必要:有时仅仅想知道有这么个匹配(并不想知道具体匹配结果),或者说存储结果这个过程占用空间、影响速度而变得不可以。此时就引入非捕获组(Non Capture Group),只表示匹配关系,不存储具体结果

(?:REG)

非捕获组也使用了小括号()来界定范围,单非捕获组正则表达式前面会有问号(?)和冒号(:)。

问号(?)来自 Perl(大部分语言的正则表达式实现都受 Perl 影响)。Perl 从 4 升级到 5 时,为了保持兼容性引入了(?的写法:对一个非转义的左括号(()用问号(?)进行重复匹配没有意义,换言之(?并不会引起歧义; 从另一个角度来看,不会有一个合法的正则表达式以问号(?)开头。

冒号(:)表示普通非捕获组,表示匹配当前位置的字符(串)。当然,还有其它非捕获组,如后向断言(lookbehind)和前向断言(lookahead)。

非捕获组不会计入捕获组的 index 中,无论是否同时存在捕获组:

In [1]:  x = re.search(r'((?:\d)\w(\d))', "a1b3x")

In [2]: x.group(0,1,2)
Out[2]: ('1b3', '1b3', '3')

In [3]: x.group(3)
---------------------------------------------------------------------------
IndexError                                Traceback (most recent call last)
<ipython-input-9-6cd92edb32bb> in <module>
----> 1 x.group(3)

IndexError: no such group

group(2)匹配最后一个3,没有group(3)。非捕获组不计入 group 结果只针对非捕获组所在的括号,但对非捕获组外面或者内部的捕获组并没有影响

In [1]: x = re.search(r'([a-c])(?:[d-f]([g-h]))','adg')

In [2]: x.group(0,1,2)
Out[2]: ('adg', 'a', 'g')

3.3.3 命名组

有时除了用 index 来访问匹配内容,还希望赋予匹配一个有意义的名字。此时可以用命名组(Named Group),它属于捕获组,主要解决组命名的问题。

(?P<name>REG)

P表示这是Python 正则表达式的语法(而非来自 Perl)。命名组是一种捕获组,之前捕获组使用 index 来访问 group 的方式依旧有效。命名组在此之外添加了用名字访问的方式,同时会获得一个非空的groupdict

In [1]: x = re.search(r'(?P<hh>[a-c])(?:[d-f]([g-h]))','adg')

In [2]: x
Out[2]: <re.Match object; span=(0, 3), match='adg'>

In [3]: x.group(0,1,2)
Out[3]: ('adg', 'a', 'g')

In [4]: x.group('hh')
Out[4]: 'a'

In [5]: x.groupdict()
Out[5]: {'hh': 'a'}

3.3.4 组的应用--匹配重复的字符(串)

如果要匹配连续重复的字符串,则可以用\{num},其中{num}是数字,一定要是一个存在的 index,且不能是0,这也要求要重复的对象一定要在小括号中。

In [1]: re.search(r'(\d)\1{2}', '21112121211')
Out[1]: <re.Match object; span=(1, 4), match='111'>

上面的\1中的1就是 group 的 index。

In [1]: re.search(r'(\d)\0{2}', '21112121211')

In [2]: re.search(r'\d\0{2}', '21112121211')

In [3]: re.search(r'(\d)\2{2}', '21112121211')
---------------------------------------------------------------------------
error                                     Traceback (most recent call last)
<ipython-input-32-86e83c2fdb65> in <module>
----> 1 re.search(r'(\d)\2{2}', '21112121211')

/usr/lib64/python3.9/re.py in search(pattern, string, flags)
    199     """Scan through string looking for a match to the pattern, returning
    200     a Match object, or None if no match was found."""
--> 201     return _compile(pattern, flags).search(string)
    202
    203 def sub(pattern, repl, string, count=0, flags=0):

/usr/lib64/python3.9/re.py in _compile(pattern, flags)
    302     if not sre_compile.isstring(pattern):
    303         raise TypeError("first argument must be string or compiled pattern")
--> 304     p = sre_compile.compile(pattern, flags)
    305     if not (flags & DEBUG):
    306         if len(_cache) >= _MAXCACHE:

/usr/lib64/python3.9/sre_compile.py in compile(p, flags)
    762     if isstring(p):
    763         pattern = p
--> 764         p = sre_parse.parse(p, flags)
    765     else:
    766         pattern = None

/usr/lib64/python3.9/sre_parse.py in parse(str, flags, state)
    946
    947     try:
--> 948         p = _parse_sub(source, state, flags & SRE_FLAG_VERBOSE, 0)
    949     except Verbose:
    950         # the VERBOSE flag was switched on inside the pattern.  to be

/usr/lib64/python3.9/sre_parse.py in _parse_sub(source, state, verbose, nested)
    441     start = source.tell()
    442     while True:
--> 443         itemsappend(_parse(source, state, verbose, nested + 1,
    444                            not nested and not items))
    445         if not sourcematch("|"):

/usr/lib64/python3.9/sre_parse.py in _parse(source, state, verbose, nested, first)
    523
    524         if this[0] == "\\":
--> 525             code = _escape(source, this, state)
    526             subpatternappend(code)
    527

/usr/lib64/python3.9/sre_parse.py in _escape(source, escape, state)
    421                 state.checklookbehindgroup(group, source)
    422                 return GROUPREF, group
--> 423             raise source.error("invalid group reference %d" % group, len(escape) - 1)
    424         if len(escape) == 2:
    425             if c in ASCIILETTERS:

error: invalid group reference 2 at position 5

3.4 关系匹配

有时候希望匹配的字符串前面或者后面满足一些条件,但是对条件本身的内容并不关心。此时就要用到关系匹配。

关系匹配也可以认为是零宽度匹配,返回宽度为零的字符串。

3.4.1 开头结尾匹配

^$分别匹配开头和结尾:

In [1]: re.search(r"^c\w", 'cacb')
Out[1]: <re.Match object; span=(0, 2), match='ca'>

In [2]: re.search(r"\w$", 'cacb')
Out[2]: <re.Match object; span=(3, 4), match='b'>

In [3]: re.search(r"https|http","https")
Out[3]: <re.Match object; span=(0, 4), match='http'>

In [4]: re.search(r"(http)|(https)", "https")
Out[4]: <re.Match object; span=(0, 4), match='http'>

更宽泛的条件是边界匹配,可用\b来表示正则表达式。

3.4.2 后向断言(lookbehind)

实际上要求字符串前面满足一些条件。 这翻译容易让人迷惑,估计这里是说要着重观察后面的结果并且返回。

(:<=REG)  # positive
(:<!REG)  # negative
In [1]: re.search(r'(?<=\d)abc', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(4, 7), match='abc'>

In [2]: re.search(r'(?<!\d)abc', 'abc1abcdabc\n')
Out[2]: <re.Match object; span=(0, 3), match='abc'>

In [3]: re.search(r'(?<=c)(?<=\d)abc', 'abc1abcdabc\n')

1 要求前面一定要有一个 \d;而 2 正好相反,只返回 4~7 处的abc

后向断言是一种零宽度匹配,它匹配长度为零的字符(串)。按照前面正则表达式的排列规则,多个正则表达式并列排布表示一起匹配。由于零宽度字符相加还是零款度字符,多个后向断言并列排布,还是匹配一个结果。此时就起到了一个与运算的效果。

In [1]: re.search(r'(?<=c\d)abc', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(4, 7), match='abc'>

3.4.2 前向断言(lookahead)

后向断言对称操作,表达式后面要满足一定条件。

(?=REG)  # positive
(?!REG)  # negative

这两种断言也可以联合使用:

In [1]: re.search(r'(?<!\d)abc(?=\d)', 'abc1abcdabc\n')
Out[1]: <re.Match object; span=(0, 3), match='abc'>

3.4.3 用断言来拼凑正则表达式取反

如何用正则表达式来确认一个字符串中没有另外的字符串?

3.5 正则表达式优先级

如同编程语言的运算都有运算优先级一样,正则表达式也有优先级,当正则表达式遇到一起时,就需要考虑其优先级。 下表列出了优先级,从上到下递减,从左到右递减:

  • \ 转义字符
  • (), (?:), (?=), (?!),[]
  • \*, +, ?, {n}, {n,}, {n,m}
  • ^, $, \任何元字符、任何字符 定位点和序列(即:位置和顺序)
  • | "或"操作字符

4 其它函数

4.1 re.findall找到所有匹配

re.findall返回所有匹配结果组成的列表:

In [1]: re.findall(r'[a-z][A-Z]', 'aAxxxxxxbBxxxxxx')
Out[1]: ['aA', 'bB']

In [2]: re.findall(r'([a-z])([A-Z])', 'aAxxxxxxbBxxxxxx')
Out[2]: [('a', 'A'), ('b', 'B')]

若正则表达中没有捕获组,返回列标的元素是整个匹配(group()group(0)),如上面的 1;如果有捕获组,则列表元素是所有 group 组成的 tuple,如上面的 2。

4.2 re.split分割字符串

re.split用于对字符串进行切片。比 Python 自带的str.split功能更加强大。

re.split(reg, string, maxsplit=0)

maxsplit的意思是最多切分次数,默认值0表示所有都切分。

In [1]: re.split(r"\d", "20python21")
Out[1]: ['', '', 'python', '', '']

In [2]: re.split(r"\d", "20python21", 1)
Out[2]: ['', '0python21']

split的结果包含空字符串。

4.3 re.sub替换字符串

re.sub 用于匹配对象替换,sub是 substring 的意思。

re.sub(reg, repl, string)

其中repl是替换方式,可以是固定字符串、函数或者lambda表达式,对于后面两种情况,操作的对象是match group

In [1]: re.sub(r'\d', 'NUM', "hello world 5")
Out[1]: 'hello world NUM'
In [1]: re.sub(r'\d', lambda x: str(int(x.group()) + 3), "hello world 5")
Out[1]: 'hello world 8'

字符串替换函数replace替换对象只能是固定的字符串,而正则表达式扩展了替换的对象。多次调用replace的结果并不一定和re.sub等价。

sub相似的函数是subn,返回一个 tuple,两个元素依次是替换后的字符串和替换次数:

In [1]: re.subn(r'\d', lambda x: str(int(x.group()) + 3), "hello world 5")
Out[1]: ('hello world 8', 1)

5 正则表达式实战

  1. 自动给 markdown 标题编号
  2. 自动给代码中的 IPython 输入输出编号

6 参考文献