(原文出自 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 | def name_regex(name, pattern): |
为了搞清楚这个函数到底做了啥,我们传入两个字符串:
1 | print name_regex('myname', 'Tony') |
它返回:
1 | (?P<myname>Tony) |
这个字符串是个正则表达式,它所做的是:匹配目标字符串(Tony
)并将结果保存在一个命名组(myname
)中。
来个更有趣的例子吧,假设你想从一句话中提取一个价格,你可以找后面跟着数字和小数点的美元符号。
1 | # 这并不是个好的正则式,因为它会匹配任何数字和小数点,不过现在就让它这样保持简单 |
像任何其他正则式一样,你可以用 Python 的内置正则表达式包 re
来使用这个正则式:
1 | import re |
1 | $9.95 |
这的确提取我们想要的文字,但如果你只是找单一的串,用命名捕获组并没有多大用处。
用命名捕获组格式化字符串(Named regexes with string formatting)
这次我们不用单一的命名捕获组正则式了。让我们创建一个(name, pattern)
形式的正则式字典吧:
1 | def named_regexes(**names_and_patterns): |
如果你觉得这看起来有点奇怪,那只是因为我们 打包参数为一个字典,并用 字典推导式(Dictionary Comprehensions) 来处理这个字典。现在我们用这个来创建处理时间戳的正则式:
1 | rx_letters = r'[A-z]+' |
来看看它是什么样的:
1 | from pprint import pprint |
1 | {'day': '(?P<day>\d{1,2})', |
这不是很可读,但关键是使用它。例如,让我们考虑以下时间戳:
1 | timestamp = "Date: Apr 12 09:51:23 2015 -0500" |
我们可以用刚刚生成的正则式字典,来格式化字符串,来捕获我们需要的数据:
1 | rx_timestamp = "Date: {month} {day} {time} {year}".format(**rx_patterns) |
1 | {'month': 'Apr', 'year': '2015', 'day': '12', 'time': '09:51:23'} |
成功了!我们提取出了方便使用的目标数据。
包装成一个函数(Putting it all together)
让我们把这些包装成一个函数,该函数输入需要解析的数据、一个模板字符串、一些命名及正则表达式的组合,并按字典形式返回我们需要的数据:
1 | def match_regex_template(string, template, **keys_and_patterns): |
这个函数使用了上面的几个函数。在写正则表达式时难免也会写错,所以这个函数也包含了错误处理,以帮助调试。
为了测试这个函数,我们可以这样做:
首先,我们需要一个模板字符串,加上一些数据,并产生一个测试用的目标字符串:
1 | greeting_template = "Hey {name}! Welcome to {site}!" |
1 | Hey you! Welcome to tonysyu.github.io! |
然后我们就可以用模板字符串和目标字符串来试试 match_regex_template
函数能不能解析出我们要的数据了:
1 | rx_anything = '.+' |
1 | {'name': 'you', 'site': 'tonysyu.github.io'} |
成功了!
注意事项(Caveats)
虽然这个函数测试正常,但你要小心。里面有个非常懒惰的正则表达式:rx_anything
,它能捕获,额,任何东西。如果有明显的数据边界,那么这并不是一个问题。但如果边界模糊一点,那么你就必须动动脑筋来解决这个问题了。例如,我们可以修改上面的 greeting
让它热情点:
1 | excited_greeting = greeting + '!!!' |
1 | Hello you! Welcome to tonysyu.github.io!!!! |
使用和上面同样的方法,是这样的:
1 | attrs = match_regex_template(excited_greeting, |
1 | tonysyu.github.io!!! |
这个热情有点过头了。为了取到所需数据,我们需要更加严格的把惊叹号排除出去:
1 | rx_site = '[^!]+' # anything other than '!' |
1 | tonysyu.github.io |
通过这个小小的改动,你应该知道要注意什么了。
正则表达式是出了名的混淆,但如果你仔细地把它拆解成带标识的正则式,也可以变得很有可读性的。
这小小的习惯会让 “未来的你” 少恨 “以前的你” 一点。
(文章到这就翻译完了~)
(发上去才发现我的博客好丑,特别是代码展示 TAT,如果无法忍受可以 到 Github 上看。)