正则表达式概念

  1. python正则表达式
  2. 深入理解正则表达式环视的概念与用法
  3. 资源 | 正则表达式的功法大全
  4. 在线正则表达式验证网站

概念定义

使用单个字符串来描述匹配某个句法规则的字符串,是对字符串操作的一种逻辑公式

应用场景

处理文本和数据,提高复杂文本分析的效率

正则表达式过程

依次拿出表达式和文本中的字符比较,如果每一个字符都能匹配,则匹配成功;否则匹配失败

1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*
import re
from IPython.core.interactiveshell import InteractiveShell

InteractiveShell.ast_node_interactivity = "all" # 可同时输出多个结果
pattern_s = r'imooc' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
str1 = 'imooc book' # 需要查找的原始字符串
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')

match_r.group(): imooc

正则元字符

内容匹配

正则表达式 代表的匹配字符
. 匹配任意字符(不包括换行符)
[0-9] 0-9的数字
[a-z] 小写字母
[A-Z] 大写字母
\d 匹配数字,等同于[0-9]
\D 匹配非数字,等同于[\^0-9]
\w 匹配大小写字母、数字和下划线,等同于[a-z0-9A-Z_]
\W 匹配非大小写字母、数字和下划线,等同于[\^a-z0-9A-Z_]
\s 匹配空白
\S 匹配非空白
\u4e00-\u9fff 匹配中文字符

ps: 第(?P<law_idx>(?:(?!第|条).)*?)条可以匹配出公平竞争审查第三方评估实施指南(2023)第十一条中的第十一条,而不是第三方评估实施指南(2023)第十一条

个数匹配

正则表达式 代表的匹配字符
* 匹配前面的字符或者子表达式0次或多次
+ 匹配前一个字符或子表达式一次或多次
? 匹配前一个字符或子表达式0次或1次重复
{n} 匹配前一个字符或子表达式n次
{m,n} 匹配前一个字符或子表达式m至n次
{n,} 匹配前一个字符或者子表达式至少n次
*? / +? / ?? 惰性匹配上一个

位置匹配

正则表达式 代表的匹配字符
^ 匹配字符串开头, 多行模式下匹配每一行的开始
$ 匹配字符串结尾, 多行模式下匹配每一行的结束
\A / \Z 指定字符串必须出现在开头/结尾
\b 匹配位于单词开始或结束位置的空字符串
\B 匹配不位于单词开始或结束位置的空字符串

分组匹配

正则表达式 代表的匹配字符
| 匹配左右任意一个表达式
(ab) 括号里的表达式作为一个分组
\<number> 引用编号为num的分组匹配到的字符串
(?P<name>) 分组起别名
(?P=name) 引用别名为name的分组匹配字符串
[ ] 可匹配其中任意一个字符

转义匹配

正则表达式 代表的匹配字符
\ 转义字符,如\.只能匹配.,不能再匹配任意字符

基本使用

预编译

compile 函数用于编译正则表达式,生成一个正则表达式(Pattern)对象,供 match() 和 search() 这两个函数使用

如果重复多次地使用正则表达式,最好是使用compile函数把正则表达式编译成对象re.Pattern,这样会大大地提高搜索的效率

re.compile(pattern, flags=0)

属性说明

  • flags:编译时指定的模式
  • groupindex:以正则表达式中有别名的组的别名为键、以该组对应的编号为值的字典,没有别名的组不包含在内。
  • groups:正则表达式中分组的数量
  • pattern:编译时用的正则表达式

方法说明

  • findall、finditer、match、search、split、sub、subn 等函数

例子

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
# -*- coding: utf-8 -*
import re
s = 'Hello, Mr.Gumby : 2016/10/26'
pat = '''(?: # 构造一个不捕获分组 用于使用 |
(?P<name>\w+\.\w+) # 匹配 Mr.Gumby
| # 或
(?P<no>\s+\.\w+) # 一个匹配不到的命名分组
)
.*? # 匹配 :
(\d+) # 匹配 2016
'''
p = re.compile(pat, re.X)
print(f'p.flags: {p.flags}')
print(f'p.groupindex: {p.groupindex}')
print(f'p.groups: {p.groups}')
print(f'p.pattern: {p.pattern}')

p.flags: 96
p.groupindex: {'name': 1, 'no': 2}
p.groups: 3
p.pattern: (?: # 构造一个不捕获分组 用于使用 |
(?P<name>\w+\.\w+) # 匹配 Mr.Gumby
| # 或
(?P<no>\s+\.\w+) # 一个匹配不到的命名分组
)
.*? # 匹配 :
(\d+) # 匹配 2016

