(原文出自 Tony S. Yu 的 Readable regular expressions in Python,翻译已征得作者同意,非常感谢 Tony S. Yu。)
(博客太丑,特别是代码展示 TAT,如果无法忍受可以 到 Github 上看。)


前段时间,我需要解析 Python 格式化创建 的一些字符串。其实有个包就叫 解析(parse),它用了和字符串格式化一样的语法来从一个字符串中提取数据。不幸的是,因为一些理由我不想用这个包,于是我选择了更快的 正则表达式(regular expressions,简称 regexes)。

请注意,本文假定您已经熟悉正则表达式的基本语法。如果你不熟悉正则表达式,http://regexone.com/ 提供了很赞的互动教程。


命名捕获组(Named capturing groups)

我准备使用命名捕获组Named capturing groups),可是它的语法实在太难看了。所以让我们写了个简单的 name_regex 函数让它可读性好一点:

1
2
3
def name_regex(name, pattern):
"""返回一个‘命名捕获组’正则式"""
return r'(?P<{name}>{pattern})'.format(name=name, pattern=pattern)

为了搞清楚这个函数到底做了啥,我们传入两个字符串:

1
print name_regex('myname', 'Tony')

它返回:

1
(?P<myname>Tony)

这个字符串是个正则表达式,它所做的是:匹配目标字符串(Tony)并将结果保存在一个命名组(myname)中。

来个更有趣的例子吧,假设你想从一句话中提取一个价格,你可以找后面跟着数字和小数点的美元符号。

1
2
# 这并不是个好的正则式,因为它会匹配任何数字和小数点,不过现在就让它这样保持简单
rx_price = name_regex('price', r'\$[\d.]+')

像任何其他正则式一样,你可以用 Python 的内置正则表达式包 re 来使用这个正则式:

1
2
3
4
import re

match = re.search(rx_price, "All your's for only $9.95!")
print match.groupdict()['price']
1
$9.95

这的确提取我们想要的文字,但如果你只是找单一的串,用命名捕获组并没有多大用处。


用命名捕获组格式化字符串(Named regexes with string formatting)

这次我们不用单一的命名捕获组正则式了。让我们创建一个(name, pattern)形式的正则式字典吧:

1
2
3
def named_regexes(**names_and_patterns):
"""返回一个包含多个对应命名捕获组的正则式字典"""
return {k: name_regex(k, p) for k, p in names_and_patterns.items()}

如果你觉得这看起来有点奇怪,那只是因为我们 打包参数为一个字典,并用 字典推导式(Dictionary Comprehensions) 来处理这个字典。现在我们用这个来创建处理时间戳的正则式:

1
2
3
4
5
6
7
rx_letters = r'[A-z]+'
rx_patterns = named_regexes(
month=rx_letters, # 任意字符
day=r'\d{1,2}', # 1 或 2 个数字
time=r'\d{2}:\d{2}:\d{2}', # 用':'分开的 3 对数字
year=r'\d{4}' # 4 个数字
)

来看看它是什么样的:

1
2
3
from pprint import pprint

pprint(rx_patterns)
1
2
3
4
{'day': '(?P<day>\d{1,2})',
'month': '(?P<month>[A-z]+)',
'time': '(?P<time>\d{2}:\d{2}:\d{2})',
'year': '(?P<year>\d{4})'}

这不是很可读,但关键是使用它。例如,让我们考虑以下时间戳:

1
timestamp = "Date: Apr 12 09:51:23 2015 -0500"

我们可以用刚刚生成的正则式字典,来格式化字符串,来捕获我们需要的数据:

1
2
rx_timestamp = "Date: {month} {day} {time} {year}".format(**rx_patterns)
print re.search(rx_timestamp, timestamp).groupdict()
1
{'month': 'Apr', 'year': '2015', 'day': '12', 'time': '09:51:23'}

成功了!我们提取出了方便使用的目标数据。


包装成一个函数(Putting it all together)

让我们把这些包装成一个函数,该函数输入需要解析的数据、一个模板字符串、一些命名及正则表达式的组合,并按字典形式返回我们需要的数据:

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
def match_regex_template(string, template, **keys_and_patterns):
"""Return dictionary of matches.

Parameters
----------
string : str
包含所需的数据的字符串。
template : str
包含命名字段的模板字符串。
keys_and_patterns : str
模板字符串中各个字段的正则式。
"""

named_patterns = named_regexes(**keys_and_patterns)
pattern = template.format(**named_patterns)

match = re.search(pattern, string)
if match is None:
raise RuntimeError(error_message.format(string=string,
template=template,
pattern=pattern))
return match.groupdict()

error_message = """
string: {string}
template: {template}
pattern: {pattern}
"""

这个函数使用了上面的几个函数。在写正则表达式时难免也会写错,所以这个函数也包含了错误处理,以帮助调试。

为了测试这个函数,我们可以这样做:

首先,我们需要一个模板字符串,加上一些数据,并产生一个测试用的目标字符串:

1
2
3
4
greeting_template = "Hey {name}! Welcome to {site}!"
input_attrs = dict(name='you', site='tonysyu.github.io')
greeting = greeting_template.format(**input_attrs)
print greeting
1
Hey you! Welcome to tonysyu.github.io!

然后我们就可以用模板字符串和目标字符串来试试 match_regex_template 函数能不能解析出我们要的数据了:

1
2
3
4
5
6
rx_anything = '.+'
attrs = match_regex_template(greeting,
greeting_template,
name=rx_anything,
site=rx_anything)
print attrs
1
{'name': 'you', 'site': 'tonysyu.github.io'}

成功了!


注意事项(Caveats)

虽然这个函数测试正常,但你要小心。里面有个非常懒惰的正则表达式:rx_anything,它能捕获,额,任何东西。如果有明显的数据边界,那么这并不是一个问题。但如果边界模糊一点,那么你就必须动动脑筋来解决这个问题了。例如,我们可以修改上面的 greeting 让它热情点:

1
2
excited_greeting = greeting + '!!!'
print excited_greeting
1
Hello you! Welcome to tonysyu.github.io!!!!

使用和上面同样的方法,是这样的:

1
2
3
4
5
attrs = match_regex_template(excited_greeting,
greeting_template,
name=rx_anything,
site=rx_anything)
print attrs['site']
1
tonysyu.github.io!!!

这个热情有点过头了。为了取到所需数据,我们需要更加严格的把惊叹号排除出去:

1
2
3
4
5
6
rx_site = '[^!]+'  # anything other than '!'
attrs = match_regex_template(excited_greeting,
greeting_template,
name=rx_anything,
site=rx_site)
print attrs['site']
1
tonysyu.github.io

通过这个小小的改动,你应该知道要注意什么了。

正则表达式是出了名的混淆,但如果你仔细地把它拆解成带标识的正则式,也可以变得很有可读性的。
这小小的习惯会让 “未来的你” 少恨 “以前的你” 一点。


(文章到这就翻译完了~)
(发上去才发现我的博客好丑,特别是代码展示 TAT,如果无法忍受可以 到 Github 上看。)


X