学习正则的第一个教程应该去看<正则表达式30分钟入门教程>, 学会里面的内容就足够满足大部分的工作需要了。而对于Python来说,就要学习模块re的使用方法。文本将展示一些大家都应该掌握的技巧。

编译正则对象

re.compile函数根据一个模式字符串和可选的标志参数生成一个正则表达式对象。该对象拥有一系列方法用于正则表达式匹配和替换。用法上略有区别,举个例子, 匹配一个字符串可用如下方式:

1
re.match(r'hello \w+', 'hello world') # re.match(pattern, string, flags=0)

如果使用compile,将变成:

1
2
regex = re.compile(r'hello \w+')
regex.match('hello world')

为什么要这么用呢?其实就是为了提高正则匹配的速度,重复利用正则表达式对象。我们对比一下2种方式的效率:

1
2
3
4
5
6
7
In : timeit -n 10000 re.match(r'hello \w+', 'hello world')
10000 loops, best of 3: 2.06 µs per loop
In : regex = re.compile(r'hello \w+')
In : timeit -n 10000 regex.match('hello world')
10000 loops, best of 3: 927 ns per loop

可以看到第二种方式要快很多。在实际的工作中你会发现越多的使用编译好的正则表达式对象,效果就越好。

分组(group)

你可能已经见过对匹配的内容进行分组的用法了:

1
2
3
4
5
6
7
8
9
10
In : match = re.match(r'hello (\w+)', 'hello world')
In : match.groups()
Out: ('world',)
In : match.group()
Out: 'hello world'
In : match.group(1)
Out: 'world'

通过对要匹配的对象添加括号,就可以精确的对应符合的结果了。我们还可以进行嵌套的分组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
In : m = re.search(r'(((\d{4})-\d{2})-\d{2})', '2016-01-01')
In : m.groups()
Out: ('2016-01-01', '2016-01', '2016')
In : m.group()
Out: '2016-01-01'
In : m.group(1)
Out: '2016-01-01'
In : m.group(2)
Out: '2016-01'
In : m.group(3)
Out: '2016'

分组都满足的需求的,但是有时候可读性很差,那可以对分组进行命名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
In : pattern = '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
In : m = re.search(pattern, '2016-01-01')
In : m.groupdict()
Out: {'day': '01', 'month': '01', 'year': '2016'}
In : m.group('year')
Out: '2016'
In : m.group('month')
Out: '01'
In : m.group('day')
Out: '01'

现在可读性非常高了。

字符串匹配

学过sed的同学可能见过如下替换用法:

1
2
echo ab123c | sed 's/\([0-9]\{3\}\)/[\1]/'
ab[123]c

这个\1表示前面正则匹配到的结果,也就是给匹配到的结果加上中括号。

在re模块中也存在这样的用法:

1
2
In : re.sub(r'(\d{4})-(\d{2})-(\d{2})', r'\2/\3/\1', '2016-01-01')
Out: '01/01/2016'

用命名分组也是可以的:

1
2
3
4
5
In : pattern = '(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
In : re.sub(pattern, r'\g<month>/\g<day>/\g<year>', '2016-01-01')
Out: '01/01/2016'

附近匹配(Look around)

re模块也支持附近匹配,看看例子就懂了:

1
2
3
4
5
6
7
8
9
10
11
In : re.sub('(?=\d{3})', ' ', 'abc12345def') # (?=XX) 从左向右匹配,符合内容的字符串后面加空格
Out: 'abc 1 2 345def'
In : re.sub('(?!\d{3})', ' ', 'abc12345def') # (?!XX) 和上面的匹配效果相反,也是后面加空格
Out: ' a b c123 4 5 d e f '
In : re.sub('(?<=\d{3})', ' ', 'abc12345def') # (?<=XX) 从右向左匹配,符合内容的字符串前面加空格
Out: 'abc123 4 5 def'
In : re.sub('(?<!\d{3})', ' ', 'abc12345def') # (?<!XX) 和上面的匹配效果相反,也是前面加空格
Out: ' a b c 1 2 345d e f '

正则匹配的时候使用函数

之前我们看到的大部分内容都是匹配的是一个表达式,但是有时候需求要复杂得多,尤其是在替换的时候。
举个例子,通过Slack的API能获取聊天记录,比如下面这句:

1
s = <@U1EAT8MG9>, <@U0K1MF23Z> 嗯 确实是这样的

其中<@U1EAT8MG9>和<@U0K1MF23Z>是2个真实的用户,但是被Slack封装了,需要通过其他接口获取这个对应关系,
其结果类似这样:

1
ID_NAMES = {'U1EAT8MG9': 'xiaoming', 'U0K1MF23Z': 'laolin'}

在解析对应关系之后,还希望吧尖括号也去掉,替换后的结果是「@xiaoming, @laolin 嗯 确实是这样的 」

用正则怎么实现呢?

1
2
3
4
5
6
7
8
9
10
In : REGEX_AT = re.compile(r'\<@.*?\>')
In : def id_to_name(match):
...: content = match.group()
...: name = ID_NAMES.get(content[2:-1])
...: return '@{}'.format(name) if name else content
...:
In : print REGEX_AT.sub(id_to_name, s)
@xiaoming, @laolin 嗯 确实是这样的

所以pattern当然也可以是一个函数