匹配模式

Python正则表达式,请不要再用re.compile了

在正则表达式中,采用预编译的优势就是可以节约时间,提高效率

re.compile(pattern, flags=0)给定一个正则表达式 pattern

指定使用的模式 flags 默认为0 即不使用任何模式,然后会返回一个 SRE_Pattern对象

参数说明

  • pattern: 一个字符串形式的正则表达式
  • flags: 可选,表示匹配模式,比如忽略大小写,多行模式等,使用按位或运算符 | 同时添加多个模式,具体参数为:
    • re.I:数值2,忽略大小写
    • re.L:数值4,表示特殊字符集 \w, \W, \b, \B, \s, \S 依赖于当前环境
    • re.M:数值8,多行模式
    • re.S:数值16,即为 . 并且包括换行符在内的任意字符(. 不包括换行符)
    • re.U:数值32,表示特殊字符集 \w, \W, \b, \B, \d, \D, \s, \S 依赖于 Unicode 字符属性数据库
    • re.X:数值64,为了增加可读性,忽略空格和 # 后面的注释

忽略大小写re.I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# I: IGNORECASE, 忽略大小写的匹配模式

# -*- coding: utf-8 -*
import re
str1 = 'hello World!'
pattern_s = "hello world!"
pattern_r = re.compile(pattern_s, re.I)
print(f'pattern_r.match(str1).group(): {pattern_r.match(str1).group()}')
# 或在正则表达式中指定模式以及注释
pattern_s = "(?#注释)(?i)hello world!"
pattern_r = re.compile(pattern_s)
print(f'pattern_r.match(str1).group(): {pattern_r.match(str1).group()}')

pattern_r.match(str1).group(): hello World!
pattern_r.match(str1).group(): hello World!

字符集本地化re.L

1
2
# L: LOCALE, 字符集本地化。这个功能是为了支持多语言版本的字符集使用环境的,比如在转义符\w,在英文环境下,它代表[a-zA-Z0-9_],即所以英文字符和数字。
# 如果在一个法语环境下使用,缺省设置下,不能匹配"é" 或"ç"。加上这L选项和就可以匹配了。不过这个对于中文环境似乎没有什么用,它仍然不能匹配中文字符。

多行模式re.M

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# M: MULTILINE,多行模式, 改变 ^ 和 $ 的行为
# -*- coding: utf-8 -*
import re
str1 = '''first line1
second line2
third line3'''
# ^
regex_start = re.compile("^\w+")
print(f'regex_start.findall(str1): {regex_start.findall(str1)}')
regex_start_m = re.compile("^\w+", re.M)
print(f'regex_start_m.findall(str1): {regex_start_m.findall(str1)}')

# $
regex_end = re.compile("\w+$")
print(f'regex_end.findall(str1): {regex_end.findall(str1)}')
regex_end_m = re.compile("\w+$", re.M)
print(f'regex_end_m.findall(str1): {regex_end_m.findall(str1)}')


regex_start.findall(str1): ['first']
regex_start_m.findall(str1): ['first', 'second', 'third']
regex_end.findall(str1): ['line3']
regex_end_m.findall(str1): ['line1', 'line2', 'line3']

所有字符re.S

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# S: DOTALL,此模式下 '.' 的匹配不受限制,可匹配任何字符,包括换行符
# -*- coding: utf-8 -*
import re
str2 = '''first line1
second line2
third line3'''
# 此时.无法匹配换行符
regex = re.compile(".+")
print(f'regex.findall(str2): {regex.findall(str2)}')
# 此时.匹配换行符
regex_dotall = re.compile(".+", re.S)
print(f'regex_dotall.findall(str2): {regex_dotall.findall(str2)}')

regex.findall(str2): ['first line1', 'second line2', 'third line3']
regex_dotall.findall(str2): ['first line1\nsecond line2\nthird line3']

冗余模式re.X

1
2
3
4
5
6
7
8
9
10
# X: VERBOSE,冗余模式, 忽略正则表达式中的空白和#号注释,如写一个匹配邮箱的正则表达式

# -*- coding: utf-8 -*
import re
email_regex = re.compile("[\w+\.]+@[a-zA-Z\d]+\.(com|cn)")

email_regex = re.compile("""[\w+\.]+ # 匹配@符前的部分
@ # @符
[a-zA-Z\d]+ # 邮箱类别
\.(com|cn) # 邮箱后缀 """, re.X)

UNICODE解析re.U

