Python的re模块与正则表达式

正则表达式即 regular expression ,描述了一种字符串匹配的模式(pattern),可以用来检查一个字符串是否含有某种子串,也可以将匹配的子串替换,还可以从某个字符串中取出符合某个条件的子串等。

模式和被搜索的字符串既可以是 Unicode 字符串 (str) ,也可以是 8 位字节串 (bytes)。 但是 Unicode 字符串与 8 位字节串不能混用:也就是说,你不能用一个字节串模式去匹配 Unicode 字符串,反之亦然;类似地,当进行替换操作时,替换字符串的类型也必须与所用的模式和搜索字符串的类型一致。

正则表达式并不是 Python 语言独有的,在其他语言也很广泛地使用到正则表达式。Python 自 1.5 版本起增加了re 模块,它提供 Perl 风格的正则表达式模式。

正则表达式语法

普通字符

普通字符,即所有大写和小写字母、所有数字、所有标点符号和一些其他符号。

表达式 待匹配字符 匹配结果 说明
[0123456789] 8 True 在一个字符组里枚举合法的所有字符,字符组里的任意一个字符和”待匹配字符”相同都视为可以匹配
[0123456789] a False 由于字符组中没有 a 字符,所以不能匹配
[0-9] 7 True 也可以用 - 表示范围,[0-9] 就等同于 [0123456789]
[a-z] s True 同样的如果要匹配小写字母,直接用 [a-z] 就可以表示
[A-Z] A True [A-Z] 就表示匹配大写字母
[0-9a-fA-F] e True 可以匹配数字,大小写形式的 a~f,用来验证十六进制字符

元字符

元字符 匹配内容
. 匹配除换行符以外的任意单个字符
\w 匹配 字母 或 数字 或 下划线
\s 匹配任意的空白符,包括空格和制表符
\d 匹配一个数字
\n 匹配一个换行符
\t 匹配一个制表符
\bhello 匹配以 hello 为词首
hello\b 匹配以 hello 为词尾
\bhello\\b 精确匹配一个单词 hello
^ 锚定开始
$ 锚定结尾
\W 匹配非 字母 或 数字 或 下划线
\D 匹配一个非数字的字符
\S 匹配一个非空白符的字符
() 匹配括号内的表达式,也表示一个组
[...] 匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符
1
a|b   匹配 a 或 b

量词

量词 用法说明
* 其前面的字符出现任意次
+ 其前面的字符出现一次或多次
其前面的字符出现一次或零次
{n} 其前面的字符出现 n 次
{n,} 其前面的字符至少出现 n 次
{n,m} 其前面的字符至少出现 n 次,最多出现 m 次

分组及其捕获和后向引用

(…)

除了简单地判断是否匹配之外,正则表达式还有利用分组来提取子字符串的强大功能。用 () 表示的就是要提取的分组(Group)。匹配完成后,组合的内容可以被获取,并可以在之后用 \number 转义序列进行后向引用再次匹配。要匹配字符 ( 或者 ), 用 \(\),或者把它们包含在字符集合里: [(][)]

如果正则表达式中定义了组,就可以在 match 对象 或 search 对象上用 group()groups() 方法提取出子字符串来。group(1)group(2)……表示第1、2、……个个组匹配到的子字符串。 例如 ^(\d{3})-(\d{3,8})$ 分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

1
2
3
4
5
6
7
8
9
10
11
12
>>> import re
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')
>>> m # 返回一个匹配对象
<_sre.SRE_Match object; span=(0, 9), match='010-12345'>
>>> m.group() # 返回整个正则表达式匹配到的内容
'010-12345'
>>> m.group(1) # 返回被第一个分组匹配到的内容
'010'
>>> m.group(2) # 返回被第二个分组匹配到的内容
'12345'
>>> m.groups() # 返回所有分组匹配到的内容
('010', '12345')

但是需要注意如果没有定义组 group(0) (等价于 group())返回的将永远是正则匹配到的所有内容,而不会有子字符串。group(1)group(2)……表示第1、2、……个个组匹配到的子字符串串。

1
2
3
4
5
6
>>> import re
>>> m = re.match(r'^\d{3}-\d{3}', '010-12345')
>>> m
<_sre.SRE_Match object; span=(0, 7), match='010-123'>
>>> m.group()
'010-123'

(?:…)

这将禁止正则捕获分组。它匹配在括号内的任何正则表达式,但该分组所匹配的子字符串 不能 在执行匹配后被获取或是之后在模式中被引用。

