今天我们来说说正则表达式。说到正则表达式,自己切身的感受是,很多情况下,自己其实在逃避使用正则表达式,进而会使用split,rsplit,if等方法去分割和提起想要的字符。
究其原因,总结下来,可能有两方面,
- 自己没能很好的将各种正则表达式的规则和特殊字符记忆下来。
- 使用正则表达式初期,经常需要一些尝试和探索后,才能实现想要的效果。但在工作中常常急于实现目标,所以转而用其他看似更简便的方法去实现。
回顾过往,这样的逃避,最终都带来了更多的软件issue,所付出的时间和精力远远大于研究正则表达式所需要的时间。所以,建议学习Python的你,认真的去学习正则表达式并勤加实践。
我在这里提供几篇学习正则的好文章和工具,供大家参考:
- Python 官方的正则表达式文档:
https://docs.python.org/zh-cn/3/library/re.html - Python正则表达式指南
https://www.cnblogs.com/stephenmc/p/5981517.html - 正则表达式的生成和验证神器(针对JS,但基本规则也适用于Python,可免去记忆部分特殊符号)
https://regex-vis.com/
接下来我通过一个实例,来解释正则表达式在自己工作中的应用。
首先,以下字符串是两条同事手写的测试用例的预期结果。由于是手写的用例,所以在格式上并不十分规范。我们最终希望提取以下两个字符串中的四个参数及值,并按规定的键名存放在字典中,如:{"Freq":"750.0", "Times":"5","Cycle":"1.0", "Duty"="80.0"}
手写case实例:
step1="Freq=750.0, TIMES= 5, cycle = 1.0 duty = 80.0"
step2="Times= 5, Freq=700.0, cyc = 1.0 duty = 80.0"
接下去,我们分析下这两个字符串,从内容看,这些参数本应该用逗号隔开的,但是由于人为书写的失误,cycle和duty之间并没有加上逗号,只是用空格隔开。
其次每个参数名的大小写及=号前后的空格也不统一,参数的顺序也不一致,最可怕的是,其中有个参数cycle,用户还简写成了cyc。
经过分析,我们发现单纯用split + if的方法,其实是很难完美的解析出这些不规范的参数。那么正则表达式是否可以哪?答案是肯定的。
接着,我来说下实现的思路:
- 首先,我们发现每一个参数在格式上还是有规律可寻的,都逃不出
XXX = YYY + 逗号或空格或结尾符
的样式
那么我们先针对freq这个参数,编写正则表达式。
re_freq = r"freq\s*=\s*[0-9.]+(,|$|\s)"
这里,简单解释下其中特殊字符的含义。
freq
代表需要在字符串中匹配freq 一次,
\s*
代表匹配空格0次或多次,
[0-9.]+
代表 匹配字符0到9 以及字符.
中的任意字符 一次或多次
(,|$|\s)
代表 匹配,
或 空格 或 字符串结束符 中任意一个字符 一次
这里简单说明下为何既用了方括号又用了圆括号,因为方括号中是不支持特殊含义的字符的。比如$在方括号中仅仅是个美元符号,而不是结束符。
同时,这里提下我们学习正则表达式的好帮手 regex-vis.com,下图是regex-vis.com中解析此表达式的结果,通过图例我们可以更直观的了解到此表达式的判断逻辑和匹配的次数。 regex-vis.com除了支持解析表达式之外,也支持在网页端生成正则表达式。
freq.png
- 随后我们发现,cycle这个参数有两种书写方式,cycle和cyc,所以我们用
|
号分割了两种可能的参数名称:
re_timer = r"(cycle\s*=\s*[0-9.]+(\s|$|,)|cyc\s*=\s*[0-9.]+(,|$|\s))"
cycle.png
- 接着,我们使用
(?P<name>…)
命名组合功能,为每一个参数的正则表达式命名。 经过这里命名之后,我们就可以通过此名称,在最终的输出结果中进行查询了。
re_times = r"(?P<times>times\s*=\s*[0-9.]+(\s|$|,))"
re_freq = r"(?P<freq>freq\s*=\s*[0-9.]+(\s|$|,))"
re_cycle = r"(?P<cycle>cycle\s*=\s*[0-9.]+(\s|$|,)|cyc\s*=\s*[0-9.]+(,|$|\s))"
re_duty = r"(?P<duty>duty\s*=\s*[0-9.]+(\s|$|,))"
- 我们将这些正则表达式用
|
分割开,并作为re.finditer的正则参数,
此时,python会依次从左向右扫描字符串,并依次返回以下四个表达式的匹配对象到一个迭代器中(类似于一个list中),在迭代器中,匹配结果支持以groupdict()的形式输出(输出一个包含P<name>定义名称的字典)。
all_params={}
for m in re.finditer(f"{re_timer}|{re_freq}|{re_cycle}|{re_duty}", step1, re.I): #re.I代表忽略大小写
t=m.groupdict() # t返回一个字典,内容如:{'times': 'times= 5.9,', 'freq': None, 'cycle': None, 'duty': None}
t = clear_empty_dict(t) #自定义函数,去除空值的键 以及 提取数值部分
all_params.update(t) #合并到一个字典中
完整的代码如下:
import re
def clear_empty_dict(data):
for k in list(data.keys()):
if not data[k] or str(data[k]).strip() == "":
del data[k]
elif "=" in data[k]:
tmp = str(data[k]).rsplit("=",1)[1].replace(",","").strip()
if tmp:
data[k] =tmp
else:
del data[k]
return data
step1 = "times= 5.9, cycle = 1.0, duty = 80.0, freq=60"
step2 = "Times= 5, Freq=700.0, cyc = 1.0 duty = 80.0"
re_times = r"(?P<times>times\s*=\s*[0-9.]+(\s|$|,))"
re_freq = r"(?P<freq>freq\s*=\s*[0-9.]+(\s|$|,))"
re_cycle = r"(?P<cycle>cycle\s*=\s*[0-9.]+(\s|$|,)|cyc\s*=\s*[0-9.]+(\s|$|,))"
re_duty = r"(?P<duty>duty\s*=\s*[0-9.]+(\s|$|,))"
all_params={}
for m in re.finditer(f"{re_timer}|{re_freq}|{re_cycle}|{re_duty}", step1, re.I):
t=m.groupdict()
t = clear_empty_dict(t) #自定义函数,去除空值的键 以及 去除值中的逗号
all_params.update(t)
最终,两个字符串的参数,都顺利被提取到了字典中。从这个例子我们也能看出正则表达式的优势,即兼容性,我们可以通过正则表达式去提取,查找或删除那些不那么统一,不那么规范的字符。
{'times': '5.9', 'cycle': '1.0', 'duty': '80.0', 'freq': '60'}
{'times': '5', 'freq': '700.0', 'cycle': '1.0', 'duty': '80.0'}
网友评论