1
# U: UNICODE,使用 \w, \W, \b, \B 这些元字符时将按照 UNICODE 定义的属性.

re.Match类

若匹配成功,match/search返回的是Match对象,finditer返回的也是Match对象的迭代器

获取匹配结果需要调用Match对象的group()、groups或group(index)方法

属性说明

  • endpos: 本次搜索结束位置索引
  • lastgroup: 本次搜索匹配到的最后一个分组的别名
  • lastindex: 本次搜索匹配到的最后一个分组的索引
  • pos: 本次搜索开始位置索引
  • re: 本次搜索使用的 SRE_Pattern 对象
  • regs: 列表,元素为元组,包含本次搜索匹配到的所有分组的起止位置

方法说明

  • end([group=0]):返回指定分组的结束位置,默认返回正则表达式所匹配到的最后一个字符的索引

  • expand(template):根据模版返回相应的字符串,类似与 sub 函数里面的 repl, 可使用 \1 或者 \g 来选择分组

  • group([group1, …]):根据提供的索引或名字返回响应分组的内容

    默认返回 start() 到 end() 之间的字符串, 提供多个参数将返回一个元组

  • groupdict([default=None]):返回一个包含所有匹配到的命名分组的字典,没有命名的分组不包含在内

    key 为组名, value 为匹配到的内容,参数 default 为没有参与本次匹配的命名分组提供默认值

  • groups([default=None]):以元组形式返回每一个分组匹配到的字符串,包括没有参与匹配的分组,其值为 default

  • span([group]):返回指定分组的起止位置组成的元组,默认返回由 start() 和 end() 组成的元组

  • start([group]):返回指定分组的开始位置,默认返回正则表达式所匹配到的第一个字符的索引

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
# -*- coding: utf-8 -*
import re
s = 'Hello, Mr.Gumby : 2016/10/26'
m = re.search(', (?P<name>\w+\.\w+).*?(\d+)', s)
# 本次搜索的结束位置索引
print(f'm.endpos: {m.endpos}')
# 本次搜索匹配到的最后一个分组的别名
# 本次匹配最后一个分组没有别名
print(f'm.lastgroup: {m.lastgroup}')
# 本次搜索匹配到的最后一个分组的索引
print(f'm.lastindex: {m.lastindex}')
# 本次搜索开始位置索引
print(f'm.pos: {m.pos}')
# 本次搜索使用的 SRE_Pattern 对象
print(f'm.re: {m.re}')
# 列表,元素为元组,包含本次搜索匹配到的所有分组的起止位置 第一个元组为正则表达式匹配范围
print(f'm.regs: {m.regs}')
# 本次搜索操作的字符串
print(f'm.string: {m.string}')

m.endpos: 28
m.lastgroup: None
m.lastindex: 2
m.pos: 0
m.re: re.compile(', (?P<name>\\w+\\.\\w+).*?(\\d+)')
m.regs: ((5, 22), (7, 15), (18, 22))
m.string: Hello, Mr.Gumby : 2016/10/26
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
# -*- coding: utf-8 -*
import re
s = 'Hello, Mr.Gumby : 2016/10/26'
m = re.search('''(?: # 构造一个不捕获分组 用于使用 |
(?P<name>\w+\.\w+) # 匹配 Mr.Gumby
| # 或
(?P<no>\s+\.\w+) # 一个匹配不到的命名分组
)
.*? # 匹配 :
(\d+) # 匹配 2016
''',
s, re.X)
# 返回指定分组的结束位置,默认返回正则表达式所匹配到的最后一个字符的索引
print(f'm.end(): {m.end()}')
# 根据模版返回相应的字符串,类似sub()的 repl,可使用 \1 或者 \g<name> 来选择分组
mExpend = m.expand('my name is \\1')
print(f"m.expand('my name is \\1'): {mExpend}")
# 根据提供的索引或名字返回响应分组的内容,默认返回 start() 到 end() 之间的字符串,
# 提供多个参数将返回一个元组
print(f'm.group(): {m.group()}')
print(f'm.group(1, 2): {m.group(1, 2)}')
# 返回一个包含所有匹配到的命名分组的字典,没有命名的分组不包含在内,key 为组名,
# value 为匹配到的内容,参数 default 为没有参与本次匹配的命名分组提供默认值
m.groupdict('default_string')
# 以元组形式返回每一个分组匹配到的字符串,包括没有参与匹配的分组,其值为 default
print(f"m.groups('default_string'): {m.groups('default_string')}")
# 返回指定分组的起止未知组成的元组,默认返回由 start() 和 end() 组成的元组
print(f'm.span(3): {m.span(3)}')
# 返回指定分组的开始位置,默认返回正则表达式所匹配到的第一个字符的索引
print(f'm.start(3): {m.start(3)}')