1
2
3
4
5
6
>>> import re
>>> re.findall('www.(baidu|google).com', 'www.baidu.com')
['baidu']
# 这是因为findall会优先把匹配结果组里内容返回,如果想要匹配结果,使用 (?:) 取消优先权即可
>>> re.findall('www.(?:baidu|google).com', 'www.baidu.com')
['www.baidu.com']

(?P\<name>…)

(命名组合)类似正则组合,但是匹配到的子字符串组在外部是通过定义的 name 来获取的。组合名必须是有效的 Python 标识符,并且每个组合名只能用一个正则表达式定义,只能定义一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> import re
>>> s = '<body>something</body>'
>>> m = re.search(r'(?P<tag_start>.*?>).*?(?P<tag_end>/.*?>)', s)
>>> m.group(0)
'<body>something</body>'
>>> m.group(1)
'<body>'
>>> m.group(2)
'/body>'
>>> m.groups()
('<body>', '/body>')
>>> m.group('tag_start')
'<body>'
>>> m.group('tag_end')
'/body>'
>>>

命名组合可以在三种上下文中引用。如果样式是 (?P<quote>['"]).*?(?P=quote) (也就是说,匹配单引号或者双引号括起来的字符串):

引用组合 “quote” 的上下文 引用方法
在正则式自身内 (?P=quote)\1
处理匹配对象 m m.group('quote')m.end('quote')
传递到 re.sub() 里的 repl 参数中 \g<quote>\g<1>\1

(?P=name)

后向引用一个命名组合;它匹配和前面那个叫 name 的命名组中匹配到的字符串一模一样的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
>>> import re
>>> s = '<head><title>百度一下,你就知道</title></head>'
>>> m = re.search(r'<(?P<tag_start>title)>(?P<content>.*?)</(?P=tag_start)>', s)
>>> m.group()
'<title>百度一下,你就知道</title>'
>>> m.groups()
('title', '百度一下,你就知道')
>>> m.group(1)
'title'
>>> m.group('tag_start')
'title'
>>> m.group(2)
'百度一下,你就知道'
>>> m.group('content')
'百度一下,你就知道'
>>>
>>> m.group(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: no such group
>>>

转义符 \

正则表达式使用反斜杠(\)来表示特殊形式,或者把特殊字符转义成普通字符。 而反斜杠在普通的 Python 字符串里也有相同的作用,所以就产生了冲突。比如说,要匹配一个字面上的反斜 杠\,正则表达式模式不得不写成 \\\\,因为正则表达式里匹配一个反斜杠必须是 \\ ,而每个反斜杠在普通的 Python 字符串里都要写成 \\

这样就太麻烦了,比较人性化的解决办法是对于正则表达式样式使用 Python 的原始字符串表示法;在带有 r 前缀的字符串字面值中,反斜杠就不必做任何特殊处理。也就是说这种方法会让字符串中的 \ 失去转义功能,仅仅代表 \ 这个字符。 因此 r"\n" 表示包含 \n 两个字符的字符串,而 "\n" 则表示只包含一个换行符的字符串。

贪婪匹配

在满足匹配时,匹配尽可能长的字符串,这种匹配模式就是贪婪匹配。当在量词之后加一个问号 ?,则尽可能少的匹配,这就是懒惰匹配。在 Python 中默认情况下,采用是贪婪匹配。

正则 待匹配字符 匹配 结果 说明
<.*> <script>...<script> <script>...<script> 默认为贪婪匹配模式,会匹配尽量长的字符串
<.*?> <script>...<script> <script> <script> 加上? 则将贪婪匹配模式转为非贪婪匹配模式,会匹配尽量短的字符串

几个常用的非贪婪匹配 Pattern

1
2
3
4
5
*? 重复任意次,但尽可能少重复
+? 重复1次或更多次,但尽可能少重复
?? 重复0次或1次,但尽可能少重复
{n,m}? 重复n到m次,但尽可能少重复
{n,}? 重复n次以上,但尽可能少重复

对于正则表达式 .*? ,其中 . 是任意字符,* 是取任意长度,? 是非贪婪模式。合在一起就是匹配尽量少的任意字符,一般不会这么单独写。比如 .*?x 就是取前面任意长度的字符,直到一个字符 x 出现。

模块内容

re 模块定义了几个函数,有些函数是编译后的正则表达式方法的简化版本(少了一些特性)。绝大部分重要的应用,当我们在使用正则表达式时,re 模块内部会做两件事情:

  • 编译正则表达式,如果正则表达式的字符串本身不合法,会报错;

  • 用编译后的正则表达式去匹配字符串

在 Python 的交互式解释器中先导入 re 模块,然后输入 re._all__ 命令,即可看到该模块所包含的全部属性和函数。

1
2
>>> re.__all__
['match', 'fullmatch', 'search', 'sub', 'subn', 'split', 'findall', 'finditer', 'compile', 'purge', 'template', 'escape', 'error', 'A', 'I', 'L', 'M', 'S', 'X', 'U', 'ASCII', 'IGNORECASE', 'LOCALE', 'MULTILINE', 'DOTALL', 'VERBOSE', 'UNICODE']

compile

1
re.compile(pattern, flags=0)

将正则表达式的样式 pattern 编译为一个 正则表达式对象 (正则对象),通过这个对象的 match()search() 等方法来用于匹配。

这个表达式的行为可以通过 flags 指定 标记 的值来改变。具体的请参照本文最后 正则标志位 flags 相关内容。

对于如下正则

1
2
3
4
5
6
7
8
9
10
11
import re

s = 'Hello World'

pattern1 = re.compile('hello', flags=re.IGNORECASE)
result1 = pattern1.findall(s)

'''
输出结果:
['hello']
'''

其实等价于:

1
2
3
4
5
6
7
8
9
10
import re

s = 'Hello World'

result2 = re.findall('hello', s, flags=re.IGNORECASE)

'''
输出结果:
['hello']
'''

如果一个正则表达式要重复使用几千次,使用 re.compile() 方法预编译保存这个正则对象以便复用,接下来重复使用时就不需要编译这个步骤了,直接匹配,可以让程序更加高效。

注意: 通过 re.compile() 编译后的样式,和模块级的函数会被缓存, 所以少数的正则表达式使用无需考虑编译的问题。

findall

1
re.findall(pattern, string, flags=0)

在 string 中从左到右进行扫描,找到 pattern 所匹配到的所有子字符串,并按照找到的顺序返回一个列表,如果没有找到匹配的就返回一个空列表。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re

r1 = re.findall('abc[0-9]+', '123abc223abc323abcde')
r2 = re.findall('abc', 'bcd')
r3 = re.findall('.*?c', 'abc1234abcccc')

print(r1)
print(r2)
print(r3)

'''
输出结果:
['abc223', 'abc323']
[]
['abc', '1234abc', 'c', 'c', 'c']
'''

注意: 如果 pattern 中有分组,findall 会优先把分组匹配到的内容返回,如果想要整个正则的匹配结果,则使用 (?:) 取消优先权即可。

1
2
3
4
5
>>> import re
>>> re.findall('www.(baidu|google).com', 'www.baidu.com')
['baidu']
>>> re.findall('www.(?:baidu|google).com', 'www.baidu.com')
['www.baidu.com']

finditer

1
re.finditer(pattern, string, flags=0)

和 findall 类似,在字符串中找到正则表达式所匹配的所有子字符串,并把匹配到的内容保存到一个迭代器后返回。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

s = 'Hello World\nOne world, one dream.'

r1 = re.finditer('world', s, flags=re.IGNORECASE)

print(r1)

for i in r1:
print(i.group())


'''
输出结果:
<callable_iterator object at 0x7f7ddd3dc2b0>
World
world
'''

对于匹配到的内容特别多的情况,这种方法可以节省内存空间。

1
re.search(pattern, string, flags=0)

扫描整个 string 寻找第一个被 pattern 匹配的位置后不再匹配, 并返回一个相应的匹配对象。如果没有匹配,就返回 None。注意这和找到一个零长度的匹配是不同的,比如 (abc)? 去匹配字符串 123 可以匹配到一个空字符串,而不是没有匹配到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import re

s = 'Hello World\nOne world, one dream.'

r1 = re.search('world', s, flags=re.IGNORECASE)

print(r1)
print(r1.group())

'''
输出结果:
<_sre.SRE_Match object; span=(6, 11), match='World'>
world
'''

match

1
re.match(pattern, string, flags=0)

从字符串 string 的起始位置进行匹配,匹配到第一个则不再匹配 并返回一个对象,如果不是起始位置匹配成功的话,就返回 None 。注意这和找到一个零长度的匹配是不同的,比如 (abc)? 去匹配字符串 123 可以匹配到一个空字符串,而不是没有匹配到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import re

s = 'Hello World\nOne world, one dream.'

r1 = re.match('world', s, flags=re.IGNORECASE)
r2 = re.match('hello.*dream', s, flags=re.IGNORECASE | re.DOTALL)

print(r1)
print(r2)
print(r2.group())

'''
输出结果:
None
<_sre.SRE_Match object; span=(0, 32), match='Hello World\nOne world, one dream'>
Hello World
One world, one dream
'''

split

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

用 pattern 分开 string 。如果 maxsplit 非零, 最多进行 maxsplit 次分隔, 剩下的字符全部返回到列表的最后一个元素。默认情况下 maxsplit 值为 0 ,即不限制分割次数。对于一个没有被 pattern 匹配的字符串而言,split 不会对其作出分割。

如果在 pattern 中没有出现括号 ()(捕获组合),那么只会返回被 pattern 分隔的结果,不会保留 pattern 所匹配到的内容:

1
2
3
4
5
>>> import re
>>> s = 'one1two2three3four4five5'
>>> re.split(r'\d+', s)
['one', 'two', 'three', 'four', 'five', '']
>>>

如果在 pattern 中有括号 () (捕获组合),那么所有的组合里的文字也会包含在列表里,对于结尾的地方也是一样:

1
2
>>> re.split(r'(\d+)', s)
['one', '1', 'two', '2', 'three', '3', 'four', '4', 'five', '5', '']

这样的话,分隔组中的文字将会出现在结果列表中同样的位置。

sub

1
re.sub(pattern, repl, string, count=0, flags=0)

在 string 中使用 pattern 进行匹配,将所有被匹配到的内容使用 repl 将其替换掉,并将替换掉的字符串返回。如果整个 string 没有被 pattern 匹配到,则不加改变的返回 string 。

1
2
3
4
>>> import re
>>> s = 'def myfunc():'
>>> re.sub(r'def\s+[a-zA-Z_][a-zA-Z_]*\s*\(\s*\)', r'I replaced your func', s)
'I replaced your func:'

其中 repl 参数可以是字符串,也可以是函数。如果是字符串,则其中任何反斜杠转义序列都会被处理。 也就是说,\n 会被转换为一个换行符,\r 会被转换为一个回车符,依此类推。 未知的 ASCII 字符转义序列保留在未来使用,会被当作错误来处理。 其他未知转义序列例如 \& 会保持原样。 向后引用像是 \6 会使用 pattern 中第 6 组所匹配到的子字符串来替换。

在字符串类型的 repl 参数里,如上所述的转义和向后引用中,\g<name> 会使用命名组合 name(在 (?P<name>…) 语法中定义), \g<number> 会使用数字组;\g<2> 就是 \2,但它避免了二义性,如 \g<2>0\20 就会被解释为组 20,而不是组 2 后面跟随一个字符 0。后向引用 \g<0> 把 pattern 作为一整个组进行引用。

1
2
3
4
5
6
7
>>> import re
>>> s = 'hello w012345rld'
>>> re.sub(r'(hello w)([0-9]+)(.*)', r'\1o\3 \2', s)
'hello world 012345'
>>> re.sub(r'(hello w)(?P<number>\d+)(.*)', r'\1o\3 \g<number>', s)
'hello world 012345'
>>>

可选参数 count 是要替换的最大次数。count 必须是非负整数。如果忽略这个参数,或者设置为0,所有被匹配到的内容都会被替换。空匹配只在不相临连续的情况被更替,所以 sub('x*', '-', 'abxd') 返回 -a-b--d-

1
2
3
4
5
6
>>> import re
>>> s = 'Hello w00000rld'
>>> re.sub('\d', 'o', s, 1)
'Hello wo0000rld'
>>> re.sub('\d', 'o', s)
'Hello wooooorld'

subn

1
re.subn(pattern, repl, string, count=0, flags=0)

用法与 sub() 相同,但是会返回一个元组 (字符串, 替换次数):

1
2
3
4
5
6
>>> import re
>>> s = 'Hello w00000rld'
>>> re.sub('\d', 'o', s)
'Hello wooooorld'
>>> re.subn('\d', 'o', s)
('Hello wooooorld', 5)

正则表达式对象

编译后的正则表达式对象支持以下方法和属性:

search

1
Pattern.search(string[, pos[, endpos]])

扫描整个 string 寻找第一个被 pattern 匹配的位置后不再匹配, 并返回一个相应的匹配对象。如果没有匹配,就返回 None。注意这和找到一个零长度的匹配是不同的,比如 (abc)? 去匹配字符串 123 可以匹配到一个空字符串,而不是没有匹配到。

1
2
3
>>> pattern = re.compile("d")
>>> pattern.search("dog") # Match at index 0
<re.Match object; span=(0, 1), match='d'>

可选的第二个参数 pos 给出了字符串中开始搜索的位置索引;默认为 0,它不完全等价于字符串切片;

1
2
3
4
5
6
7
8
>>> import re
>>> r = re.compile('abc', flags=re.IGNORECASE)
>>> r.search('abc1ABC2aBc3').group()
'abc'
>>> r.search('abc1ABC2aBc3',2).group()
'ABC'
>>> r.search('abc1ABC2aBc3',5).group()
'aBc'

^ 样式字符匹配字符串真正的开头,和换行符后面的第一个字符,但不会匹配索引规定开始的位置。

1
2
3
4
5
6
7
8
9
10
11
>>> import re
>>> r = re.compile('^d', flags=re.MULTILINE | re.IGNORECASE)
>>> r.search('Tom\nJerry\nDog')
<_sre.SRE_Match object; span=(10, 11), match='D'>
>>>
>>> r.search('Tom and Jerry and Dog', 6) # 索引为6的字符是 and 中 d 的位置,但 ^ 不会匹配此处开始的字符
>>> r.search('Tom and Jerry and Dog', 6).group()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'group'
>>>

可选参数 endpos 限定了字符串搜索的结束;它假定字符串长度到 endpos , 所以只有从 pos 到 endpos - 1 的字符会被匹配。如果 endpos 小于 pos,就不会有匹配产生;

1
2
3
4
5
6
>>> r = re.compile(r'\d{3}')
>>> r.search('0123456789')
<_sre.SRE_Match object; span=(0, 3), match='012'>
>>> r.search('0123456789', 2, 8)
<_sre.SRE_Match object; span=(2, 5), match='234'>
>>>

另外,如果 rx 是一个编译后的正则对象, rx.search(string, 0, 50) 等价于 rx.search(string[:50], 0)

1
2
3
4
5
6
7
>>> rx = re.compile(r'\d{3}')
>>> rx.search('0123456789', 3, 7)
<_sre.SRE_Match object; span=(3, 6), match='345'>
>>> # 等价于
>>> rx.search('01234567890'[:7], 3)
<_sre.SRE_Match object; span=(3, 6), match='345'>
>>>

match

1
Pattern.match(string[, pos[, endpos]])

从字符串 string 的起始位置进行匹配,匹配到第一个则不再匹配 并返回一个对象,如果不是起始位置匹配成功的话,就返回 None 。注意这和找到一个零长度的匹配是不同的,比如 (abc)? 去匹配字符串 123 可以匹配到一个空字符串,而不是没有匹配到。

1
2
3
4
>>> pattern = re.compile("o")
>>> pattern.match("dog") # No match as "o" is not at the start of "dog".
>>> pattern.match("dog", 1) # Match as "o" is the 2nd character of "dog".
<re.Match object; span=(1, 2), match='o'>

可选参数 pos 和 endpos 与 search() 中的含义相同。

split

1
Pattern.split(string, maxsplit=0)

等价于 split() 函数,使用了编译后的样式。

findall

1
Pattern.findall(string[, pos[, endpos]])

类似函数 findall() , 使用了编译后样式,但也可以接收可选参数 pos 和 endpos ,限制搜索范围,就像 search()。

finditer

1
Pattern.finditer(string[, pos[, endpos]])

类似函数 finiter() , 使用了编译后样式,但也可以接收可选参数 pos 和 endpos ,限制搜索范围,就像 search()。对于匹配到的内容特别多的情况,这种方法可以节省内存空间。

sub

1
Pattern.sub(repl, string, count=0)

等价于 sub() 函数,使用了编译后的样式。

subn

1
Pattern.subn(repl, string, count=0)

等价于 subn() 函数,使用了编译后的样式。

flags

1
Pattern.flags

正则匹配标记。这是可以传递给 compile() 的参数,任何 (?…) 内联标记,隐性标记比如 UNICODE 的结合。

groups

1
Pattern.groups

捕获组合的数量。

pattern

1
Pattern.pattern

编译对象的原始样式字符串。

匹配对象

匹配对象总是有一个布尔值 True。如果没有匹配的话 match()search() 返回 None 。所以你可以简单的用 if 语句就能来判断是否匹配:

1
2
3
match = re.search(pattern, string)
if match:
process(match)

下面列举了匹配对象常用的方法和属性。

group

1
Match.group([group1, ...])

返回一个或者多个匹配的子字符串分组。如果只有一个参数,结果就是一个字符串,如果有多个参数,结果就是一个元组(每个参数对应一个项)。当要获得整个匹配的子串时,可直接使用 group()group(0) 。 如果它是 1 到 99 内的一个数字,结果就是相应的括号组字符串。

1
2
3
4
5
6
7
8
9
10
11
>>> import re
>>> s = 'Hello world! One world, one dream'
>>> matched=re.match(r'(\w+) (\w+)', s)
>>> matched.group()
'Hello world'
>>> matched.group(0)
'Hello world'
>>> matched.group(2)
'world'
>>> matched.group(1,2)
('Hello', 'world')

如果正则表达式使用了 (?P<name>…) 语法, groupN 参数就也可能是命名组合的名字:

1
2
3
4
5
6
7
>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.group()
'Malcolm Reynolds'
>>> m.group('first_name')
'Malcolm'
>>> m.group('last_name')
'Reynolds'

命名组合同样可以通过索引值引用:

1
2
3
4
5
6
>>> m.group(1)
'Malcolm'
>>> m.group(2)
'Reynolds'
>>> m.group(1,2)
('Malcolm', 'Reynolds')

如果一个组匹配成功多次,就只返回最后一个匹配:

1
2
3
4
5
6
7
>>> m = re.match(r"(..)+", "a1b2c3")  # Matches 3 times.
>>> m.group(1) # Returns only the last match.
'c3'
>>> m.group(2)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: no such group

__getitem__

这个等价于 m.group(g)。这允许更方便地引用一个匹配:

1
2
3
4
5
6
7
>>> m = re.match(r"(\w+) (\w+)", "Isaac Newton, physicist")
>>> m[0] # The entire match
'Isaac Newton'
>>> m[1] # The first parenthesized subgroup.
'Isaac'
>>> m[2] # The second parenthesized subgroup.
'Newton'

groups

1
Match.groups(default=None)

返回一个元组,包含所有匹配的子字符串分组。 default 参数用于某个分组没有匹配到的情况,默认为 None 。

1
2
3
4
5
6
>>> m = re.match(r'(\d+)\.(\d+)', '3.1415926')
>>> m.group()
'3.1415926'
>>> m.groups()
('3', '1415926')
>>>

如果我们使小数点可选,那么不是所有的组都能匹配到内容。对于这些没有匹配到的分组默认会返回一个 None ,除非指定了 default 参数。

1
2
3
4
5
>>> m = re.match(r"(\d+)\.?(\d+)?", "24")
>>> m.groups() # Second group defaults to None.
('24', None)
>>> m.groups(default='0') # Now, the second group defaults to '0'.
('24', '0')

start 和 end

1
Match.start([group])

用于获取分组匹配的子串在整个字符串中的起始位置(子串第一个字符的索引),参数默认值为 0;

1
Match.end([group])

用于获取分组匹配的子串在整个字符串中的结束位置(子串最后一个字符的索引+1),参数默认值为 0;

这两个方法中, group 默认都为 0,意思是针对正则匹配到的整个子字符串取其所在位置。

1
2
3
4
5
6
7
>>> s = '01234567_abc_'
>>> m = re.search(r'_[a-z]+_', s)
>>> m.start()
8
>>> m.end()
13
>>>

如果正则中使用了分组,没有指定参数就返回分组所匹配到的字符串的开始索引和结束索引标号,指定了参数就返回对应分组匹配到的字符串的开始索引和结束索引标号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> s = 'hello world, one world one dream'
>>> m = re.search(r'(\w+) (world).*?(world).*', s)
>>> m.groups()
('hello', 'world', 'world')
>>> m.start()
0
>>> m.end()
32
>>> m.start(1)
0
>>> m.start(2)
6
>>> m.start(3)
17
>>> m.end(1)
5
>>> m.end(2)
11
>>> m.end(3)

如果正则中使用了分组,但未产生匹配,就返回 -1 。

1
2
3
4
5
6
7
8
9
>>> s = '01234567_abc_'
>>> m = re.match(r'(\d+)_[a-z]+_(\d+)?', s)
>>> m.groups()
('01234567', None)
>>> m.start(1)
0
>>> m.start(2)
-1
>>>

注意,如果分组匹配到的是一个空字符串的话 m.start(group) 将会等于 m.end(group)

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> m = re.search('b(c?)', 'cba')
>>> m.groups() # 结果说明第一个分组匹配到的是空字符串
('',)
>>> m.group() # 结果说明整个正则匹配到的是 b
'b'
>>> m.start() # 整个字符串中匹配到的内容起始索引
1
>>> m.end() # 整个字符串中匹配到的内容结束索引
2
>>> m.start(1) # 第一个分组匹配到的是一个空字符串,在字符串 cba 中 b 跟 a 之间的空字符串,起始和结束索引都为2
2
>>> m.start(1)
2

这个例子会从 email 地址中移除掉 remove_this

1
2
3
4
5
6
7
8
9
10
11
>>> email = "tony@tiremove_thisger.net"
>>> m = re.search('remove_this', email)
>>> m.group()
'remove_this'
>>> m.start()
7
>>> m.end()
18
>>> email[:m.start()] + email[m.end():]
'tony@tiger.net'
>>>

span

1
Match.span([group])

对于一个匹配对象 m , 返回一个二元组 (m.start(group), m.end(group)) 。 注意如果对应的分组没有成功匹配,就返回 (-1, -1) 。group 默认为0,也就是整个匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> m = re.search(r'(\d+)\.(\d+)(\w+)([A-Z]+)?', '3.1415926')
>>> m.groups() # 返回所有分组匹配到的结果
('3', '141592', '6', None)
>>> m.group() # 返回整个正则匹配到的结果
'3.1415926'
>>> m.span() # 整个正则匹配到的字符串的起始结束索引
(0, 9)
>>> m.span(1) # 第一个分组匹配到的字符串的起始结束索引
(0, 1)
>>> m.span(2) # 第二个分组匹配到的字符串的起始结束索引
(2, 8)
>>> m.span(3)
(8, 9)
>>> m.span(4)
(-1, -1)

正则标志位 flags

正则函数中的 flags 是可选标志位,用于控制正则表达式的匹配方式,比如是否区分大小写,是否进行多行匹配等等。标志可以使用两种名字,一种是全名如 IGNORECASE,一种是缩写如 I 。多个标志可以通过按位 OR(|) 它们来指定。如 re.I | re.M 被设置成 IM 标志。

re.I
re.IGNORECASE

进行忽略大小写匹配;表达式如 [A-Z] 也会匹配小写字符。Unicode 匹配(比如 Ü 匹配 ü)同样有用,除非设置了 re.ASCII 标记来禁用非 ASCII 匹配。当前语言区域不会改变这个标记,除非设置了 re.LOCALE 标记。这个相当于内联标记 (?i)

1
2
3
4
5
6
7
8
9
10
import re

r1 = re.findall('[a-e]+', 'abCDEF', flags=re.IGNORECASE)

print(r1)

'''
输出结果:
['abCDE']
'''

re.L
re.LOCALE

由当前语言区域决定 \w\W\b\B , 和大小写敏感匹配。举个例子,如果你正在处理法文文本,想用 w+ 来匹配文字,但 w 只匹配字符类 [A-Za-z],它并不能匹配 é?。如果你的系统配置适当且本地化设置为法语,那么内部的 C 函数将告诉程序 é 也应该被认为是一个字母。当在编译正则表达式时使用 LOCALE 标志会得到用这些 C 函数来处理 w 后的编译对象;这会更慢,但也会像你希望的那样可以用 w+ 来匹配法文文本。

这个标记只能对 byte 样式有效,并且不推荐使用,因为语言区域机制很不可靠,它一次只能处理一个 “习惯”,而且只对 8 位字节有效。Unicode 匹配在 Python 3 里默认启用,并可以处理不同语言。

这个对应内联标记 (?L)

re.M
re.MULTILINE

使用 ^ 只匹配以某个字符串为开头,而 $ 则只匹配以某个字符串为结尾。设置了此标志将会改变 ^$ 的行为,此时 ^ 匹配以某个字符串为开头,还匹配每一行的开始(换行符后面紧跟的内容,如果有的话);并且 $ 匹配以某个字符串为结尾,还匹配每一行的结尾(换行符前面的内容,如果有的话)。说地直白点,就是开启多行模式的匹配。对应内联标记 (?m) 。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

r1 = re.findall('^a\w+$', 'abCDEF\nafda')
r2 = re.findall('^a\w+$', 'abCDEF\nafda', flags=re.MULTILINE)

print(r1)
print(r2)

'''
输出结果:
[]
['abCDEF', 'afda']
'''

re.S
re.DOTALL

默认情况下 . 会匹配除了换行符外的任何单个字符,如果设置了此标志将会使 . 匹配任何一个字符,包括换行符。对应内联标记 (?s) 。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
import re

r1 = re.findall('^H.*', 'Hello\nWorld')
r2 = re.findall('^H.*', 'Hello\nWorld', flags=re.DOTALL)

print(r1)
print(r2)

'''
输出结果:
['Hello']
['Hello\nWorld']
'''

re.X
re.VERBOSE

该标志通过给予你更灵活的格式以便你将正则表达式写得更容易理解。当设置了这个标志后,在 正则表达式 字符串中的空白符被忽略,除非该空白符在字符类中或在反斜杠之后;这可以让你更清晰地组织和缩进 正则表达式。它也可以允许你将注释写入 正则表达式,这些注释会被正则引擎忽略;注释用 #号 来标识,不过该符号不能在字符串或反斜杠之后。对应内联标记 (?x) 。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import re

s = '123.456 2.0 3 4.5'
r1 = re.findall(r'''\d+ # 一个或多个数字
\. # 一个小数点
\d* # 任意个数的数字''', s, flags=re.VERBOSE)
r2 = re.findall('\d+\.\d*', s)

print(r1)
print(r2)

'''
输出结果:
['123.456', '2.0', '4.5']
['123.456', '2.0', '4.5']
'''

re.DEBUG

显示编译时的 debug 信息,没有内联标记。

正则表达式例子

给定赋值操作 aline = '<title>Python的正则练习-获取tag间的内容</title>' ,利用 re 模块取出标记 <title </title 之间的内容 Python的正则练习-获取tag间的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re

astring = '<title>Python的正则练习-获取tag间的内容</title>'

m1 = re.search(
r'<(?P<tag_start>.*?)>(?P<content>.*?)</(?P=tag_start)', astring)

print(m1.group('content'))


m2 = re.search(
r'<(.*?)>(.*?)</\1>', astring
)

print(m2.group(2))

在爬虫时 Python 利用正则的分组十分广泛,例如我们要爬取豆瓣网站的 top250 电影信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from urllib.request import urlopen
import re

douban_top250_url = 'https://movie.douban.com/top250?start=%s&filter=' % 1
# 第一页的 URL

url_response = urlopen(douban_top250_url)
# 使用 url 模块通过网络将网页获取下来,网络传输的内容为 bytes 类型

web_resource = url_response.read().decode('utf-8')
print(type(web_resource))
# 将 utf-8 格式的 bytes 类型的内容 再以 utf-8 编码进行解码 decode
# 解码后的内容是 unicode格式,也就是 str 类型, 此时的内容和我们在网页上查看源代码一模一样

recomp = re.compile( # 预编译正则表达式
'<div class="item">.*?'
'<div class="pic">.*?'
'<em class="">(?P<id>\d+).*?'
'<span class="title">(?P<title>.*?)</span>', re.DOTALL
)

the_matched = recomp.finditer(web_resource)

for i in the_matched:
print({
"ID": i.group("id"),
# 此处的id ,即 compile中的 (?P<id>\d+)中匹配到的内容
# (?P<name>正则表达式),即可为正则匹配到的内容分组并为组添加名字为 'name'
# 如果分组较多的话,使用名称比数字更方便
"title": i.group("title"),
})

爬取的结果:

1
2
3
4
5
{'id': '2', 'title': '霸王别姬'}
{'id': '3', 'title': '这个杀手不太冷'}
{'id': '4', 'title': '阿甘正传'}
{'id': '5', 'title': '美丽人生'}
# ... 省略其他内容

加强版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import re
import json
from urllib.request import urlopen


def parsePage(s):
repattern = re.compile(
'<div class="item">.*?'
'<div class="pic">.*?'
'<em class="">(?P<id>\d+).*?'
'<span class="title">(?P<title>.*?)</span>.*?'
'<span class="rating_num".*?>(?P<rating_num>(\d|\.)+)</span>.*?'
'<span>(?P<comment_num>\d+)人评价</span>', flags=re.DOTALL
)

re_match = repattern.finditer(s)

for i in re_match:
yield {
'id': i.group('id'),
'title': i.group('title'),
'rating_num': i.group('rating_num'),
'comment_num': i.group('comment_num'),
}


def main(n):
url = 'https://movie.douban.com/top250?start=%s&filter=' % n
# 第 n 页的url

html_response = urlopen(url)
# 使用url模块通过网络将网页获取下来,内容为bytes类型

web_resource = html_response.read().decode('utf-8')

with open('douban_movie', 'a', encoding='utf-8') as fwt:
for obj in parsePage(web_resource):
print(obj)
data = json.dumps(obj, ensure_ascii=False)
fwt.write(data + '\n')


if __name__ == '__main__':
count = 0
for i in range(10):
main(count)
count += 25
有钱任性,请我吃包辣条
0%