在网络运维中我们会接触很多需要解析的设备配置,之前我们简单介绍了用字符串简单解析。这种方法比较简单,有时候实现一些解析的时候会非常麻烦。今天我们讲解一下正则表达式,可以快速的解析文本提取出我们想要的数据。
在NetDevOps众多技能中,我觉得正则表达式是十分重要的一个技能,它可以让你非常方便的从解析文本这个角度入门网络运维开发。
SDN毕竟没有全面铺开,同时SDN也有一些数据是Netconf及REST API无法提供的。从网工熟悉的CLI作为切入点,更容易理解,见到成效。
数据是后续一切网络运维开发的起点,有了数据我们可以做很多事情,比如做基线检查、配置比对、设备一键检查、监控告警、根据现网的配置和状态生成新的配置、梳理资源,数不胜数。
提取数据很直接的一个方法就是Netmiko去采集,正则来解析,最后数据可以直接使用或者写入文件(其实最终目标是数据仓库),根据运维场景数据消费。
我们讲的很多东西就都可以串起来了。
今天讲的是基于Python的re模块去实现文本的解析,后续我们还会继续分享一个比正则更牛的Python包。但在这之前,我希望大家看完这个文章掌握正则最基本的能力。
正则表达式
什么是正则表达式
又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。
许多程序设计语言都支持利用正则表达式进行字符串操作。正则表达式这个概念最初是由Unix中的工具软件(例如sed和grep)普及开的。正则表达式通常缩写成“regex”,单数有regexp、regex,复数有regexps、regexes、regexen 。代码简写regex、regexp或RE
简单点理解,解析文本的引擎
作用
给定一个正则表达式和另一个字符串,我们可以达到如下的目的:
给定的字符串是否符合正则表达式的过滤逻辑(称作“匹配”)
可以通过正则表达式,从字符串中获取我们想要的特定部分
比如判断一个字符串是否是IP地址
抽取出端口的一些基本信息
组成
正则表达式由一些普通字符和一些元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义。
普通字符串,包括字母和数字
元字符,如下
因为正则表达式也是用字符串表示的,所以,我们要首先了解如何用字符来描述字符。正则表达式语法:
a|b 匹配 a 或 bgr(a|e)y 匹配 gray 或 grey. 匹配任一字符[abc] 匹配任一字符: a 或 b 或 c[^abc] 匹配任一字符, 但不包括 a, b, c[a-z] 匹配从 a 到 z 之间的任一字符[a-zA-Z] 匹配从 a 到 z, 及从 A 到 Z 之间的任一字符^ 匹配文件名的头部$ 匹配文件名的尾部( ) 匹配标记的子表达式\n 匹配第 nth 个标记的子表达式, nth 代表 1 到 9\b 匹配字词边界* 匹配前一项内容 0 或多次? 匹配前一项内容 0 或 1 次+ 匹配前一项内容 1 或多次*? 匹配前一项内容 0 或多次 (懒人模式)+? 匹配前一项内容 1 或多次 (懒人模式){x} 匹配前一项内容 x 次{x,} 匹配前一项内容 x 或多次{x,y} 匹配前一项内容次数介于 x 和 y 之间\ 特殊转义字符
在正则表达式中,如果直接给出字符,就是精确匹配。用\d
可以匹配一个数字,\w
可以匹配一个字母或数字,所以:
'00\d'
可以匹配'007'
,但无法匹配'00A'
;'\d\d\d'
可以匹配'010'
;'\w\w\d'
可以匹配'py3'
;
.
可以匹配任意字符,所以:
'py.'
可以匹配'pyc'
、'pyo'
、'py!'
等等。
要匹配变长的字符,在正则表达式中,用*
表示任意个字符(包括0个),用+
表示至少一个字符,用?
表示0个或1个字符,用{n}
表示n个字符,用{n,m}
表示n-m个字符:
来看一个复杂的例子:\d{3}\s+\d{3,8}
。
我们来从左到右解读一下:
\d{3}
表示匹配3个数字,例如'010'
;\s
可以匹配一个空格(也包括Tab等空白符),所以\s+
表示至少有一个空格,例如匹配' '
,' '
等;\d{3,8}
表示3-8个数字,例如'1234567'
。
综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。
如果要匹配'010-12345'
这样的号码呢?由于'-'
是特殊字符,在正则表达式中,要用'\'
转义,所以,上面的正则是\d{3}\-\d{3,8}
。
但是,仍然无法匹配'010 - 12345'
,因为带有空格。所以我们需要更复杂的匹配方式。
进阶
要做更精确地匹配,可以用[]
表示范围,比如:
[0-9a-zA-Z\_]
可以匹配一个数字、字母或者下划线;[0-9a-zA-Z\_]+
可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100'
,'0_Z'
,'Py3000'
等等;[a-zA-Z\_][0-9a-zA-Z\_]*
可以匹配由字母或下划线开头,后接任意个由一个数字、字母或者下划线组成的字符串,也就是Python合法的变量;[a-zA-Z\_][0-9a-zA-Z\_]{0, 19}
更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。
A|B
可以匹配A或B,所以[P|p]ython
可以匹配'Python'
或者'python'
。
^
表示行的开头,^\d
表示必须以数字开头。
$
表示行的结束,\d$
表示必须以数字结束。
你可能注意到了,py
也可以匹配'python'
,但是加上^py$
就变成了整行匹配,就只能匹配'py'
了。
python的re模块
有了准备知识,我们就可以在Python中使用正则表达式了。Python提供re
模块,包含所有正则表达式的功能。由于Python的字符串本身也用\
转义,所以要特别注意:
s = 'ABC\\-001' # Python的字符串# 对应的正则表达式字符串变成:# 'ABC\-001'
因此我们强烈建议使用Python的r
前缀,就不用考虑转义的问题了:
s = r'ABC\-001' # Python的字符串# 对应的正则表达式字符串不变:# 'ABC\-001'
匹配
match
re.match(pattern, string[, flags])
从首字母开始开始匹配,string如果包含pattern子串,则匹配成功,返回Match对象,失败则返回None,若要完全匹配,pattern要以$结尾
search
re.search(pattern, string[, flags])
若string中包含pattern子串,则返回Match对象,否则返回None,注意,如果string中存在多个pattern子串,只返回第一个。
findall
re.findall(pattern, string[, flags])
返回string中所有与pattern相匹配的全部字串,返回形式为数组。
finditer
re.finditer(pattern, string[, flags])
返回string中所有与pattern相匹配的全部字串,返回形式为迭代器。
区别若匹配成功,match()/search()返回的是Match对象,finditer()返回的也是Match对象的迭代器,获取匹配结果需要调用Match对象的group()、groups或group(index)方法group():母串中与模式pattern匹配的子串;group(0):结果与group()一样;groups():所有group组成的一个元组,group(1)是与patttern中第一个group匹配成功的子串,group(2)是第二个,依次类推,如果index超了边界,抛出IndexError;findall():返回的就是所有groups的数组,就是group组成的元组的数组,母串中的这一撮组成一个元组,那一措组成一个元组,这些元组共同构成一个list,就是findall()的返回结果。另,如果groups是只有一个元素的元组,findall的返回结果是子串的list,而不是元组的list了。
以match search为例
先看看如何判断正则表达式是否匹配:
import reintf_re = r'Ethernet(\d+)/(\d+)'def test_match(my_str): intf_match = re.match(intf_re, my_str) if intf_match: slot = intf_match.group(1) index = intf_match.group(2) print('re match :slot is {}, index is {}'.format(slot, index)) print('re match :groups is {}'.format(intf_match.groups())) else: print('re match:No intf info')def test_search(my_str): intf_match = re.search(intf_re, my_str) if intf_match: slot = intf_match.group(1) index = intf_match.group(2) print('re search:slot is {}, index is {}'.format(slot, index)) else: print('re serach: No intf info')if __name__ == '__main__': # 将my_str赋予不同的值分别测试这段代码来看看吧 my_str = 'Ethernet4/5' test_match(my_str) test_search(my_str) my_str = 'intferface Ethernet4/5' test_match(my_str) test_search(my_str)
split 切分字符串
用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:
>>> 'a b c'.split(' ')['a', 'b', '', '', 'c']
嗯,无法识别连续的空格,用正则表达式试试:
>>> re.split(r'\s+', 'a b c')['a', 'b', 'c']
无论多少个空格都可以正常分割。加入,
试试:
>>> re.split(r'[\s\,]+', 'a,b, c d')['a', 'b', 'c', 'd']
再加入;
试试:
>>> re.split(r'[\s\,\;]+', 'a,b;; c d')['a', 'b', 'c', 'd']
如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。
group 分组
除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()
表示的就是要提取的分组(Group)。比如:
^(\d{3})-(\d{3,8})$
分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:
>>> m = re.match(r'^(\d{3})-(\d{3,8})$', '010-12345')>>> m<_sre.SRE_Match object; span=(0, 9), match='010-12345'>>>> m.group(0)'010-12345'>>> m.group(1)'010'>>> m.group(2)'12345'
如果正则表达式中定义了组,就可以在Match
对象上用group()
方法提取出子串来。
注意到group(0)
永远是原始字符串,group(1)
、group(2)
……表示第1、2、……个子串。
提取子串非常有用。来看一个更凶残的例子:
>>> t = '19:05:30'>>> m = re.match(r'^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$', t)>>> m.groups()('19', '05', '30')
这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:
'^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$'
对于'2-30'
,'4-31'
这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。
贪婪匹配
最后需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0
:
>>> re.match(r'^(\d+)(0*)$', '102300').groups()('102300', '')
由于\d+
采用贪婪匹配,直接把后面的0
全部匹配了,结果0*
只能匹配空字符串了。
必须让\d+
采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0
匹配出来,加个?
就可以让\d+
采用非贪婪匹配:
>>> re.match(r'^(\d+?)(0*)$', '102300').groups()('1023', '00')
编译
当我们在Python中使用正则表达式时,re模块内部会干两件事情:
编译正则表达式,如果正则表达式的字符串本身不合法,会报错;
用编译后的正则表达式去匹配字符串。
如果一个正则表达式要重复使用几千次,出于效率的考虑,我们可以预编译该正则表达式,接下来重复使用时就不需要编译这个步骤了,直接匹配:
>>> import re# 编译:>>> re_telephone = re.compile(r'^(\d{3})-(\d{3,8})$')# 使用:>>> re_telephone.match('010-12345').groups()('010', '12345')>>> re_telephone.match('010-8086').groups()('010', '8086')
编译后生成Regular Expression对象,由于该对象自己包含了正则表达式,所以调用对应的方法时不用给出正则字符串。
小结
正则表达式非常强大,要在短短的一节里讲完是不可能的。要讲清楚正则的所有内容,可以写一本厚厚的书了。如果你经常遇到正则表达式的问题,你可能需要一本正则表达式的参考书。
动手写一个正则
判断一个单词是否是a开头b结尾。
使用到了python中的re模块,match函数,match的group函数
# 引入正则引擎模块import re# 编译正则表达式,形成一个规则验证的patternmy_pattern = re.compile(r'^a.*b$')# 定义两个字符串a = 'acbc'b = 'accb'a_match = my_pattern.match(a)b_match = my_pattern.match(b)if a_match: print(a_match.group())else: print('a no match')if b_match: # match.group()会返回整个符合规则的字符串;group中可加参数0 ,0 即默认整个匹配的部分 print(b_match.group()) print(b_match.group(0))else: print('b no match')################找出字符串中符合要求的部分a = 'acbccaab'b = 'accbabavb'a_match = my_pattern.search(a)b_match = my_pattern.search(b)if a_match: print(a_match.group())else: print('a no find')if b_match: # match.group()会返回整个符合规则的字符串;group中可加参数0 ,0 即默认整个匹配的部分 print(b_match.group()) print(b_match.group(0))else: print('b no find')print('## 贪婪(默认) 非贪婪(加“?”)')my_pattern = re.compile(r'a.*?b')a = 'acbccaab'b = 'accbabavb'a_match = my_pattern.search(a)b_match = my_pattern.search(b)if a_match: print(a_match.group())else: print('a no find')if b_match: # match.group()会返回整个符合规则的字符串;group中可加参数0 ,0 即默认整个匹配的部分 print(b_match.group())else: print('b no find')print('## 发现多个')my_pattern = re.compile(r'a.*?b')a = 'ababababababb'a_list = my_pattern.findall(a)for a_item in a_list: print(a_item,'\n')
后续我们会继续分享一些解析常见类型设备的脚本,供大家参考!
欢迎大家订阅、分享、点击在看!