m.end(): 22
m.expand('my name is \1'): my name is Mr.Gumby
m.group(): Mr.Gumby : 2016
m.group(1, 2): ('Mr.Gumby', None)
m.groups('default_string'): ('Mr.Gumby', 'default_string', '2016')
m.span(3): (18, 22)
m.start(3): 18

匹配

match

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

首字母开始匹配,string如果包含pattern子串,则匹配成功,返回Match对象,失败则返回None,若要完全匹配,pattern要以$结尾

参数说明

  • pattern:匹配的正则表达式
  • string:要匹配的字符串。
  • flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志

match函数在string的开始位置匹配,如果匹配则返回对象,否则返回None

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

import re

text = 'python world'
m = re.match(r"\w+", text)
print(m.group(0)) if m else print('not match')

text = '#python world'
m = re.match(r"\w+", text)
print(m.group(0)) if m else print('not match')


output: python
output: not match
1
2
3
4
5
6
7
8
# re.match()函数
# -*- coding: utf-8 -*
import re
str1 = '333STR1666STR299'
pattern_s = r'([A-Z]+(\d))'
print(f're.match(pattern_s, str1): {re.match(pattern_s, str1)}') # str1的开头不符合正则,所以结果为None

re.match(pattern_s, str1): None

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

若string中包含pattern子串,则返回Match对象,否则返回None,注意,如果string中存在多个pattern子串,只返回第一个

参数说明

  • pattern:匹配的正则表达式
  • string:要匹配的字符串
  • flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等

search会扫描整个string查找匹配,如果找到则返回一个相应的匹配对象,否则返回None

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

import re

text = 'python world'
m = re.search(r"\w+", text)
print(m.group(0)) if m else print('not match')

text = '#python world'
m = re.search(r"\w+", text)
print(m.group(0)) if m else print('not match')


output: python
output: python
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# re.search()函数

# -*- coding: utf-8 -*
import re
str1 = '333STR1666STR299'
pattern_s = r'([A-Z]+(\d))'
match_r = re.search(pattern_s, str1)
# match_r[0]是regex所代表的整个字符串
print(f'match_r.group(0): {match_r.group(0)}') # match_r.group()
# match_r[1]是第一个()中的内容
print(f'match_r.group(1): {match_r.group(1)}')
# match_r[2]是第二对()中的内容
print(f'match_r.group(2): {match_r.group(2)}')
# 所有group组成的一个元组
print(f'match_r.groups(): {match_r.groups()}')

match_r.group(0): STR1
match_r.group(1): STR1
match_r.group(2): 1
match_r.groups(): ('STR1', '1')

检索

findall

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

返回string中所有与pattern相匹配的全部字串,返回形式为数组

参数说明

  • string: 待匹配的字符串
  • pos: 可选参数,指定字符串的起始位置,默认为 0
  • endpos: 可选参数,指定字符串的结束位置,默认为字符串的长度

findall返回所有匹配的指定模式的文本子串到列表中,一个元素一个匹配串

1
2
3
4
5
6
7
8
9
10
11
12
# re.findall()函数

# -*- coding: utf-8 -*
import re
str1 = '333STR1666STR299'
pattern_s = r'([A-Z]+(\d))'
match_r = re.findall(pattern_s, str1)
for m in match_r:
print(f'm[0], m[1]: {m[0], m[1]}')

m[0], m[1]: ('STR1', '1')
m[0], m[1]: ('STR2', '2')

finditer

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

返回string中所有与pattern相匹配的全部字串,返回形式为迭代器

参数说明

  • pattern:匹配的正则表达式
  • string:要匹配的字符串。
  • flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志

finditer函数跟findall函数类似,但返回的是一个迭代器

1
2
3
4
5
6
7
8
9
10
11
12
# re.finditer()函数

# -*- coding: utf-8 -*
import re
str1 = '333STR1666STR299'
pattern_s = r'([A-Z]+(\d))'
match_r = re.finditer(pattern_s, str1)
for m in match_r:
print(f'm.group(0), m.group(1), m.group(2): {m.group(0), m.group(1), m.group(2)}') # 字符串

m.group(0), m.group(1), m.group(2): ('STR1', 'STR1', '1')
m.group(0), m.group(1), m.group(2): ('STR2', 'STR2', '2')

