感谢知乎大佬:@弈心
本文是基于@弈心大佬(王印)的书籍《网络工程师的python之路》所整理的笔记
1.条件(判断语句)
在Python中,条件语句(Conditional Statements)又叫作判断语句,判断语句由if、 elif和else 3种语句组成,其中if为强制语句,可以独立使用,elif和else为可选语句,并且不能独立使用。判断语句配合布尔值,通过判断一条或多条语句的条件是否成立(True或者False),从而决定下一步的动作,如果判断条件成立(True),则执行if或elif语句下的代码;如果判断条件不成立(False),则执行else语句下的代码;如果没有else语句,则不做任何事情。 布尔值是判断语句不可或缺的部分,在基本语法中讲到的比较运算符、逻辑运算符,以及字符串自带的startswith()、endswith()、isdigit()、isalpha()等方法,还有下面将会讲到的成员运算符等都会返回布尔值。下面就举例讲解它们各自在Python判断语句中的应用场景。
1.1通过比较运算符作判断
在讲布尔类型时,我们已经提到与布尔值True和False息息相关的各种比较运算符,包括等于号==、不等于号!=、大于号>、小于号<、大于等于号>=和小于等于号<=,因为使用比较运算符后会直接返回布尔值,所以比较运算符在判断语句中会经常被用到,举例如下。 首先我们用脚本模式写一段代码。
score=input('请输入你hcie开始分数:')
if int(score)>600:
print('恭喜你通过考试!')
elif int(score)==600:
print('恭喜你压线通过考试!')
else:
print('未能通过考试!')
写在if、elif和else下的代码都做了代码缩进(Indentation),也就是print()函数的前面保留了4个空格。不同于C、C++、Java等语言,Python要求严格的代码缩进,目的是让代码工整并且具有可读性,方便阅读和修改。缩进不一定必须是4个空格,两个空格或者8个空格都是允许的,目前最常见的是4个空格的缩进。
• if、elif和else语句的结尾必须接冒号,这点需要注意。
• 使用input()函数让用户输入自己的分数,并把它赋值给score变量。input()函数的返回值是字符串,因为要与600这个整数做比较,所以需要通过int()函数先将score从字符串转换为整数。
• 与if和elif语句不同,else后面不需要再给任何判断条件。
1.2通过字符串方法+逻辑运算符作判断
当使用input()函数让用户输入内容时,你无法保证用户输入的内容合乎规范。
比如你给用户6个选项,每个选项分别对应一个动态路由协议的名称(选项1:RIP;选项2:IGRP;选项3:EIGRP;选项4:OSPF;选项5:ISIS;选项6:BGP),提示用户输入路由协议的选项号码来查询该路由协议的类型,然后让Python根据用户输入的选项号码告诉用户该路由协议属于链路状态路由协议、距离矢量路由协议,还是路径适量路由协议。
这里你无法保证用户输入的肯定是整数,即使用户输入的是整数,也无法保证输入的是1~6的数字。
因为input()函数的返回值是字符串,所以可以首先使用字符串的isdigit()函数来判断用户输入的内容是否为整数,这是判断条件之一。然后通过int()将该字符串数字转换成整数,继续判断该整数是否介于1和6之间(包含1和6),这是判断条件之二。再将这两个判断条件通过逻辑运算符and来判断它俩是否同时成立。如果成立,则返回相应的答案;如果不成立,则提示用户输入的内容不符合规范并终止程序。代码如下:
print('''请根据对应的的号码选择一个路由协议:
1.RIP
2.IGRP
3.EIGRP
4.OSPF
5.ISIS
6.BGP ''')
option=input('请输入你的选项(数字1—6):')
if option.isdigit() and 1 <=int(option)<= 6:
if option== '1' or option== '2' or option== '3':
print('该路由协议属于距离矢量路由协议')
elif option== '4' or option== '5':
print('该路由协议属于链路状态路由协议')
else:
print('该路由协议属于路径矢量路由协议')
else:
print('选项无效,程序终止。')
1.3 通过成员运算符作判断
成员运算符用于判断是否可以在给定的一组字符串、列表、字典和元组中找到一个给定的值或变量,如果能找到,则返回布尔值True;如果找不到,则返回布尔值False。成员运算符有两种:in和not in,举例如下。
>>>netdevops='网络工程师需要学python吗?'
>>> 'python'in netdevops
True
>>> 'java'not in netdevops
True
依靠成员运算符,我们还可以将类似上一节的脚本简化。上一节的脚本给出的选项只有6种,我们尚且能够使用if option == '1' or option == '2' or option == '3':此类的方法来列举所需的选项,但是如果所需选项超过20个甚至上百个,那么再使用这种方法岂不是太笨了?这时可以使用range()函数配合list()函数创造一个整数列表,然后配合成员运算符来判断用户输入的选项号码是否存在于该整数列表中。 按照这种思路,我们将上一节的脚本做如下简化。
print('''请根据对应的的号码选择一个路由协议:
1.RIP
2.IGRP
3.EIGRP
4.OSPF
5.ISIS
6.BGP ''')
option=input('请输入你的选项(数字1—6):')
if option.isdigit() and int(option) in list(range(1,7)):
if int(option) in list(range(1,4)):
print('该路由协议属于距离矢量路由协议')
elif int(option) in list(range(4,6)):
print('该路由协议属于链路状态路由协议')
else:
print('该路由协议属于路径矢量路由协议')
else:
print('选项无效,程序终止。')
需要注意的是,由于input()函数返回的是字符串,因此需要把变量option先通过int()函数转换为整数才能使用成员运算符in来判断它是否存在于range()和list()函数所创建的整数列表中。
2.循环语句
Python中最常用的循环语句(Looping Statements)有两种:while和for。除此之外,还有文件迭代器(File Iterator)、列表解析式(List Comprehension)等循环工具,不过对于网络工程师来说,用得最多的还是while和for,因此本节将只讲解这两种循环语句。
2.1while语句
在Python中,while语句用于循环执行一段程序,它和if语句一样,两者都离不开判断语句和缩进。每当写在while语句下的程序被执行一次,程序就会自动回到“顶上”(也就是while语句的开头部分),根据while后的判断语句的返回值来决定是否要再次执行该程序,如果判断语句的返回值为True,则继续执行该程序,一旦判断语句的返回值为False,则该while循环随即终止,如此反复。如果需要中途强行中止while循环,则需要使用break语句。
◎ 例1
a=1
b=10
while a < b:
print(a)
a=a+1
输出结果:
1
2
3
4
5
6
7
8
9
上面例子中,一旦用户输入的选项不符合规范,程序就会立即中止,用户必须再次手动运行一次脚本重新输入选项,这样显得很笨拙。借助while循环,可以不断地重复执行input()函数,直到用户输入正确的选项号码。优化后的脚本代码如下。
print('''请根据对应的的号码选择一个路由协议:
1.RIP
2.IGRP
3.EIGRP
4.OSPF
5.ISIS
6.BGP ''')
while True:
option=input('请输入你的选项(数字1—6):')
if option.isdigit() and int(option) in list(range(1,7)):
if int(option) in list(range(1,4)):
print('该路由协议属于距离矢量路由协议')
elif int(option) in list(range(4,6)):
print('该路由协议属于链路状态路由协议')
else:
print('该路由协议属于路径矢量路由协议')
break
else:
print('选项无效,重新输入。')
while True是一种很常见的while循环的用法,因为这里的判定条件的结果已经手动指定了True,意味着判定条件将永久成立,也就意味着while下面的程序将会被无数次重复执行,从而引起“无限循环”(Indefinite Loop)的问题。为了避免无限循环,我们必须在程序代码中使用break语句来终止while循环,注意break在上面代码里的位置。
2.2for语句
同为循环语句,for语句的循环机制和while语句完全不同:while语句需要配合判断语句来决定什么时候开始循环和中止循环,而for语句则用来遍历一组可迭代的序列,可迭代的序列包括字符串、列表、元组等。在将这些序列中的元素遍历完后,for语句的循环也随即终止。for语句的基本语法格式如下。 这里的sequence为可迭代的序列(如字符串、列表、元组),而item可以理解为该序列里的每个元素(item名称可以任意选取),statements则是循环体(将要循环的程序部分)。 举例如下。
◎ 例1
for words in 'python':
print(words)
结果:
p
y
t
h
o
n
我们用letter作为for语句中的item来遍历字符串“Python”,并将该字符串中的元素依次全部打印出来,得到P、y、t、h、o、n。
◎ 例2
sum=0
for number in range(1,6):
sum=sum+number
print(sum)
结果:
1
3
6
10
15
◎ 例3
routing_protocols=['RIP','IGRP','EIGRP','OSPF','ISIS','BGP']
link_protocols=['OSPF','ISIS']
for protocols in routing_protocols:
if protocols in link_protocols:
print(protocols+'属于链路状态协议')
else:
print(protocols+'不属于链路状态协议')
结果:
RIP不属于链路状态协议
IGRP不属于链路状态协议
EIGRP不属于链路状态协议
OSPF属于链路状态协议
ISIS属于链路状态协议
BGP不属于链路状态协议
我们分别创建两个列表:routing_protocols和link_state_protocols,列表中的元素是对应的路由协议和链路状态路由协议。首先用protocols作为for语句中的item来遍历第一个列表routing_protocols,然后使用if语句来判断哪些protocols不属于第二个列表link_state_protocols中的元素,并将它们打印出来。
正如前面讲到的,上述三个例子中写在for后面的letter、number和protocols代表将要遍历的可迭代序列里的每一个元素(即item名称),它们的名称可以由用户随意制定,比如在例1中,我们把letter换成a也没问题。
3.文本文件的读/写
在日常网络运维中,网络工程师免不了要和大量的文本文件打交道,比如用来批量配置网络设备的命令模板文件,存放所有网络设备IP地址的文件,以及备份网络设备show run输出结果之类的配置备份文件。
3.1open()函数及其模式
在Python中,我们可以通过open()函数来访问和管理文本文件,open()函数用来打开一个文本文件并创建一个文件对象(File Object),通过文件对象自带的多种函数和方法,可以对文本文件执行一系列访问和管理操作。在讲解这些函数和方法之前,首先创建一个名为test.txt的测试文本文件,该文件包含5个网络设备厂商的名字,内容如下。
C:\Users\86157>cd Desktop
C:\Users\86157\Desktop>type test.txt
Cisco
Juniper
Arista
H3C
然后用open()函数访问该文件。
file=open('test.txt','r')
我们通过open()函数的r模式(只读)访问test.txt文件,并返回一个文件对象,再将该文件对象赋值给file变量。r(reading)是默认访问模式,除此之外,open()函数还有很多其他文件访问模式。这里只介绍网络工程师最常用的几种模式,如下表所示。
网络工程师必须熟练掌握open()函数的上述6种模式,关于它们的具体使用将在下一节中举例讲解。
3.2文件读取
在使用open()函数创建文件对象之后,我们并不能马上读取文件里的内容。如下所示,在创建了文件对象并将它赋值给file变量后,如果用print()函数将file变量打印出来,则只会得到文件名称、open()函数的访问模式及该文件对象在内存中的位置等信息。
file=open('test.txt','r')
print(file)
============= RESTART: C:\Users\86157\Desktop\split()and join().py =============
<_io.TextIOWrapper name='test.txt' mode='r' encoding='cp936'>
要想读取文件里的具体内容,我们还需要用read()、readline()或者readlines() 3种方法中的一种。因为这3种方法都和读取有关,因此open()函数中只允许写入的w模式和只允许追加的a模式不支持它们,而其他4种模式则都没有问题。
read()、readline()和readlines()是学习open()函数的重点内容,三者的用法和差异很大,其中readlines()更是重中之重(原因后面会讲到),网络工程师必须熟练掌握。下面对这3种函数一一进行讲解。
3.2.1read()
read()方法读取文本文件里的全部内容,返回值为字符串。
file=open('test.txt','r')
print(file.read())
print(file.read())
结果
Cisco
Juniper
Arista
H3C
Huawei
我们尝试连续两次打印test.txt文件的内容,第一次打印出的内容没有任何问题,为什么第二次打印的时候内容为空了呢?这是因为在使用read()方法后,文件指针的位置从文件的开头移动到了末尾,要想让文件指针回到开头,必须使用seek()函数,方法如下。
>>>file.seek(0))
>>>file.tell()
0
>>>print(file.read())
Cisco
Juniper
Arista
H3C
Huawei
>>>file.tell()
32
我们用seek(0)将文件指针从末尾移回开头,并且用tell()方法确认文件指针的位置(文件开头的位置为0),随后使用read()方法打印文件内容并成功,之后再次使用tell()方法确认文件指针的位置,可以发现指针现在已经来到文件末尾处(32)。这个32是怎么得来的?下面我们去掉print()函数,再次通过read()方法来读取一次文件内容。
>>>file.seek(0))
0
>>>file.read())
Cisco\nJuniper\nArista\nH3C\nHuawei\n
去掉print()函数的目的是能清楚地看到换行符\n,如果这时从左往右数,则会发现Cisco(5) + \n(1) + Juniper(7) + \n(1) + Arista(6) + \n(1) + H3C(3) + \n(1) + Huawei(6) + \n(1)= 32,这就解释了为什么在文件指针移动到文件末尾后,tell()方法返回的文件指针的位置是32。文件指针的位置及seek()和tell()方法的用法是文本文件访问和管理中很重要但又容易被忽略的知识点,网络工程师务必熟练掌握。
3.2.2readline()
readline()与read()的区别是它不会像read()那样把文本文件的所有内容一次性都读完,而是会一排一排地去读。readline()的返回值也是字符串。举例如下。
>>>file=open('test.txt','r')
>>>print(file.readline())
Cisco
>>>print(file.readline())
Juniper
>>>print(file.readline())
Arista
>>>print(file.readline())
H3C
>>>print(file.readline())
Huawei
>>>print(file.readline())
>>>
readline()方法每次返回文件的一排内容,顺序由上至下(这里出现的空排部分是因为换行符的缘故)。另外,文件指针会跟随移动直到文件末尾,因此最后一个print(file.readline())的返回值为空。
3.2.3readlines()
readlines()与前两者最大的区别是它的返回值不再是字符串,而是列表。可以说readlines()是read()和readline()的结合体,首先它同read()一样把文本文件的所有内容都读完。另外,它会像readline()那样一排一排地去读,并将每排的内容以列表元素的形式返回,举例如下。
file=open('test.txt')
print(file.readlines())
file.seek(0)
devices=file.readlines()
print(devices[0])
print(devices[1])
print(devices[2])
print(devices[3])
print(devices[4])
结果:
['Cisco\n', 'Juniper\n', 'Arista\n', 'H3C\n', 'Huawei']
Cisco
Juniper
Arista
H3C
Huawei
同read()和readline()一样,使用一次readlines()后,文件指针会移动到文件的末尾。为了避免每次重复使用seek(0)的麻烦,可以将readlines()返回的列表赋值给一个变量,即devices,之后便可以使用索引号来一个一个地验证列表里的元素。注意readlines()返回的列表里的元素都带换行符\n,这与我们手动创建的普通列表是有区别的。
同read()和readline()相比,笔者认为readlines()应该是网络运维中使用频率最高的一种读取文本文件内容的方法,因为它的返回值是列表,通过列表我们可以做很多事情。举个例子,现在有一个名为ip.txt的文本文件,该文件保存了一大堆没有规律可循的交换机的管理IP地址,具体如下。
C:\Users\86157\Desktop>type ip.txt
172.16.100.1
172.16.100.2
172.16.100.3
172.16.100.4
172.16.100.5
172.16.100.6
172.16.100.7
172.16.100.8
172.16.100.9
172.16.100.10
192.168.230.29
192.168.32.1
192.168.2.1
10.3.2.1
10.58.23.3
192.168.230.69
10.235.21.42
192.168.32.32
10.4.3.3
172.16.100.32
172.16.100.13
172.16.100.16
现在需要回答3个问题: (1)怎么使用Python来确定该文件有多少个IP地址? (2)怎么使用Python来找出该文件中的B类IP地址(172.16开头的IP地址),将它们打印出来并统计个数。
答案(1):最好的方法是使用open()函数的readlines()来读取该文件的内容,因为readlines()的返回值是列表,可以方便我们使用len()函数来判断该列表的长度,从而得到该文件中所包含IP地址的数量,举例如下。
f=open('ip.txt')
print(len(f.readlines()))
结果
22
仅仅通过两行代码,就得到了结果:22个IP地址。注意此时若文本存在一行空行,得到的长度也会相应加上1。
答案(2):最好的方法依然是使用readlines()来完成,因为readlines()返回的列表中的元素的数据类型是字符串,可以使用for循环来遍历所有的字符串元素,然后配合if语句,通过字符串的startswith()函数判断这些IP地址是否以172.16开头。如果是,则将它们打印出来,举例如下。
f=open('ip.txt')
number=0
for ip in f.readlines():
if ip.startswith('172.16'):
number=number+1
print(ip)
print('总共有'+str(number)+'个B类IP地址')
结果
172.16.100.1
172.16.100.2
172.16.100.3
172.16.100.4
172.16.100.5
172.16.100.6
172.16.100.7
172.16.100.8
172.16.100.9
172.16.100.10
172.16.100.32
172.16.100.13
172.16.100.16
总共有13个B类IP地址
print('总共有'+str(number)+'个B类IP地址'),+号不能同时连接字符和整数,故要使用str()函数将number转换成字符。
需要注意的是,因为readlines()返回的列表中的元素是带换行符\n的,所以打印出来的每个B类IP地址之间都空了一排,影响阅读和美观,解决方法也很简单,只需要使用讲过的strip()函数去掉换行符即可,举例如下。
f=open('ip.txt')
number=0
for ip in f.readlines():
if ip.startswith('172.16'):
number=number+1
print(ip.strip())
print('总共有'+str(number)+'个B类IP地址')
结果
172.16.100.1
172.16.100.2
172.16.100.3
172.16.100.4
172.16.100.5
172.16.100.6
172.16.100.7
172.16.100.8
172.16.100.9
172.16.100.10
172.16.100.32
172.16.100.13
172.16.100.16
总共有13个B类IP地址
3.3.3文件写入
在使用open()函数创建文件对象后,我们可以使用write()函数来对文件写入数据。顾名思义,既然write()函数与文件写入相关,那么只允许只读的r模式并不支持它,而其他5种模式则不受限制(包括r+模式),举例如下。 write()函数在r+、w/w+、a/a+这5种模式中的应用讲解如下。
r+
-
在r+模式下使用write()函数,新内容会添加在文件的开头部分,而且会覆盖开头部分原来已有的内容,举例如下。
#文本修改前的内容 C:\Users\86157\Desktop>type test.txt Cisco Juniper Arista H3C Huawei #在r+模式下使用write()函数修改文本内容 f=open('test.txt','r') f.write('Avaya') f.close() #文本修改后的内容 C:\Users\86157\Desktop>type test.txt Avaya Juniper Arista H3C Huawei
可以看到,文本开头的Cisco已经被Avaya覆盖了。这里注意使用write()函数对文本写入新内容后,必须再用close()方法将文本关闭,这样新写入的内容才能被保存。
2.w/w+
在w/w+模式下使用write()函数,新内容会添加在文件的开头部分,已存在的文件的内容将会完全被清空,举例如下。
#文本修改前的内容
C:\Users\86157\Desktop>type test.txt
Avaya
Juniper
Arista
H3C
Huawei
#在w模式下使用write ()函数修改文本内容
f = open ( 'test.txt ','w')
f.write ( 'test')
f.close ()
#文本修改后的内容
C:\Users\86157\Desktop>type test.txt
test
#在w+模式下使用write ()函数修改文本内容
f = open ( 'test.txt ','w+')
f.write ( '''Cisco
Juniper
Arista
H3C
Huawei ''' )
f.close ()
#文本修改后的内容
C:\Users\86157\Desktop>type test.txt
cisco
Juniper
Arista
H3C
Huawei
'w'后面注意不要有空格,不然会报错。错误示例:'w '
3.a/a+
在a/a+模式下使用write()函数,新内容会添加在文件的末尾部分,已存在的文件的内容将不会被清空,举例如下。
#文本修改前内容
C:\Users\86157\Desktop>type test.txt
Cisco
Juniper
Arista
H3C
Huawei
#在a模式使用write()函数修改文本内容
f = open ( 'test.txt ','a')
f.write ('Avaya\n')
f.close ()
#文本修改后的内容
C:\Users\86157\Desktop>type test.txt
Cisco
Juniper
Arista
H3C
Huawei
Avaya
#在a+模式使用write()函数修改文本内容
f = open ( 'test.txt ','a')
f.write ('Aruba')
f.close ()
文本修改后的内容
C:\Users\86157\Desktop>Type test.txt
Cisco
Juniper
Arista
H3C
Huawei
Avaya
Aruba
3.3.4with语句
每次用open()函数打开一个文件,该文件都将一直处于打开状态。这一点我们可以用closed方法来验证:如果文件处于打开状态,则closed方法返回False;如果文件已被关闭,则closed方法返回True。
f = open ( 'test.txt ')
print(f.closed)
f.close()
print(f.closed)
结果
False
True
这种每次都要手动关闭文件的做法略显麻烦,可以使用with语句来管理文件对象。用with语句打开的文件将被自动关闭,举例如下。
with open ( 'test.txt ') as f:
print(f.read())
print(f.closed)
结果
Cisco
Juniper
Arista
H3C
Huawei
True
这里没有使用close()来关闭文件,因为with语句已经自动将文件关闭(closed方法的返回值为True)。
4.自定义函数
函数是已经组织好的可以被重复使用的一组代码块,它的作用是用来提高代码的重复使用率。在Python中,有很多内建函数(Built-in Function),比如前面已经讲到的type()、dir()、print()、int()、str()、list()、open()等,在安装好Python后就能立即使用。除了上述内建函数,我们也可以通过创建自定义函数(User-Defined Function)来完成一些需要重复使用的代码块,提高工作效率
4.1函数的创建和调用
在Python中,我们使用def语句来自定义函数。def语句后面接函数名和括号(),括号里根据情况可带参数也可不带参数。在自定义函数创建好后,要将该函数调用才能得到函数的输出结果(即使该函数不带参数),举例如下。
#带参数的自定义函数
def add(x,y):
result=x+y
print(result)
print(add(1,2))
结果
3
None
#不带参数的自定义函数
def name():
print('renkie')
print(name())
结果
renkie
None
不管自定义函数是否带参数,函数都不能在创建前就被调用,比如下面这段用来求一个数的二次方的脚本。
4.2函数值的返回
任何函数都需要返回一个值才有意义。自定义函数可以用print和return两种语句来返回一个值,如果函数中没有使用print或return,则该函数只会返回一个空值(None),举例如下。
def add(x):
x=x+1
print(add(1))
结果
None
◎ print和return的区别 print用来将返回值打印输出在控制端上,以便让用户看到,但是该返回值不会被保存下来。也就是说,如果将来把该函数赋值给一个变量,该变量的值将仍然为空值(None),举例如下。
def name():
print('renkie')
a=name()
print(a)
结果
renkie
None
注:此时用print将返回值打印在控制端,调用name()函数,在控制端打印renkie,但该变量的值仍为空值。故输出结果为:renkie None
而return则恰恰相反,在调用函数后,return的返回值不会被打印输出在控制端上,如果想看输出的返回值,则要在调用函数时在函数前加上print。但是返回值会被保存下来,如果把该函数赋值给一个变量,则该变量的值即该函数的返回值,举例如下。
def name():
print('renkie')
return 'renkie'
a=name()
print(a)
结果
===================== RESTART: C:\Users\86157\Desktop\1.py =====================
renkie
renkie
4.2嵌套函数
函数支持嵌套,也就是一个函数可以在另一个函数中被调用,举例如下。
def square(x):
result=x**2
return result
def cube(x):
result=square(x)*x
return result
print(cube(3))
结果
27
我们首先创建一个求2次方的函数square(x)(注意这里用的是return,不是print,否则返回值将会是None,不能被其他函数套用)。然后创建一个求3次方的函数cube(x),在cube(x)中我们套用了square(x),并且也使用了return来返回函数的结果。
5.模块
在前面已经讲过,Python的运行模式大致分为两种:一种是使用解释器的交互模式,另一种是运行脚本的脚本模式。在交互模式下,一旦退出解释器,那么之前定义的变量、函数及其他所有代码都会被清空,一切都需要从头开始。因此,如果想写一段较长、需要重复使用的代码,则最好使用编辑器将代码写进脚本,以便将来重复使用。
不过在网络工程师的日常工作中,随着网络规模越来越大,需求越来越多,相应的代码也将越写越多。为了方便维护,我们可以把其中一些常用的自定义函数分出来写在一个独立的脚本文件中,然后在交互模式或脚本模式下将该脚本文件导入(import)以便重复使用,这种在交互模式下或其他脚本中被导入的脚本被称为模块(Module)。在Python中,我们使用import语句来导入模块,脚本的文件名(不包含扩展名.py)即模块名。 被用作模块的脚本中可能带自定义函数,也可能不带,下面将分别举例说明。
5.1不带自定义函数的模块
首先创建一个脚本,将其命名为script1.py,该脚本的代码只有一行,即打印内容“这是脚本1.”。
print('这是脚本1.')
然后创建第二个脚本,将其命名为script2.py。在脚本2里,我们将使用import语句导入脚本1(import script1),打印内容“这是脚本2.”,并运行脚本2。
import script1
print('这是脚本2.')
运行结果
这是脚本1.
这是脚本2.
可以看到,在运行脚本2后,我们同时得到了“这是脚本1.”和“这是脚本2.”的打印输出内容,其中,“这是脚本1.”正是脚本2通过import script1导入脚本1后得到的。
5.2带自定义函数的模块
首先修改脚本1的代码,创建一个test()函数,该函数的代码只有一行,即打印内容“这是带函数的脚本1.”。
def test():
print('这是带函数的脚本1.')
然后修改脚本2的代码,调用脚本1的test()函数,因为是脚本1的函数,所以需要在函数名前加入模块名,即script1.test()。
import script1
print('这是脚本2.')
script1.test()
运行结果
这是脚本2.
这是带函数的脚本1.
5.3Python内建模块和第三方模块
除了上述两种用户自己创建的模块,Python还有内建模块及需要通过pip下载安装的第三方模块(也叫作第三方库),下面分别讲解。
Python内建模块 Python有大量的内建模块直接通过import就可以使用,后面实验部分的案例代码将会重点讲解这些内建模块的使用。这里仅举一例,我们可以使用os这个内建函数来发送Ping包,判断网络目标是否可达,脚本代码及解释如下。
import os
hostname='www.baidu.com'
response=os.system("ping -n 1"+hostname)
if response==0:
print(hostname+'is reachable.')
else:
print(hostname+'is not reachable.')
• os是很常用的Python内建模块,os是operating system的简称。顾名思义,它是用来与运行代码的主机操作系统互动的,后面实验部分的案例代码会介绍关于os在网络运维中的许多用法。 • 这里我们使用os模块的system()函数来直接在主机里执行ping -n 1 www.baidu.com这条命令。
• os.system()的返回值为一个整数。如果返回值为0,则表示目标可达;如果为非0,则表示不可达。我们将os.system()的返回值赋给变量response,然后写一个简单的if...else判断语句来作判断。如果response的值等于0,则打印目标可达的信息,反之则打印目标不可达的信息。执行代码看效果。
除了os,能实现ping命令功能的Python内建模块还有很多,如subprocess。当在Python中使用os和subprocess模块来执行ping命令时,两者都有个“小缺陷”,就是它们都会显示操作系统执行ping命令后的回显内容,如果想让Python“静悄悄”地ping,则可以使用第三方模块pythonping,关于pythonping模块的用法在实验部分中将会提到。
Python第三方模块
Python第三方模块需要从pip下载安装,pip随Python版本的不同有对应的pip2、pip3和pip3.8,在CentOS 8里已经内置了pip2和pip3,可以在Windows命令行及CentOS 8下输入pip3.10来验证
对于网络工程师来说,最常用的Python第三方模块无疑是用来SSH登录网络设备的Paramiko和Netmiko。首先使用命令pip3.10 install paramiko和pip3.10 install netmiko来分别安装它们(如果你的Python不是3.10.x版本,而是其他Python 3版本,则将pip3.10替换成即可)。在安装之前,请确认你的CentOS 8主机或虚拟机、windows能够连上外网。
pip3.10 install paramiko
pip3.10 install netmiko
使用pip安装好Paramiko和Netmiko后,打开Python测试是否可以使用import paramiko和import netmiko来引用它,如果没报错,则说明安装成功。
>>> import paramiko
>>> import netmiko
如果没有安装成功则显示下列错误信息:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'Netmiko'
5.4from ... import ...
对于带自定义函数的模块,除了import语句,我们还可以使用from...import...来导入模块。它的作用是省去了在调用模块函数时必须加上模块名的麻烦,该语句具体的格式为from [模块名] import [函数名],举例如下。
将脚本2的代码修改如下,然后运行脚本2
from script1 import test
print('这是脚本2.')
test()
#运行脚本二
这是脚本2.
这是带函数的脚本1.
因为我们在脚本2中使用了from script1 import test,所以在调用脚本1的test()函数时不再需要写成script1.test(),直接用test()即可。
5.5if __name__ == '__main__':
在5.1节所举的例子中,脚本2在导入了模块脚本1后,立即就引入模块中的代码内容,这时就可以用if __name__ == '__main__':这个判断语句来达到目的。 在基本语法中已经讲过,Python中前后带双下画线的变量叫作内置变量,如__name__。我们可以这么来理解:如果一个Python脚本文件中用到了if __name__ == '__main__':判断语句,则所有写在其下面的代码都将不会在该脚本被其他脚本用作模块导入时被执行。 我们首先将脚本1和脚本2的代码分别修改如下,在脚本1中将print ("这是脚本1.")写在if __name__ == '__main__':的下面。(if __name__的if后面注意空格)
我们首先将脚本1和脚本2的代码分别修改如下,在脚本1中将print ("这是脚本1.")写在if\ __name__ == '__main__':的下面。
#脚本1
if __name__=='__mian__':
print('这是脚本1.')
#脚本2
import script1
print('这是脚本2.')
然后运行脚本2,发现虽然脚本2导入了脚本1,但是输出结果中却不再出现“这是脚本1.”的输出结果。
这是脚本2.
6.正则表达式
在网络工程师的日常工作中,少不了要在路由器、交换机、防火墙等设备的命令行中使用各种show或者display命令来查询配置、设备信息或者进行排错,比如思科和Arista设备上最常见的show run、show log、show interface和show ip int brief等命令。通常这些命令输出的信息和回显内容过多,需要用管道符号|(Pipeline)配合grep (Juniper设备)或者include/exclude/begin(思科设备)等命令来匹配或筛选我们所需要的信息。举个例子,要查询一台24口的思科2960交换机当前有多少个端口是Up的,可以使用命令show ip int brief | i up。 管道符号后面的命令部分(i up)即本节将要讲的正则表达式(Regular Expression)。另外,BGP(Border Gateway Protocol,边界网关协议)中出现的自治域路径访问控制列表(as-path access-list)是正则表达式在计算机网络中最典型的应用,比如下面的as-path access-list 1中的^4$就是一个标准的正则表达式。
ip as-path access-list 1 permit ^4$
6.1什么是正则表达式
根据维基百科的解释:正则表达式(Regular Expression,在代码中常简写为regex、regexp或RE),又称正规表示式、正规表示法、正规表达式、规则表达式、常规表示法,是计算机科学的一个概念。正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串。在很多文本编辑器中,正则表达式通常被用来检索、替换那些匹配某个模式的文本。 在Python中,我们使用正则表达式来对文本内容做解析(Parse),即从一组给定的字符串中通过给定的字符来搜索和匹配我们需要的“模式”(Pattern,可以把“模式”理解成我们要搜索和匹配的“关键词”)。正则表达式本身并不是Python的一部分,它拥有自己独特的语法及独立的处理引擎,效率上可能不如字符串自带的各种方法,但它的功能远比字符串自带的方法强大得多,因为它同时支持精确匹配(Exact Match)和模糊匹配(Wildcard Match)。
6.2正则表达式的验证
怎么知道自己的正则表达式是否写正确了呢?方法很简单,可以通过在线正则表达式模拟器来校验自己写的正则表达式是否正确。在线正则表达式模拟器很多,通过搜索引擎很容易找到。在线正则表达式模拟器的使用也很简单,通常只需要提供文本内容(即字符串内容),以及用来匹配模式的正则表达式即可。regex101就是一个常用的在线正则表达式模拟器,后面在举例讲解正则表达式时,会在该模拟器上验证的截图来佐证。在线正则表达式https://regex101.com/
6.3正则表达式的规则
正则表达式是一套十分强大但也十分复杂的字符匹配工具,本节只选取其中部分网络运维中常用并且适合网络工程师学习的知识点,然后配合案例讲解。首先来了解正则表达式的精确匹配和模糊匹配,其中模糊匹配又包括匹配符号(Matching Characters)和特殊序列(Special Sequence)。
6.3.1精确匹配
精确匹配即明文给出我们想要匹配的模式。比如上面讲到的在24口的思科2960交换机里查找Up的端口,我们就在管道符号|后面明文给出模式Up。又比如我们想在下面的交换机日志中找出所有日志类型为%LINK-3-UPDOWN的日志,那我们按照要求明文给出模式%LINK-3-UPDOWN即可,即命令show logging | i %LINK-3- UPDOWN。 但是如果同时有多种日志类型需要匹配,代码如下。
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up
000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up
000461 : Feb 17 22:39:26.464: %SSH-5-SSH2
SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded
000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
这时,再用精确匹配就显得很笨拙,因为要通过命令show logging | i %LINK-3-UPDOWN|%LINEPROTO-5-UPDOWN|%SSH-5-SSH2_SESSION|%SSH-5-SSH2_USERAUTH把所有感兴趣的日志类型都明文列出来,这里只有4种日志类型还比较容易,如果有几十上百种日志类型去匹配,再进行精确匹配的工作量就会很大,这时我们需要借助模糊匹配来完成这项任务。
6.3.2模糊匹配
模糊匹配包括匹配符号和特殊序列,下面分别讲解。 正则表达式中常见的匹配符号如下表所示。
6.3.3贪婪匹配
*、+、?、{m}、{m,}和{m,n}这6种匹配符号默认都是贪婪匹配的,即会尽可能多地去匹配符合条件的内容。 假设给定的一组字符串为“xxzyxzyz”,我们使用正则表达式模式x.*y来做匹配(注:精确匹配和模糊匹配可以混用)。在匹配到第一个“x”后,开始匹配.*,因为.和*默认是贪婪匹配的,它会一直往后匹配,直到匹配到最后一个“y”,因此匹配结果为“xxzyxzy”,可以在regex101上验证,如下图所示。
又假设给定的字符串依然为“xxzyxzyz”,我们使用正则表达式模式xz.y来做匹配,在匹配到第一个“xz”后,开始匹配“贪婪”的.,这里将会一直往后匹配,直到最后一个“y”,因此匹配结果为“xzyxzy”
6.3.4非贪婪匹配
要实现非贪婪匹配很简单,就是在上述6种贪婪匹配符号后面加上问号?即可,即?、+?、??、{m}?、{m,}?和{m,n}?。 假设给定的另一组字符串为“xxzyzyz”(注意不是之前的“xxzyxzyz”),我们使用正则表达式模式x.?y来做匹配。在匹配到第一个“x”后,开始匹配.?,因为.?是非贪婪匹配的,它在匹配到第一个“y”后便随即停止,因此匹配结果为“xxzy” 又假设给定的字符串依然为“xxzyzyz”,我们使用正则表达式模式xz.?y来做匹配,在匹配到第一个“xz”后,开始匹配.?,它在匹配到第一个“y”后便随即停止,因此匹配结果为<font color='red'>“xzy”。
◎ 正则表达式中常见的特殊序列 特殊序列由转义符号\和一个字符组成,常见的特殊序列及其用法如下表所示。
模糊匹配在正则表达式中很常用,前面精确匹配中提到的匹配思科交换机日志类型的例子可以用模糊匹配来处理,比如我们要在下面的日志中同时匹配%LINK-3UPDOWN、%LINEPROTO-5-UPDOWN、%SSH-5-SSH2_SESSION和%SSH-5-SSH2_USE RAUTH 4种日志类型,用正则表达式%\w{3,9}-\d-\w{6,13}即可完全匹配。
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up
000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up
000461 : Feb 17 22:39:26.464: %SSH-5-SSH2
SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded
000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
关于该正则表达式%\w{3,9}-\d-\w{6,13}是如何完整匹配上述4种日志类型的讲解如下。 首先将%\w{3,9}-\d-\w{6,13}拆分为6部分。 第1部分:%用来精确匹配百分号“%”(4种日志全部以“%”开头)
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
第2部分:\w{3,9}用来匹配“SSH”和”LINK““LINEPROTO”
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
第3部分:-用来精确匹配第一个“-”
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
第4部分:\d用来匹配数字“3”或“5”
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
第5部分:-用来精确匹配第二个“-”
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
第6部分:由上图可知,至此还剩下“UPDOWN” “SSH2_SESSION” “SSH2_USERAUTH”3部分需要匹配,因为\w可以同时匹配数字、字母及下画线,因此,用\w{6,13}即可完整匹配最后这3部分。
000459:Feb 17 17:10:35.202:%LINK-3-UPDOWN: Interface FastEthernet0/2,changedstate to up 000460: Feb 17 17:10:36.209: %LINEPROTO-5-UPDOWN: Line protocol on InterfaceFastEthernet0 / 2,changed state to up 000461 : Feb 17 22:39:26.464: %SSH-5-SSH2 SESSION:SSH2 session request from10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc',hmac 'hmac-sha1' Succeeded 000462: Feb 17 22:39:27.748: %SSH-5-SSH2_USERAUTH: User 'test' authentication for SSH2 Session from 10.1.1.1 (tty = 0) using crypto cipher 'aes128-cbc ',hmac' hmac-shal' succeeded
6.4正则表达式在Python中的应用
在Python中,我们使用import re来导入正则表达式这个内建模块(无须使用pip来安装)。 首先使用dir()函数来看下re模块中有哪些内建函数和方法。
其中,网络工程师较常用的Python正则表达式的函数主要有4种,分别为re.match()、re.search()、 re.findall()和re.sub(),下面对它们分别进行说明。
6.4.1re.match()
re.match()函数用来在字符串的起始位置匹配指定的模式,如果匹配成功,则re.match()的返回值为匹配到的对象。如果想查看匹配到的对象的具体值,则还要对该对象调用group()函数。如果匹配到的模式不在字符串的起始位置,则re.match()将返回空值(None)。re.match()函数的语法如下。
re.match(pattern,string,flags=0)
pattern即我们要匹配的正则表达式模式。string为要匹配的字符串。flags为标志位,用来控制正则表达式的匹配方式,如是否区分大小写、是否多行匹配等。flags为可选项,不是很常用。举例如下。
import re
test= 'test match() function of regular expression.'
a=re.match(r'test',test)
print(a)
print(a.group())
结果
<re.Match object; span=(0, 4), match='test'>
test
我们使用re.match()函数,从字符串“test match() function of regular expression.”里精确匹配模式“test”,因为“test”位于该段字符串的起始位置,所以匹配成功,并且返回一个匹配到的对象<re.Match object; span=(0, 4), match='test'> (即用print(a)看到的内容),为了查看该对象的具体值,我们可以对该对象调用group()方法,得到具体值“test”(即用print (a.group())看到的内容),该值的数据类型为字符串,group()函数在Python的正则表达式中很常用,务必熟练使用。如果我们不从字符串的起始位置去匹配,而是去匹配中间或末尾的字符串内容,则re.match()将匹配不到任何内容,从而返回空值(None)。
在上面的例子中,我们在模式“test”的前面加上了一个r,这个r代表原始字符串(Raw String)。在Python中,原始字符串主要用来处理特殊字符所产生的歧义,比如前面讲到的转义字符\就是一种特殊字符,它会产生很多不必要的歧义。举个例子,假设你要在Windows中用Python的open()函数打开一个文件,代码如下。
>>> f=open('C:\Users\86157\Desktop\test.txt','r')
File "<stdin>", line 1
f=open('C:\Users\86157\Desktop\test.txt','r') ^
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 2-3: truncated \UXXXXXXXX escape
这时,你会发现该文件打不开了,Python返回了一个错误,这是因为“\t”被当作了不属于文件名的特殊符号。解决的办法也很简单,就是在代表文件所在路径的字符串前面使用原始字符串。
>>> f=open(r'C:\Users\86157\Desktop\test.txt', 'r')
>>> print(f)
<_io.TextIOWrapper name='C:\\Users\\86157\\Desktop\\test.txt' mode='r' encoding='cp936'>
在正则表达式中使用原始字符串也是同样的道理,只是还有更多其他原因,网络工程师只需记住:在正则表达式中,建议使用原始字符串。
6.4.2re.search()
re.search()函数和re.match()一样,返回值为字符串,但是它比re.match()更灵活,因为它允许在字符串的任意位置匹配指定的模式。 re.search()函数的语法如下。
re.search(pattern,string,flags=0)
如果前面我们用re.match()在“Test match() function of regular expression.”中尝试匹配“function”不成功,因为“function”不在该字符串的起始位置。我们改用re.search()来匹配。
import re
test= 'test match() function of regular expression.'
a=re.search(r'function',test)
print(a)
print(a.group())
结果
<re.Match object; span=(13, 21), match='function'>
function
虽然re.search()可以在字符串的任意位置匹配模式,但是它和re.match()一样一次只能匹配一个字符串内容,比如下面是某台路由器上show ip int brief命令的回显内容,我们希望用正则表达式来匹配该回显内容中出现的所有IPv4地址。
Router#show ip int b
Interface IP-Address OK? Method status Protocol
GigabitEthernet1/1 192.168.121.181 YES NVRAM up up
GigabitEthernet1/2 192.168.110.2 YES NVRAM up up
GigabitEthernet2/1 10.254.254.1 YES NVRAM up up
GigabitEthernet2/2 10.254.254.5 YES NVRAM up up
我们尝试用re.search()来匹配。
import re
test='''Router#show ip int b
Interface IP-Address OK? Method status Protocol
GigabitEthernet1/1 192.168.121.181 YES NVRAM up up
GigabitEthernet1/2 192.168.110.2 YES NVRAM up up
GigabitEthernet2/1 10.254.254.1 YES NVRAM up up
GigabitEthernet2/2 10.254.254.5 YES NVRAM up up'''
ip_address= re.search(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',test)
print(ip_address.group())
结果
192.168.121.181
我们用正则表达式\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}作为模式来匹配任意IPv4地址,注意我们在分割每段IP地址的“.”前面都加了转义符号\,如果不加\,写成\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3},则会将匹配到GigabitEthernet1/1中的“1/1”。因为不加转义符号\,"."就会被当成匹配符号"." 。print (a.group())后可以看到只匹配到了192.168.121.181这一个IPv4地址,这就是re.search()的短板。如果想匹配其他所有IPv4地址(192.168.110.2,10.254.254.1,10.254.254.5),则必须用下面要讲的re.findall()。
6.4.3re.findall()
如果字符串中有多个能被模式匹配到的关键词,并且我们希望把它们全部匹配出来,则要使用re.findall()。同re.match()和re.search()不一样,re.findall()的返回值为列表,每个被模式匹配到的字符串内容分别是该列表中的元素之一。
re.findall()函数的语法如下。
re.findall(pattern,string,flags=0)
还是以上面尝试匹配show ip int brief命令的回显内容中所有IPv4地址的例子为例。
import re
test='''Router#show ip int b
Interface IP-Address OK? Method status Protocol
GigabitEthernet1/1 192.168.121.181 YES NVRAM up up
GigabitEthernet1/2 192.168.110.2 YES NVRAM up up
GigabitEthernet2/1 10.254.254.1 YES NVRAM up up
GigabitEthernet2/2 10.254.254.5 YES NVRAM up up'''
ip_address= re.findall(r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',test)
print(ip_address)
结果
['192.168.121.181', '192.168.110.2', '10.254.254.1', '10.254.254.5']
这里成功通过re.findall()匹配到所有的4个IPv4地址,每个IPv4地址分别为re.findall()返回的列表中的一个元素。
6.4.4re.sub()
最后要讲的re.sub()函数是用来替换字符串里被匹配到的字符串内容的,类似Word的替换功能。除此之外,它还可以定义最大替换数(Maximum Number of Replacement)来指定sub()函数所能替换的字符串内容的数量,默认状态下为全部替换。re.sub()的返回值是字符串。 re.sub()函数的语法如下。
a=re.sub(pattern,replacement,string,optional flags)
re.sub()函数的语法与前面讲到的re.match()、re.search()、re.findall() 3个函数略有不同,re.sub()函数里多了一个replacement参数,它表示被替换后的字符串内容。optional flags可以用来指定所替换的字符串内容的数量,如果只想替换其中1个字符串内容,则可以将optional flags位设为1;如果想替换其中的前两个字符串内容,则设为2;依此类推。如果optional flags位空缺,则默认状态下为全部替换。 下面以某台路由器上的ARP表的输出内容为例,我们用re.sub()来将其中所有的MAC地址全部替换为1234.56ab.cdef。
import re
test = '''
... Router#show ip arp
...Protocol Address Age (min) Hardware Addr Type Interface
... Internet 10.1.21.1 - b4a9.5aff.c845 ARPA TenGigabitEthernet2/1
... Internet 10.11.22.1 51 b4a9.5a35.aa84 ARPA TenGigabitEthernet2/2
... Internet 10.201.13.17 - b4a9.5abe.4345 ARPA TenGiaabitEthernet2/3'''
a = re.sub(r'\w{4}\.\w{4}\.\w{4}','1234.56ab.cdef', test)
print (a)
结果
... Router#show ip arp
... Protocol Address Age (min) Hardware Addr Type Interface
... Internet 10.1.21.1 - 1234.56ab.cdef ARPA TenGigabitEthernet2/1
... Internet 10.11.22.1 51 1234.56ab.cdef ARPA TenGigabitEthernet2/2
... Internet 10.201.13.17 - 1234.56ab.cdef ARPA TenGiaabitEthernet2/3
因为optional flags位空缺,所以默认将3个MAC地址全部替换成1234.56ab.cdef。如果将optional flags位设为1,则只会替换第一个MAC地址。
如果只希望替换某一个特定的MAC要怎么做呢?方法也很简单,直接精确匹配要替换的特定MAC,如下:
a = re.sub(r'b4a9.5abe.4345','1234.56ab.cdef', test)
6.5异常处理
异常处理(Exception Handling)是Python中很常用的知识点。通常在写完代码第一次运行脚本时,我们难免会遇到一些代码错误。Python中有两种代码错误:语法错误(Syntax Errors)和异常(Exceptions)。比如忘了在if语句末尾加冒号:就是一种典型的语法错误,Python会回复一个“SyntaxError: invalid syntax”的报错信息。 有时一条语句在语法上是正确的,但是执行代码后依然会引发错误,这类错误叫作异常。异常的种类很多,比如把零当作除数的“零除错误”(ZeroDivisonError)、变量还没创建就被调用的“命名错误”(NameError)、数据类型使用有误的“类型错误”(TypeError),以及尝试打开不存在的文件时会遇到的“I/O错误”(IOError)等都是很常见的异常。
除了这些常见的Python内置异常,从第三方导入的模块也有自己独有的异常,比如后面实验部分将会重点讲的Paramiko就有与SSH用户名、密码错误相关的“AuthenticationException异常”,以及网络设备IP地址不可达导致的Socket模块的“socket.error异常”。 使用异常处理能提高代码的鲁棒(Robust,健壮)性,帮助程序员快速修复代码中出现的错误。在Python中,我们使用try...except...语句来做异常处理,举例如下。
for i in range(1,6):
print(i/0)
Traceback (most recent call last):
File "C:\Users\86157\Desktop\ping.py", line 2, in <module>
print(i/0)
ZeroDivisionError: division by zero
for i in range(1,6):
try:
print(i/0)
except ZeroDivisionError:
print('division by 0 is not allowed')
division by 0 is not allowed
division by 0 is not allowed
division by 0 is not allowed
division by 0 is not allowed
division by 0 is not allowed
我们故意尝试触发零除错误,在没有做异常处理时,如果Python解释器发现range(1,6)返回的第一个数字1试图去和0相除,会马上返回一个ZeroDivisonError,并且程序就此终止。在使用try...except...做异常处理时,我们使用except去主动捕获零除错误这个异常类型(except ZeroDivisonError:),并告诉Python解释器:在遇到零除错误时,不要马上终止程序,而是打印出“Division by 0 is not allowed”这个信息,告知用户代码具体出了什么问题,然后继续执行剩下的代码,直至完成。正因如此,在遇到零除错误后,程序并没有终止,而是接连打印出了5个“Division by 0 is not allowed”(因为range(1,6)返回了1、2、3、4、5共5个整数)。
我们在except语句后面加入了ZeroDivisionError,这种提前捕获异常类型的做法是因为我们知道自己的程序将会触发零除错误,如果这时代码里出现了另外一种异常,则程序还是会被中断,因为except并没有捕获该异常。一般来说,我们很难记住Python中所有可能出现的异常类型,如果代码复杂,则无法预测脚本中将会出现什么样的异常类型,当出现这种情况时,我们要怎么做异常处理呢?有两种方法:一是在except语句后面不接任何异常类型,直接写成“except:”。二是通过Exceptions捕获所有异常。下面分别举例讲解。
try:
10/0
except:
print('there is an error')
当单独使用except、不捕获任何异常时,可以确保程序在遭遇任何异常时都不会被中断,并返回自定义打印出来的错误信息(比如上面例子中的There’s an error.)来代替所有可能出现的异常类型。这么做的优点是省事,但缺点也很明显:我们无法知道代码中具体出现了什么类型的异常,导致代码排错困难。
try:
10/0
except Exception as e:
print(e)
===================== RESTART: C:\Users\86157\Desktop\1.py =====================
division by zero
而使用Exceptions时(except Exception as e:),不仅可以捕获除SystemExit、KeyboardInterrupt、GeneratorExit外的所有异常,还能让Python告诉我们具体的错误原因(比如上面例子中出现的零除错误),方便对代码进行排错。如果要捕获SystemExit、KeyboardInterrupt和GeneratorExit这3种异常,则只需要把Exceptions替换成BaseExceptions即可。