正则表达式

正则表达式(Regular Expression,常简写为regex、regexp或re),是一种简练地描绘一组字符的方式,可用于高效、精确地执行字符串检索、替换等任务。

元字符

语法 描述 语法 描述 语法 描述 语法 描述
[abc] 单个字符:a或b或c [^abc] a/b/c以外的单个字符 [a-zA-Z0-9] 范围内的字符 . 任意字符(除\n)
\s 空白符(等价于[ \f\n\r\t\v]) \S 非空白符 \d 数字字符(十进制) \D 非数字字符
\w 单词(字母,数字,下划线,中文) \W 非单词 \b 单词边界 \B 非单词边界
^ \A 开头 $ \Z 结尾 (…) 分组 (a|b) 择一匹配:a或b
a* 重复0次或多次 a+ 重复1次或多次 a? 重复0次或1次 a{n} 重复n次
a{n,} 重复n次或多次 a{n,m} 重复n到m次 ? 非贪婪匹配 (?:abc) 非捕获分组
(?=abc) 正向匹配abc (?!abc) 正向不匹配abc \xhh 十六进制hh字符 \uhhhh 十六进制hhhh字符
\u{hhhh} 十六进制hhhh字符(设置u标志) (?#comment) 注释

修饰符

  • i - IgnoreCase:不区分大小写
  • m - MultiLine:多行匹配
  • s - DotAll:.匹配所有字符(包括\n
1
2
3
4
5
6
7
8
9
# 不区分大小写
print(re.match(r'a', 'A')) # None
print(re.match(r'a', 'A', re.I)) # <re.Match object; span=(0, 1), match='A'>
# 多行匹配
print(re.findall(r'^test-\w*', 'test-google\ntest-baidu\ntest-weibo')) # ['test-google']
print(re.findall(r'^test-\w*', 'test-google\ntest-baidu\ntest-weibo', re.M)) # ['test-google', 'test-baidu', 'test-weibo']
# 更改.含义
print(re.match(r'.*', 'a\nb\nc')) # <re.Match object; span=(0, 1), match='a'>
print(re.match(r'.*', 'a\nb\nc', re.S)) # <re.Match object; span=(0, 5), match='a\nb\nc'>

贪婪与懒惰

当正则表达式中包含限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符(主动回溯),这被称为贪婪匹配;相反的是,在表达式能得到匹配的前提下匹配尽可能少的字符,这就是懒惰匹配。在使用上,只需在限定符后添加?,就可以将贪婪限定符转换为惰性限定符。

限定符:限定前面的表达式出现的次数。

1
2
3
4
5
6
# 贪婪匹配
print(re.match(r'^a{2,5}$', 'aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
print(re.match(r'^a{2,5}', 'aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
# 懒惰匹配
print(re.match(r'^a{2,5}?$', 'aaaaa')) # <re.Match object; span=(0, 5), match='aaaaa'>
print(re.match(r'^a{2,5}?', 'aaaaa')) # <re.Match object; span=(0, 2), match='aa'>

分组和后向引用

捕获分组

(exp) :匹配表达式exp,并捕获文本到自动命名的组里。

(?Pexp) :匹配表达式exp,并捕获文本到名称为name的组里。

在使用小括号进行分组后,每个分组会自动拥有一个组号。组号分配会从左向右扫描两遍:先给未命名组分配,第一个出现的分组组号默认为1,第二个为2,以此类推,之后给命名组分配分组名称。分组0代表全局分组,对应着整个正则表达式匹配的结果。

1
2
3
4
5
6
7
8
it = re.finditer(r'age:(?P<agegroup>\d+),name:(\w+)', 'age:13,name:Tom;age:18,name:John')
for m in it:
print('------')
print(m.group()) # 输出捕获到的所有内容
print(m.group(0)) # 同m.group()
print(m.group(1)) # 输出age分组
print(m.group('agegroup')) # 同m.group(1)
print(m.group(2)) # 输出name分组

非捕获分组

(?:exp):匹配表达式exp,不捕获匹配的文本,也不给此分组分配组号。

1
2
3
4
# 不忽略分组
print(re.findall(r'age:(\d+),name:(\w+)', 'age:13,name:Tom;age:18,name:John')) # [('13', 'Tom'), ('18', 'John')]
# 忽略分组
print(re.findall(r'age:(?:\d+),name:(\w+)', 'age:13,name:Tom;age:18,name:John')) # ['Tom', 'John']

后向引用

所谓后向引用,就是在后式中对前面出现过的分组再一次引用,可用于重复搜索前面某个分组匹配的文本。

  • 通过索引引用: \1表示引用第一个分组,\2表示引用第二个分组,以此类推,\n表示引用第n个分组。
  • 通过命名分组名引用:命名 (?Pexp),引用 (?P=name) 。
1
2
3
4
5
6
7
8
9
# 示例1:匹配字符串中连续出现的两个相同单词
print(re.findall(r'\b(\w+)\b\s+\1\b', 'this is a test to match: Go Go home home')) # ['Go', 'home']
print(re.findall(r'\b(?P<testgroup>\w+)\b\s+(?P=testgroup)\b', 'this is a test to match: Go Go home home')) # ['Go', 'home']

# 示例2:字符串从后往前每隔3个字符插入一个','符号
s = '12345678900'
s = s[::-1]
s = re.sub(r'(...)', r'\1,', s)
print(s[::-1]) # 12,345,678,900

零宽断言

断言用来声明一个应该为真的事实,正则表达式中只有当断言为真时才会继续进行匹配。零宽断言是一种零宽度的匹配,它匹配到的内容不会保存到匹配结果中,最终匹配结果只是一个位置

零宽断言用于查找在某些内容(但并不包括这些内容)之前或者之后的东西,这个位置应该满足一定的条件(即断言)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 零宽度正预测先行断言
print(re.findall(r'\b\w+(?=ing\b)', 'reading and watching')) # ['read', 'watch']
print(re.findall(r'\b\w+(?=ing\b)', 'reading and watch')) # ['read']
print(re.findall(r'\b\w+(?=ing\b)', 'reading and watching1')) # ['read']
print(re.findall(r'\b.+(?=ing\b)', 'reading and watching')) # ['reading and watch']
# 零宽度正回顾后发断言
print(re.findall(r'(?<=\bread)\w+\b', 'reading and watching')) # ['ing']
print(re.findall(r'(?<=\bread)\w+\b', 'reading1 and watching')) # ['ing1']
print(re.findall(r'(?<=\bread)\w+\b', 'reading1 and reading2')) # ['ing1', 'ing2']
print(re.findall(r'(?<=\bread).+\b', 'reading and watching')) # ['ing and watching']
# 零宽度负预测先行断言
print(re.findall(r'\b\w+d(?!ing\b)\w*', 'reading and watching')) # ['and']
print(re.findall(r'\b\w+d(?!ing\b)\w*', 'read and watching')) # ['read', 'and']
print(re.findall(r'\b\w+d(?!ing\b)\w*', 'reading1 and watching')) # ['reading1', 'and']
print(re.findall(r'\b\w+d(?!ing)\w*', 'reading1 and watching')) # ['and']
print(re.findall(r'\b\w+(?!ing\b)\w*', 'reading and watching')) # ['reading', 'and', 'watching'] ??
# 零宽度负回顾后发断言
print(re.findall(r'\w+(?<!\brea)d\w*\b', 'reading and watching')) # ['and']
print(re.findall(r'\w+(?<!\brea)d\w*\b', 'eading and watching')) # ['eading', 'and']
print(re.findall(r'\w+(?<!\brea)d\w*\b', '1reading and watching')) # ['1reading', 'and']
print(re.findall(r'\w+(?<!rea)d\w*\b', '1reading and watching')) # ['and']
print(re.findall(r'\w+(?<!\brea)\w*\b', 'reading and watching')) # ['reading', 'and', 'watching'] ??