替换和分割

sub替换

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

  • 将字符串中匹配正则表达式的部分替换为其他值
  • Return the string obtained by replacing the leftmost non-overlapping occurrences of the pattern in string by the replacement repl.
  • repl can be either a string or a callable; if a string, backslash escapes in it are processed. If it is a callable, it’s passed the Match object and must return a replacement string to be used.

参数说明

  • pattern: 正则中的模式字符串。
  • repl: 替换的字符串,也可为一个函数。
  • string: 要被查找替换的原始字符串。
  • count: 模式匹配后替换的最大次数,默认 0 表示替换所有的匹配。
代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# -*- coding: utf-8 -*
import re
def add_one(match):
val = match.group()
num = int(val) + 1
return str(num)
str2 = 'imooc videonum=200'
reSub_1 = re.sub(r'\d+', add_one, str2)
print(f"re.sub(r'\d+', add_one, str2): {reSub_1}")
reSub_2 = re.sub(r'\d+', '203', str2)
print(f"re.sub(r'\d+', '203', str2): {reSub_2}")

sample_text = '2020-05-20 10:59:23 hello world 2020-05-21 10:59:24 hello kitty'
sample_pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
sample_repl = r'\g<month>/\g<day>/\g<year>'
print(re.sub(sample_pattern, sample_repl, sample_text))
输出
1
2
3
re.sub(r'\d+', add_one, str2): imooc videonum=201
re.sub(r'\d+', '203', str2): imooc videonum=203
05/20/2020 10:59:23 hello world 05/21/2020 10:59:24 hello kitty

高级用法

一日一技:如何正确使用 re.sub 的第二个参数

re.sub第二个参数可以是函数

设想有一个字符串abc18123456794xyz123,这个字符串中有两段数字,并且长短是不一样的

第一个数字是11位的手机号。我想把字符串替换为:`abc[隐藏手机号]xyz*

不是手机号的数字,每一位数字逐位替换为星号

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

def test(repl):
if len(repl.group(0)) == 11:
return '[隐藏手机号]'
else:
return '*' * len(repl.group(0))

a = 'abc18123456794xyz123'
b = re.sub('\d+', test, a)
print(b)

subn替换

subn(pattern, repl, string, count=0, flags=0)

作用与函数 sub 一样, 唯一不同之处在于返回值为一个元组,第一个值为替换后的字符串,第二个值为发生替换的次数

split分割

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

  • 根据匹配分割字符串,返回分割字符串组成的列表
  • Split the source string by the occurrences of the pattern, returning a list containing the resulting substrings.
  • If capturing parentheses are used in pattern, then the text of all groups in the pattern are also returned as part of the resulting list. If maxsplit is nonzero, at most maxsplit splits occur, and the remainder of the string is returned as the final element of the list.

参数说明

  • pattern:匹配的正则表达式
  • string:要匹配的字符串。
  • maxsplit:分隔次数,maxsplit=1 分隔一次,默认为 0,不限制次数。
  • flags:标志位,用于控制正则表达式的匹配方式,如:是否区分大小写,多行匹配等等。参见:正则表达式修饰符 - 可选标志
代码
1
2
3
4
5
# -*- coding: utf-8 -*
import re
str3 = 'imooc:C C++ Java Python,c#'
resub_3 = re.split(r':| |,',str3)
print(f"re.split(r':| |,',str3):{resub_3}")
输出
1
re.split(r':| |,',str3):['imooc', 'C', 'C++', 'Java', 'Python', 'c#']

正则缓存

当你在程序中使用 re 模块,无论是先使用 compile 还是直接使用比如 findall 来使用正则表达式操作文本

re 模块都会将正则表达式先编译一下, 并且会将编译过后的正则表达式放到缓存中

这样下次使用同样的正则表达式的时候就不需要再次编译, 因为编译其实是很费时的,这样可以提升效率

而默认缓存的正则表达式的个数是 100,当你需要频繁使用少量正则表达式的时候,缓存可以提升效率

而使用的正则表达式过多时,缓存带来的优势就不明显了

这个re.purge()函数的作用是清除缓存中的正则表达式,可能在你需要优化占用内存的时候会用到

爬虫例子

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -*- coding: utf-8 -*
import urllib.request
import re
import os

url = r'https://www.imooc.com/course/list?c=ai'
res = urllib.request.urlopen(url)
html = res.read().decode('utf-8')
listurl = re.findall(r'src="//img\d.+?jpg"', html)
lurl = ['http:%s' % x[5:-1] for x in listurl]
print(lurl)

basepath = './res'
if not os.path.exists(basepath):
os.makedirs(basepath)
imgspath = os.path.join(basepath, 're_op')
if not os.path.exists(imgspath):
os.mkdir(imgspath)
for ii, uu in enumerate(lurl):
savepath = os.path.join(imgspath, '%d.jpg' % (ii))
res = urllib.request.urlretrieve(uu, savepath)
输出
1
['http://img4.mukewang.com/5bd8157a0001a7a506000336-240-135.jpg', 'http://img3.mukewang.com/5bc6e6b80001434f06000338-240-135.jpg', 'http://img2.mukewang.com/5ba2386600013d3705980337-240-135.jpg', 'http://img3.mukewang.com/5b9105800001288905400300-240-135.jpg', 'http://img4.mukewang.com/5b7f737a0001cfb706000336-240-135.jpg', 'http://img4.mukewang.com/5abc6159000142f706000338-240-135.jpg', 'http://img1.mukewang.com/5a40c6370001d13c06000338-240-135.jpg']

进阶用法

非贪婪匹配

匹配模式

  • 贪婪匹配:贪婪匹配在匹配字符串时总是尝试匹配尽可能多的字符

  • 非贪婪匹配:与贪婪匹配相反,非贪婪匹配在匹配字符串时总是尝试匹配尽可能少的字符

Python里数量词默认是贪婪模式的,使用*? / +? / ??可使贪婪模式变成非贪婪模式

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
# -*- coding: utf-8 -*
import re

str1 = '9abc' # 需要查找的原始字符串
pattern_s = r'[0-9][a-z]*?' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')
pattern_s = r'[0-9][a-z]*' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')

pattern_s = r'[0-9][a-z]+?' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')
pattern_s = r'[0-9][a-z]+' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')

pattern_s = r'[0-9][a-z]??' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')
pattern_s = r'[0-9][a-z]?' # 定义正则表达式
pattern_r = re.compile(pattern_s) # 编译正则表达式
match_r = pattern_r.match(str1)
print(f'match_r.group(): {match_r.group()}')


match_r.group(): 9
match_r.group(): 9abc
match_r.group(): 9a
match_r.group(): 9abc
match_r.group(): 9
match_r.group(): 9a

分组匹配(捕获组)

正则表达式分组语法示例

1
2
3
4
5
6
7
8
import regex as re

pat = re.compile(r"有(?P<zj>(许可证[件]?|执照))的?(?P<xf_type>(暂扣|吊销).{0,4}(?P=zj))")
doc = "有许可证的吊销许可证,并给予严厉惩罚"
res = list(re.finditer(pat, doc))
res[0].groupdict()

Out[16]: {'zj': '许可证', 'xf_type': '吊销许可证'}
1
2
3
4
5
6
7
8
9
# 分组匹配之 \<number>
# -*- coding: utf-8 -*
import re

str1 = '<book>python</book>'
pattern_s = r'<([\w]+>)[\w]+</\1'
print(re.match(pattern_s, str1).group())

<book>python</book>
1
2
3
4
5
6
7
8
9
10
11
# 分组匹配之 别名 (?P<name>) 和 (?P=name)
# -*- coding: utf-8 -*
import re

str1 = '<book>python</book>'
pattern_s = r'<(?P<mark1>[\w]+>)[\w]+</(?P=mark1)'
print(re.match(pattern_s, str1).group())
Out[12]: <book>python</book>

re.match(pattern_s, str1).groupdict()
Out[13]: {'mark1': 'book>'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 分组匹配之group()和groups()
group() / group(0): 母串中与模式pattern匹配的子串
group(index): 第index个group匹配成功的子串
groups(): 所有group组成的一个元组,与pattern中的()有关

# -*- coding: utf-8 -*
import re
p = re.compile('\d-\d-\d')
m = p.match('2-3-1')
print(f'm.group(): {m.group()}')
print(f'm.group(0): {m.group(0)}')
print(f'm.groups(): {m.groups()}')

m.group(): 2-3-1
m.group(0): 2-3-1
m.groups(): ()
1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*
import re
p = re.compile('(\d)-(\d)-(\d)')
m = p.match('2-3-1d5-4-3')
print(f'm.group(): {m.group()}')
print(f'm.group(0): {m.group(0)}')
print(f'm.groups(): {m.groups()}')

m.group(): 2-3-1
m.group(0): 2-3-1
m.groups(): ('2', '3', '1')
1
2
3
4
5
6
7
8
9
10
11
# -*- coding: utf-8 -*
import re
p = re.compile('(\d)-(\d)-(\d)')
m = re.match(p,'2-3-1d5-4-3')
print(f'm.group(): {m.group()}')
print(f'm.group(0): {m.group(0)}')
print(f'm.groups(): {m.groups()}')

m.group(): 2-3-1
m.group(0): 2-3-1
m.groups(): ('2', '3', '1')

转义匹配替代函数

使用python的过程中,你肯定对转义字符的使用苦恼过,因为有的时候我们需要使用一些特殊符号如”$ * . ^”等的原意

有时候需要被转义后的功能,并且转义字符地使用很繁琐,容易出错

re.escape(pattern)

  • 转义: 如果你需要操作的文本中含有正则的元字符,你在写正则的时候需要将元字符加上反斜扛 \ 去匹配自身
  • 而当这样的字符很多时,写出来的正则表达式就看起来很乱而且写起来也挺麻烦的,这个时候你可以使用这个函数
  • 可以对字符串中所有可能被解释为正则运算符的字符进行转义的应用函数
1
2
3
4
5
6
7
8
9
10
11
12
# -*- coding: utf-8 -*
import re
str2 = ".+\d123"
regex_str = re.escape(".+\d123")
# 查看转义后的字符
print(f'regex_str: {regex_str}')
# 查看匹配到的结果
for g in re.findall(regex_str, str2):
print(g)

regex_str: \.\+\\d123
.+\d123

re增强的包装

主要目的是,增强re的匹配能力,为match对象添加了start: int=0参数,start指定了当前字符串的起始位置,默认的起始位置是0

导入库,regex是re的增强版,需要pip安装,它支持更强的正则,比如一个表达式中可以出现两个一样的捕获组名字

1
2
from typing import Iterable, List
import regex as re

包装match对象为ReMatch类

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
class ReMatch:
def __init__(self, match, start: int = 0):
"""
指定起始位置的Match类
:param match: re.Match对象
:param start:
"""
self.match = match
self.start_idx = start

@property
def endpos(self):
""" 本次搜索结束位置索引 """
return self.match.endpos + self.start_idx

@property
def lastgroup(self):
""" 本次搜索匹配到的最后一个分组的别名 """
return self.match.lastgroup

@property
def lastindex(self):
""" 本次搜索匹配到的最后一个分组的索引 """
return self.match.lastindex + self.start_idx if type(self.match.lastindex) == int else self.match.lastindex

@property
def pos(self):
""" 本次搜索开始位置索引 """
return self.match.pos + self.start_idx

@property
def re(self):
""" 本次搜索使用的SRE_Pattern对象 """
return self.match.re

@property
def regs(self):
""" 列表,元素为元组,包含本次搜索匹配到的所有分组的起止位置 """
return tuple([(s + self.start_idx, e + self.start_idx) for s, e in self.match.regs])

def end(self, *args, **kwargs):
""" 返回指定分组的结束位置,默认返回正则表达式所匹配到的最后一个字符的索引 """
return self.match.end(*args, **kwargs) + self.start_idx

def expand(self, *args, **kwargs):
""" 根据模版返回相应的字符串,类似与 sub 函数里面的 repl, 可使用 \1 或者 \g 来选择分组 """
return self.match.expand(*args, **kwargs)

def group(self, *args, **kwargs):
""" 根据提供的索引或名字返回响应分组的内容,默认返回 start() 到 end() 之间的字符串, 提供多个参数将返回一个元组 """
return self.match.group(*args, **kwargs)

def groupdict(self, *args, **kwargs):
""" 返回一个包含所有匹配到的命名分组的字典,没有命名的分组不包含在内,key 为组名, value 为匹配到的内容,参数 default 为没有参与本次匹配的命名分组提供默认值 """
return self.match.groupdict(*args, **kwargs)

def _build_key_2_index(self):
groups = self.match.groups()
group_2_index = []
for idx, group in enumerate(groups, start=1):
group_2_index.append((group, self.match.start(idx)+self.start_idx, self.match.end(idx)+self.start_idx))

return group_2_index

def groupdict_with_index(self, *args, **kwargs):
""" 同groupdict, 但匹配结果包装为带索引的ContentWithIndex对象 """
value = self.match.groupdict(*args, **kwargs)
new_group_dict = dict()
for k, v in value.items():
for group, s, e in self._build_key_2_index():
if v == group:
new_group_dict[k] = ContentWithIndex(content=v, start=s, end=e)
assert len(new_group_dict) == len(value)
return new_group_dict

def groups(self, *args, **kwargs):
""" 以元组形式返回每一个分组匹配到的字符串,包括没有参与匹配的分组,其值为 default """
return self.match.groups(*args, **kwargs)

def span(self, *args, **kwargs):
""" 返回指定分组的起止位置组成的元组,默认返回由 start() 和 end() 组成的元组 """
return tuple([(s + self.start_idx, e + self.start_idx) for s, e in self.match.span(*args, **kwargs)])

def start(self, *args, **kwargs):
""" 返回指定分组的开始位置,默认返回正则表达式所匹配到的第一个字符的索引 """
return self.match.start(*args, **kwargs) + self.start_idx

def start_and_end(self):
return self.start(), self.end()

def __str__(self):
return self.__repr__()

def __repr__(self):
return f"<regex.Match object; span=({self.start()}, {self.end()}), match='{self.match.group()}'>"

匹配结果类ContentWithIndex:主要是统一像split这样的方法返回的结果

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
class ContentWithIndex:
def __init__(self, content: str, start: int, end: int):
"""
带索引的切分结果
:param content: 切分结果
:param start: 起始位置
:param end: 结束位置
"""
self.content = content
self.start = start
self.end = end

@property
def pos(self):
return self.start

@property
def posend(self):
return self.end

def start_and_end(self):
return self.start, self.end

def __str__(self):
return self.__repr__()

def __repr__(self):
return f"{self.content} ({self.start}, {self.end})"

re包装的工具类ReUtils

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class ReUtils:
def __init__(self):
""" 正则匹配的工具类 """
pass

@staticmethod
def match(pattern, string, flags=0, start: int = 0):
match = re.match(pattern=pattern, string=string, flags=flags)
return ReMatch(match=match, start=start)

@staticmethod
def fullmatch(pattern, string, flags=0, start: int = 0):
match = re.fullmatch(pattern=pattern, string=string, flags=flags)
return ReMatch(match=match, start=start)

@staticmethod
def search(pattern, string, flags=0, start: int = 0):
match = re.search(pattern=pattern, string=string, flags=flags)
return ReMatch(match=match, start=start)

@staticmethod
def sub(pattern, repl, string, count=0, flags=0):
return re.sub(pattern=pattern, repl=repl, string=string, count=count, flags=flags)

@staticmethod
def subn(pattern, repl, string, count=0, flags=0):
return re.subn(pattern=pattern, repl=repl, string=string, count=count, flags=flags)

@staticmethod
def split(pattern, string, maxsplit=0, flags=0):
return re.split(pattern=pattern, string=string, maxsplit=maxsplit, flags=flags)

@staticmethod
def split_with_index(pattern, string, maxsplit: int = 0, flags=0, start: int = 0) -> List[ContentWithIndex]:
"""
split带内容索引
:param pattern:
:param string:
:param maxsplit:
:param flags:
:param start:
:return:
"""
matches = ReUtils.finditer(pattern, string, flags, start=start) # 使用re.finditer()获取匹配的位置和内容
content_with_indexs = []
start_idx = start
for match in matches:
end_idx = match.start() # 当前分隔符的起始位置
content_with_index = ContentWithIndex(content=string[start_idx - start:end_idx - start], start=start_idx,
end=end_idx)
content_with_indexs.append(content_with_index) # 添加分隔符之前的文本及其位置信息
start_idx = match.end() # 更新下一个分隔符的起始位置
if maxsplit and len(content_with_indexs) >= maxsplit:
content_with_index = ContentWithIndex(content=string[start_idx - start:], start=start_idx,
end=len(string))
content_with_indexs.append(content_with_index) # 添加分隔符之前的文本及其位置信息
break
else:
# 添加最后一个分隔符之后的文本
content_with_index = ContentWithIndex(content=string[start_idx - start:], start=start_idx,
end=len(string) + start)
content_with_indexs.append(content_with_index)

# if maxsplit and len(content_with_indexs) < maxsplit + 1 and start_idx < len(string):
# # 添加最后一个分隔符之后的文本
# content_with_index = ContentWithIndex(content=string[start_idx-start:], start=start_idx, end=len(string))
# content_with_indexs.append(content_with_index)

return content_with_indexs

@staticmethod
def findall(pattern, string, flags=0):
return re.findall(pattern=pattern, string=string, flags=flags)

@staticmethod
def finditer(pattern, string, flags=0, start: int = 0) -> Iterable[ReMatch]:
for match in re.finditer(pattern=pattern, string=string, flags=flags):
yield ReMatch(match=match, start=start)