《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。
NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。
由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注
本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注
在第一章,为大家分享了我对NetDevOps的认知,NetDevOps发展的几个重要阶段,同时简单介绍了Python这门开发语言,本章节开始我们要开始硬核之旅,带领大家从零开始掌握Python,只有掌握了Python这门开发编程语言,我们才能够按照我们的需求编写脚本乃至工具平台。
Python是一门十分简单容易上手的编程语言,对于网络工程师而言,掌握Python这件事并不苦难,大家所要做的就是按部就班地掌握每个小节的知识点,并亲自将它敲出来!
本章节,我会从网络运维的知识体系,将Python的为大家讲解清楚,讲解最核心的部分。当新手面对一门语言,个人认为,我们需要尽快地掌握最基础、最核心的知识,快速动手编写有成效的脚本,这样才会有行进下去的动力。很多初学者最容易犯的错误,可能就是买了一本非常好的书,从头看到尾学完之后,说懂吧,写不出来有用的代码(减少自己的工作量的脚本),说不懂吧,他还能写出来一些简单的脚本,比如比数字大小、猜数字等。这也与NetDevOps的理念不合,我们并没有把开发应用到日常运维之中。如何避免这种窘境?最佳的实践方法就是掌握最核心最基础的语法,以解决实际工作中遇到的问题为行进方向,不要被庞大的编程语言体系淹没在学习的迷雾之中。通过小步快跑的方式,以基础语法为核心,以解决实际问题为宗旨,不断扩展这么语言的学习范围和深度,实现滚雪球的效应,在一个个简单的脚本中看到光,在一个个问题的解决之后找到自信!
同时在学习知识的过程中要学会举一反三,触类庞通。比如很多书籍以实际生活中的例子来讲基础语法,我们作为网络工程师,就要学会将这些情景映射到网络运维之中,类比网络运维。
工欲善其事必先利其器,首先让我们一起动手来搭建一个Python的开发环境吧!
关于Python版本的选择,在这个时代已经毫无疑问,一定选择Python3。Python2在当今这个时代已经是明日黄花,无论是官方还是第三,对于Python2的支持都到了尾声。且根据个人实践,我更推荐Python3.8、Python3.9。根据我在实际生产中的使用,以及对一些Python工具包的观测,很多包至少要求3.6的某个比较靠后的版本,基本都能支持3.9,所以3.8和3.9是不错的选择。本书所涉及到的包对3.9的支持也比较好,且3.9的官方支持时间会比3.8时间久一些,所以最优解目前个人认为是3.9。当然随着时间的变化,大家也可以按照这个思路去决定你入门的Python版本。
同时根据笔者的一些实践经验来看,我也推荐初学者安装Anaconda最新版(截止到发稿,目前最新版本是2022.5,内含Python3.9的版本),它是一个开源的Python发行版本,其包含了conda、Python等180多个科学包及其依赖项,虽然是用于科学计算,但是集成的这180个包,如果是身处隔离的办公网络内,可以减少我们很多的麻烦,且内部集成了Jupyter Notebook这种基于web的交互式的IDE(开发集成环境,简单理解就是开发用的工具)。对于新手或者简单写些测试脚本也都是不错的选择。
当然本书还是以纯粹的Python环境作为演示,暂不安装Anaconda。
对于新手,我一定会强调:**保证你的电脑中只有一个Python环境!**如果安装出现了问题没有达到预期,一定卸载干净,重启电脑后,确保没有文件残留后,再重新点击安装文件进行安装!
一个电脑是可以装多个Python环境的,但是Python的可运行文件在环境变量中只有一个会生效,我们安装了一个Python环境后加载到了环境变量中,出于某种原因,我们可能选择重装,如果稍有操作不慎,可能会导致电脑中有两个共存的Python,而对于初学者而言,往往搞不清楚自己究竟用的是哪个Python环境,这个是我在实际工作中所观察到的,很多网络工程师都会被环境的安装劝退,或者是在后期,由于环境的问题导致自己一直无法进行开发。比如你安装了一个连接网络设备的netmiko包(后续会详细讲到),以为自己使用的是A环境,在开发中IDE也默认了A环境,但是你无论怎么导入这个包,程序都说找不到。原因是由于初学阶段安装时各种问题重装Python,导致多个Python共存,netmiko实际被安装到了B环境。你的开发环境指向了A环境,无论你怎么导入都不会成功的。
我们一定要去官方网站,结合自己的操作系统下载对应的Python安装文件。官网地址https://www.python.org/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xwd7lZfe-1658882259395)(assets/image-20220527002110867.png)]
在官网中,我们点击Downloads的All releases导航栏,进入到版本选择页面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qD0Uyy9R-1658882259397)(assets/image-20220527002222391.png)]
在上半部分,我们会看到Python目前几个活跃的版本,它的发行日期以及官方支持结束日期。其中3.9的版本综合官方支持时长和目前主流的NetDevOps Python工具包支持情况,是最优解。
我们来到网站中部,选择最新的Python3.9版本即可,点击download进入安装包选择。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YI7eEdPq-1658882259398)(assets/image-20220527002424062.png)]
在新打开的网站底部,我们选择对应的安装文件点击即可下载。本书操作系统为64位win10操作系统,所以选择对应的版本下载安装。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4TGTkqjC-1658882259398)(assets/image-20220527002612549.png)]
双加对应的安装文件,这个时候一定要勾选上将Python添加到环境变量的选项,这样我们打开cmd窗口输入Python才会有效果。我们可以选择安装到默认位置,也可以安装到指定盘符目录下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cf7Ykz7A-1658882259399)(assets/image-20220527002946978.png)]
很快就可以安装完成。这个时候我们打开CMD,输入“python”,进入到Python的交互式编程窗口。至此,Python就完成了安装。
通过在CMD(或者其他terminal中)敲入“python”进入到的模式是Python自带的交互式编程,在这里我们每敲一个命令都会实时计算。但是关闭窗口后这些这些代码也都消失。其实更多用于演示或者测试一些比较短的代码,响应会更快一些。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cbtgzXci-1658882259400)(assets/image-20220527185811927.png)]
现在请跟我敲下第一行代码
print('Hello,world')
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ViT00GVB-1658882259401)(assets/image-20220527185854880.png)]
至此我们完成了Python的安装,敲下了我们的第一行Python代码。
安装好了Python环境,我们就需要选择Python的IDE(集成开发环境)了。IDE一般都支持支持语法高亮、智能代码补全、自定义快捷键、自动格式化代码等等。合理使用优秀的IDE,可以极大地提升我们的开发效率。
对于新手,Python的IDE选择,笔者推荐PyCharm的社区版本,PyCharm是由JetBrains打造的一款Python IDE,有社区版和专业版,专业版加入了一些web开发和科学计算开发的便利功能,对于普通用户,社区版足矣。
通过PyCharm的官方网站 https://www.jetbrains.com/pycharm/ 我们点击Download,会打开一个新的页面,选择社区版即可。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wz0ZoJ2u-1658882259402)(assets/image-20220527195937907.png)]
双击安装软件,按需调整安装位置,以及一些其他的选项(这些选项不影响基本使用),等待安装结束后打开PyCharm。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnjvQp6R-1658882259402)(assets/image-20220527200055130.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flugw1CM-1658882259403)(assets/image-20220527200521596.png)]
登录之后会让我们选择创建工程还是打开一个现有的代码项目。在此我们选择创建新项目。
我们修改一些项目的配置,比如我们选择修改项目名称和位置。默认的如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAywBlVx-1658882259404)(assets/image-20220527200652722.png)]
在这个配置选项中,对于初学者我们建议不适用虚拟环境(虚拟环境我们在本书的后续会讲),记得我们说过,对于新手尽量只有一套Python环境。
我们选择自己配置Python解析器,如下图点击“…”
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FxBC6We5-1658882259405)(assets/image-20220527201009179.png)]
这个时候会弹出一个窗口,我们选择系统解析器,在右侧的解析器地址中选择我们安装的Python即可(对于新手而言一定要先安装Python,再安装Pycharm)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hGqfe3xa-1658882259405)(assets/image-20220527201235346.png)]
我们也可以选择修改我们的项目名称。之后点击create按钮即可创建项目。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X4ukS04i-1658882259406)(assets/image-20220527201418725.png)]
项目创建之初PyCharm会对我们的Python环境进行索引,以便提供更方便的开发功能。同时默认帮我们创建了一个代码,我们可以删除调,或者在上一步取消创建main.py的选项。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DSvTqQyV-1658882259413)(assets/image-20220527201630959.png)]
在右侧的项目区域我们右键选择New->Python File,给文件命名即可创建好Python文件。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DtLYnGvJ-1658882259414)(assets/image-20220527202121650.png)]
在新建的Python文件中我们简单编写打印一个字符串的代码,然后右键,选择 Run 我们的脚本名称。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tNXl9qST-1658882259415)(assets/image-20220527202423024.png)]
结果如下,在下方会有一个窗口,把我们的代码结果输出,程序如果正常执行,最后的exit code是0。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8Heq9mEf-1658882259416)(assets/image-20220527202348973.png)]
至此我们的整个开发环境已经搭建好了。PyCharm有很多的便利用法,我们在后续文章中逐步展开。
在正式学习Python之前,我们先准备一些基础的知识,以便我们后续章节的讲解。
这部分内容,同时又需要在后续编程中不断加深理解。
标识符是Python用于识别不同的变量、函数、对象、类、模块而起的一个名称,比如在代码开发中给某变量起名字,这个名字就是标识符,当然我们习惯成变量名(函数的标识符就称函数名,诸如此类)。
标识符的命名规则是包含字母、数字、下划线“_”,且必须以一个非数字字符开始。且标识符是大小写敏感的,一定要注意区分大小写。
放到我们网络运维,我们定义一个变量,用来表示接入交换机,我们就可以给他们定义成as01,我们让as01的IP地址为192.168.1.1,这个过程就是赋值。
在Python中是这样表达的。
as01 = '192.168.1.1'
我们用单个等号“=”来进行赋值。仔细看会发现等号左右各有一个空格,这主要是为了可读性更好,防止字符都紧紧挨在一起,显得非常拥挤。
Python不同于其他的语言,变量定义的第一时间就要赋值
回到命名规则,起名不能随便起,我们刚才讲过了,必须是字母、数字、下划线的组合,且不能以数字开头。
比如这几个起名就是合法的:as01 _as01 as_01。
但是这个起名就是非法的:01as as-01。因为第一个以数字开头了,第二个有“-”号,不在数字、字母、下划线这三种合法组成元素之一。
还有一种极端情况,单独的下划线"",也是合法的,如下:
device_name, _ = get_device_info(id=1)
_这种情况主要是因为,在代码编写过程中,函数返回两个值,但有时候我们只关心第一个,那第二个变量名就定义为下划线,下划线作为一种约定俗称不重要的变量名使用。这种极端情况大家了解即可,因为有些人的代码是这么写的,假如有天我们遇到了,我们需要读懂其中的意思。
python的变量推荐使用蛇形命名法,其规则是单词全部用小写字母,单词之间用下划线隔开,因为酷似一条弯弯曲曲的小蛇,故称之为蛇形命名法。
如下我们定义一台设备的相关信息
device_name = 'AS01'
device_ip = '192.168.1.1'
device_start_u = 10
每门语言运行都会有一些特殊的语法,比如用if去做判断,我们不能用if来作为变量名(包括其他标识符也不行),这类我们称之为保留字(其他语言也称关键字)。Python有33个保留字,如下表。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHKmgea9-1658882259416)(assets/image-20220527233446959.png)]
有别于java、C等其他语言,Python是以缩进来分隔代码的逻辑块。这种缩进特别像一篇文章,每段段落是两个空格开始。用两个空格我们可以区分出段落。在Python中用于突出一块完整的代码逻辑。其他的语言是有的是通过大括号来实现的。
关于缩进风格:
1. 首行要顶头写,无缩进。
2. 进入到某代码块,使用相同的空格缩进,标准Python风格是使用4个空格,但是个人认为层级过深可以适当用两个空格做缩进。
我们看一个例子如下:
a = 1
b = 2
if a > b:
print('a>b')
print('a is ',a)
else:
print('a<=b')
print('a is ',a)
虽然我们还没触碰Python的编程核心,但是这段代码也非常易懂,且可以执行,这就是Python的魅力,接近自然语言、伪码。
通过这段代码的缩进我们可以清晰知道,先定义了变量a,b。然后进行了一个判断。如果符合条件或者不符合条件,分别进行了对应的输出。每种情况下的输出逻辑都可以通过缩进看出,打印的两行代码缩进相同,且上下连贯没有被中断,所以这两行代码是一起的。
在更复杂的逻辑中,Python也是靠缩进来区分代码块。所以合适的逻辑和适当的留白(在不影响代码的情况下,多敲一个空格或者空白行),可以让我们的代码可读性更好。
注意,永远不要在普通文本编辑里,在代码中使用Tab键做缩进!这种视觉上是四个空格,实际是一个制表符,是一个缩进,及其容易造成缩进混乱,程序报错。但是在PyCharm等专业Python IDE里可以使用Tab键,它们会将其Tab键的输入替换成是4个空格。
注释主要用于对代码做一些备注,良好的注释有助于提高我们代码的可维护性和可读性。
Python的注释有两种风格:
“#”单行注释,可以单独写一行,也可以在Python代码后面写,但是注释内容都不能跨行
三个单引号或者三个双引号的多行注释
示例:
# 定义变量a,单起一行注释。“#”与注释内容建议一个空格,记得我们的留白
a = 1
b = 2 # 定义变量b,行末的注释,建议"#"与代码末端保持两个空格,提高可读性
'''
判断ab的大小主体逻辑。
可以写很多行内容。
可以把三个单引号一同替换为三个双引号
'''
a = 1
b = 2
if a > b:
print('a>b')
print('a is ',a)
else:
print('a<=b')
print('a is ',a)
输出的方式千千万,我们这里讲的是用print函数打印出来结果,是为了给“人”看的,是stdout的一种方式。实际上写文件,写数据库也可以看做是输出。
# 定义变量并同时赋值
a = 'a'
b = 'b'
c = 'c'
# 在print默认空格隔开,以回车(换行)结束 直接
print(a,b,c)
print(a, b, c, sep=',') # 用sep指定的字符串来隔开
print(a, b, c, end=';') # 用end指定的符号来结束,默认是回车(换行)结束
结果如下:
a b c
a,b,c
a b c;
每个编程语言都有自己独特的基础数据类型,我们耳熟能详的比如整数、字符串、数组等等,其他所有的复杂对象都是从这些基础的数据结构衍生出来的。。
Python的基础数据类型包含以下几种:
下面我们依次展开讲解各个数据类型
另外还有一个特殊值,空值None,类似于其他语言中的NULL,它代表是一个空对象(它不是基础数据类型,但是是一个非常简单且需要掌握的数据类型),不做过多展开。
Python中的数字分为整数(int)、浮点数(float)、复数(complex),复数在日常中基本不会涉及,我们跳过。
Python中的整数与我们的书写习惯一致。正整数、负整数、零均按我们的日常使用习惯使用即可。
整数的上下限在64位系统中可以认为无上限。而且在日常网络运维也不会出现一个内存无法保存的整数。我们无需关注这些极端情况。
a = 10
b = 0
c = -20
浮点数即我们日常所说的小数,浮点数在Python中与我们的书写也一致。
x = 51.2
y = -11.2
同时它还支持科学计数法,大家了解即可,日常开发中很少使用。
a = 1.23e-18 # 等同于1.23*10^-18
python的数值支持加(+),减(-),乘(*),除(/),整除(//),取余(%),代码如下:
a = 6
b = 8
print(a + b) # 输出结果14
print(a - b) # 输出结果-2
print(a * b) # 输出结果48
print(a / b) # 输出结果0.75
print(a // b) # 输出结果0
print(a % b) # 输出结果6
字符串是非常重要的一个数据类型,它是以引号括起来的一段文本。引号可以是单引号、双引号、三引号(三个单引号或者三个双引号),但是一定要成对出现,引号中间的文本内容是字符串承载的数据。字符串是严格区分大小写的。只写一对引号,内容为空,我们称之为空字符串。在Python中没有char(单个字符)这种数据类型,字符串中的字符可以是任意个,包括零个、一个或者多个
示例:
a = 'NetDevOps'
b = "NetDevOps"
c = '''this is a book about NetDevOps
这是一本关于NetDevOps的书
'''
示例中我们用单引号,双引号和三引号分别创建了三个字符串。假如我们的字符串中含有引号,这个时候该如何处理呢?
方法1:定义字符串的引号和字符串文本中的引号使用不同的引号。对于新手建议使用这种。
d = "It's a book about NetDevOps."
e = 'It is a book about "NetDevOps".'
f = '''It's a book abount "NetDevOps".''' # 文本中既有单引号又有双引号,我们可以考虑用三引号。
方法2:使用转义符号反斜杠——“\”,转移符号后接我们要使用的引号。
d = 'It\'s a book about NetDevOps.'
e = "It is a book about \"NetDevOps\"."
我们想用字符串表示一个回车怎么处理呢?
Python的做法是使用\n
代表回车,其中\
就是转义符号,它后面接字母n
代表回车换行,字母n
的表达意义发生了转换,这就是转义。反斜杠被称为转义符号,\n
被称为转义字符。
下表是一些常用的转义字符(笔者根据日常运维所需进行了取舍,后续常用的列了出来)
转义字符 | 说明 |
---|---|
\n | 换行,将光标位置移到下一行开头。 |
\r | 将光标位置移到本行开头 |
\t | 横向制表符 |
\' |
单引号 |
\" |
双引号 |
\\ |
斜杠符号,\本身是转义符号,我们想用它表示字符串本身时需要转义 |
在NetDevOps中, 我们写代码,我们定义一个设备名称的变量,就可以赋值成字符串类型的,设备的制造商可以是字符串,设备的所在房间可以是字符串,设备端口的配置可以字符串。待执行的一条命令也可以是字符串。
dev_name = 'as01'
dev_manufacture = 'HUAWEI'
dev_room = '0401'
# 我们可以适当对单词进行缩写,比如用intf代表interface。
# 但是尽量不要用int,int是一个用于将对象转换成整数的函数。
intf_config = '''interface Vlan20
ip address 192.168.137.201 255.255.255.0
'''
cmd = "show version"
Python的字符串提供了很多便利的方法(可以简单等同于函数,初学者不必纠结名称),可以方便我们处理字符串,比如进行查找、切割、转大小写等,根据笔者的使用经验和大家简单介绍一下常用的方法。
注:函数是一个单独定义的代码块,方法是对象中的一个执行特定功能的代码块。对于初学者可以简单把函数与方法划等号,也可以简单认为方法是在对象中的函数的称谓,无需过多纠结这两个名词。本书编写会尽量严谨,但是读者在阅读过程中可以把方法和函数划等号。
以下方法直接在我们的字符串变量后写点和方法的名字即可,方法中要传入对应的参数,形式如下。
字符串变量.方法名( 参数 )
format方法是一个字符串格式化的方法,字符串的格式化是指,按照一定模板,向模板内传值,生成一个符合模板格式要求的字符串。Python的字符串格式化方法非常多,此处我们重点介绍format方法。
先编写一个字符串的模板,对于其中希望填充值的地方用花括号{}
括起来,然后对模板字符串调用format方法,依次传入要填充的字符串,数目一定要与花括号的数目对应。
server = 'host01'
ip_addr = '192.168.1.100'
intf_desc_tpl = 'connect to {}, ip:{}'
intf_desc.format(server,ip_addr)
print(intf_desc) # 结果是"connect to host01, ip:192.168.1.100"
这种方法还有另外一个书写方式,在定义字符串模板的时候,希望填充的值用花括号括起来,同时花括号内填这个填充值的参数名称,然后对模板字符串调用format方法,为花括号内的参数进行赋值。此方法可读性非常好,但是写起来会比上面的方法多写一些内容,初学者可以在看到函数部分后再回来看看此部分示例。
server = 'host01'
ip_addr = '192.168.1.100'
intf_desc_tpl = 'connect to {SERVER}, ip:{SERVER_IP}'
intf_desc.format(SERVER=server, SERVER_IP=ip_addr)
print(intf_desc) # 结果是"connect to host01, ip:192.168.1.100"
find方法用户发现字符串中是否包含子串,如果发现子串返回子串首字母出现的位置索引值(Python中的索引从零开始),如果未发现返回-1
。
我们可以通过比较find的结果与0比较,小于零代表未发现,大于等于零代表发现了子串。
intf_show = 'Eth1/1 is up'
up_index = intf_show.find('up')
print(up_index)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-chIy8dyw-1658882259417)(assets/image-20220528171136866.png)]
最终输出结果是10。
如果我们改为find('down')
则输出结果是-1。
我们在NetDevOps开发中可以用于判断回显是否包含关键字。
startswith方法用于判断是否以给定的字符串开始的,返回是真(True)或假(False)。
intf_show = 'Ethernet1/1 is up'
is_interface_line = intf_show.startswith('Ethernet')
print(is_interface_line) # 输出结果是Ture
endswith方法用于判断是否以给定的字符串结束的,返回是真(True)或假(False)。
intf_show = 'Ethernet1/1 is up'
interface_up = intf_show.endswith('up')
print(interface_up)
find 、startswith、endswith主要用于在文本中发现是否有关键字,通过关键字我们可以判断一些状态,或者确定此行中是否有我们要提取的信息等等。
split方法用于切割字符串,返回的结果是列表(list,后续会展开讲)。
默认是用空白符来进行切割,空白符泛指没有显示却又占位置的符号,比如空格、制表符、换行。
intf_show = 'Ethernet1/1 is up'
result = intf_show.split()
print(result) # 结果是['Ethernet1/1', 'is', 'up']
通过这种方式,我们就可以获取包含一些字段信息的列表,再通过列表的访问机制就可以提取出端口名称和端口状态了,这个我们在列表中讲解使用方法。
我们也可以用指定的字符去切割,比如我们使用is
去切割。
intf_show = 'Ethernet1/1 is up'
result = intf_show.split('is')
print(result) # 结果是['Ethernet1/1 ', ' up']
这样直接获取了端口和状态。
strip方法用去去除字符串左右的指定字符串,不修改原来的字符串(因为字符串是不可修改的类型),返回一个新的字符串。
默认是去除左右的所有空白符。
我们也可以在方法内直接传入要去除的字符串,同时strip还有两个变种方法lstrip和rstrip,可以只去除左侧或者右侧的指定字符串,单这些NetDevOps脚本编写很少涉及,故不演示。
intf_show = ' Ethernet1/1 is up '
result = intf_show.strip()
print(result) # 结果是"Ethernet1/1 is up"
splitlines方法用于将一大段文本按行切割,返回一个列表。行的结束符号Python会帮我们自动判断,对于初学者无需关注。
intf_config = '''interface Vlan20
ip address 192.168.137.201 255.255.255.0
'''
configs = intf_config.splitlines()
# 结果是['interface Vlan20', ' ip address 192.168.137.201 255.255.255.0']
print(configs)
replace方法用于将某字符串替换为我们指定的字符串。它有两个参数,第一个想要替换的字符串,第二个是要去替换之前那个字符串的字符串。一个是old,一个是new。
intf_name = 'Eth1/1'
full_intf_name = intf_name.replace('Eth', 'Ethernet')
print(full_intf_name) # 结果是"Ethernet1/1"
由于字符串是不可变的数据类型,所以原有的变量intf_name指向的字符串不会被修改,我们需要将函数返回的值赋值给一个变量,可以是新定义一个变量,也可以用原有的变量。
intf_name = 'Eth1/1'
intf_name = intf_name.replace('Eth', 'Ethernet')
print(intf_name) # 结果是"Ethernet1/1"
字符串通过加号可以实现字符串的拼接,生成一个新的字符串。
server = 'host01'
ip_addr = '192.168.1.100'
intf_desc = 'connect to ' + server + ', ip:' + ip_addr
print(intf_desc) # 结果是"connect to host01, ip:192.168.1.100"
对于初学者而言,多用于按照一定格式拼接多个字符串。如果是这种情况,更推荐字符串格式化这种方式。
以上几个字符串的相关使用,结合判断、循环,我们就可以写出一些复杂的逻辑,对网络配置进行解析,提取出我们想要的信息。这个思路,我们在掌握了判断循环后再去编写代码实现。
列表(list)有点类似于其他编程语言中的数组,它是一组有序的数据,每个成员都有一个索引值,索引值从零开始依次递增。
这组有序的数据类型可以是Python的基础数据类型,也可以是复杂的对象。Python的列表最大的不同在于成员的数据类型可以不一。
它的创建方式比比较简单,用中括号(方括号)创建,列表中的成员用逗号隔开。
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
dev_info = ['192.168.1.1', 'as01', 'huawei', 'ce6800', 48, ['beijing', 'dc01']]
如上,第一个我们用于定义一组端口,都是字符串的成员。这是在日常NetDevOps开发中比较常见的一种形式,成员都是相同的类型,代表一类事物。
第二个列表我们稍微改了一下,成员既有字符串,又有数字,还有列表。这种是典型用多个维度的成员组成的列表,用于描述一个事物。deb_info这个变量中我们通过成员描述了它的IP地址、设备名称、厂商、系列、端口数记忆所属数据中心。
这是笔者总结的两种组织列表的场景:同一纬度的成员描述一系列事物,不同维度的成员描述一个事物。
列表是有序的,通过它对应的排序(从零开始),称之为索引更为准确,通过方括号,我们可以访问到这个成员。
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intf = intfs[0] # 此处千万不要用int去命名端口变量,会与int函数冲突
print(intf) # 此处输出'Eth1/1'
intf = intfs[2]
print(intf) # 此处输出'Eth1/3'
Python的列表访问成员还有一个非常有意思的,异于其他语言的特性,负索引。
我们可以输入负数,代表从倒数第N个成员。负索引最后一个成员开始排序,最后一个的索引是-1。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4HNRXoKW-1658882259418)(assets/image-20220529115904356.png)]
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intf = intfs[-1] # 此处千万不要用int去命名端口变量,会与int函数冲突
print(intf) # 此处输出'Eth1/4'
无论是正索引还是负索引,我们访问都不能越界,如上图,我们不能访问索引为4或者-5的成员,因为它不存在,Python都会报错。
我们想获取列表长度的时候可以直接调用一个Python的内置函数len,然后传入列表。
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intf_sum = len(intfs)
print(intf_sum) # 此处输出4
print(intfs[intf_sum - 1]) # 此处输出'Eth1/4'
我们访问列表的最后一个成员的时候,可以使用长度减一的索引来访问,但是不建议,Python的风格(我们称之为pythonic),一般使用负索引。
列表是一个可变的数据类型,在创建之后,我们还可以继续在列表内追加成员,使用列表的append方法即可,一次只能追加一个成员,可以一直追加。
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intfs.append('Eth1/4')
# 结果是['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4', 'Eth1/4']
print(intfs)
两个列表合并有两种方式:
intfs_part1 = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intfs_part2 = ['Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
intfs = intfs_part1 + intfs_part2
print(intfs_part1) # 结果是['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4', 'Eth1/4']
print(intfs_part2) # 结果是['Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
print(intfs) # 结果是['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4', 'Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
intfs_part1 = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
intfs_part2 = ['Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
intfs_part1.extend(intfs_part2)
print(intfs_part1) # 结果是['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4', 'Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
print(intfs_part2) # 结果是['Eth1/5', 'Eth1/6', 'Eth1/7', 'Eth1/8']
切片如同这个词的字面意思,是指从一个已有的列表中切取一“片”,这一“片”也就是一个子列表,切片的方式很灵活,它的规则如下。
[start_index:stop_index:step]
start_index 是指起始索引值,可以不写,默认是列表头。
stop_index 是指结束索引值,可以不写,默认取到列表尾。
step 是指步长,是取成员索引的间隔。(可以为负数,达到反向切片效果,了解即可)
通过以上的方式,则会返回一个新的列表。
切片过程中如果指定了stop_index,则只能取到stop_index 前一个符合步长的索引值。类似于数学中的开区间,无法取值到stop_index。
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
sub_intfs = intfs[:3]
# 只能取值到3前面的索引也就是到索引2,结果是['Eth1/1', 'Eth1/2', 'Eth1/3']
print(sub_intfs)
sub_intfs = intfs[1:3]
# start_index是可以取到的,但是stop_index是取不到的,结果是['Eth1/2', 'Eth1/3']
print(sub_intfs)
sub_intfs = intfs[::2]
# 旗帜索引可以不填写,默认从头取到尾,间隔为2,结果是['Eth1/1', 'Eth1/3']
print(sub_intfs)
字典(dcit)是一组通过关键字进行索引的、有序的、不重复的、可变的数据集合,可以简单理解为一组键值对且键不可重复(key是关键字,value是对应的值,二者是一个字典的成员)。
注:自Python3.6开始,字典变为了有序的数据集合。
字典的创建方式很简单,通过花括号{}
来创建一个字典,成员(键值对)间用逗号隔开,键与值之间用冒号隔开。形如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hKja8MxE-1658882259419)(assets/image-20220529161320396.png)]
key可以是可以哈希的对象即可,对于初学者而言,认为只有字符串和数字可以作为key,实际在使用中,我们用字符串做key的更常见。
value可以是任何python的数据类型,包含了基础的数据类型,也包含复杂的数据对象。
dev_info = {'ip': '192.168.1.1', 'name': 'as01', 'manufacture': 'huawei', 'series': 'ce6800', 'ports_sum': 48}
为了提高可读性冒号的前后各一个空格,逗号后面一个空格,适当留白。当字典的成员(键值对)比较多的时候,我们可以适换行。
dev_info = {'ip': '192.168.1.1',
'name': 'as01',
'manufacture': 'huawei',
'series': 'ce6800',
'ports_sum': 48}
我们可以用方括号[]
,指定key访问到对应的value。
dev_ip = dev_info['ip']
print(dev_ip) # 输出结果是'192.168.1.1'
如果字典中无此key,则程序会报错,另有一种安全的访问方法,是字典的get方法,它的逻辑是:
None
。# 取值存在
dev_ip = dev_info.get('ip')
print(dev_ip) # 输出结果是'192.168.1.1'
# 取值不存在,返回指定默认值
ssh_port = dev_info.get('ssh_port', 22)
print(ssh_port) # 输出结果是我们给定的默认值22
# 取值不存在,未指定默认值,返回None
username = dev_info.get('username')
print(ssh_port) # 输出结果是默认值,Python中的特殊对象None
字典的键值对可以修改也可以追加,直接通过指定key进行赋值:
dev_info['name'] = 'as02'
dev_info['ssh_port'] = 22
# 输出结果是{'ip': '192.168.1.1', 'name': 'as02',
# 'manufacture': 'huawei', 'series': 'ce6800',
# 'ports_sum': 48, 'ssh_port': 22}
print(dev_info)
在这个过程中我们也发现,新添加的成员位于字典的最后。
布尔(bool)只有真或假两个值,对应True和False,在进行判断的时候非常有用。
数字、字符串、列表、字典等有一些运算是可以得到一个布尔值的。比如比较数字的大小返回的就是布尔值,某成员是否在列表内,某key是否在字典里出现。
布尔值可以进行逻辑运算,也称布尔运算。
包含三种运算:
flag1 = True
flag2 = False
flag = flag1 and flag2
print(flag) # 结果为False
flag = flag1 or flag2
print(flag) # 结果为True
flag = not flag1
print(flag) # 结果为True
数字类的比较运算如下:
# 数字类比较有大于> ,小于< ,等于== ,大于等于>= ,小于等于<= ,不等于!=
a = 10
b = 12
print(a > b) # 输出结果是False
print(a < b) # 输出结果是True
print(a == b) # 输出结果是False
print(a >= b) # 输出结果是False
print(a <= b) # 输出结果是True
print(a != b) # 输出结果是True
对于两个数字是否相等用的是==
,应为一个等于号=
代表的是赋值,不等于用的符号是!=
。
字符串、列表、字典可以通过in运算符进行成员的一个包含判断。字符串查找子串,列表查找成员、字典查找key。
intf_show = 'Eth1/1 is up'
up = 'up' in intf_show
print(up) # 因为字符串中出现过'up',故结果是True
intfs = ['Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4']
print('Eth1/7' in intfs) # 由于端口中无Eth1/7,故返回False
dev_info = {'ip': '192.168.1.1',
'name': 'as01',
'manufacture': 'huawei',
'series': 'ce6800',
'ports_sum': 48}
print('ssh_port' in dev_info) # 由于此字典中无ssh_port这个key,所以返回False
not in 进行一个不包含的计算。
intf_show = 'Eth1/1 is up'
down = 'down' not in intf_show
print(down) # 因为字符串'down'不在intf_show中,故结果是True
元组(tuple)是有序的、不可变的一组数据,与列表及其相似,除了一点,元组成员不可变(不开增加、删除、修改)。
可以通过索引访问,可以切片。
其创建方式为用圆括号括起成员,逗号间隔,成员可以为任意数据类型。
intfs = ('Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4')
intfs = ('Eth1/1',) # 只有一个成员的元组,也一定要注意在成员后面加一个逗号
dev_info = ('192.168.1.1', 'as01', 'huawei', 'ce6800', 48, ['beijing', 'dc01'])
其访问和切片方式与列表完全相同。
它有哪些使用场景呢?(针对初学者)
1特性不必多说,我们通过一段代码看看2特性:
dev_info = ('192.168.1.1', 'as01')
dev_ip, dev_name = dev_info
print(dev_ip, dev_name) # 输出结果192.168.1.1 as01
有些函数或者方法会返回多个数据,这个时候会自动封装成元组。通过上述方法可以非常方便的进行赋值,而不需要索引访问。
集合(set)是无序的、不可重复的一组数据,与列表类似,但是它的特点是成员不重复,我们在初始化的时候传入多个值相等的成员时,集合会自动去重保留一个。
其创建方法是使用花括号{}
创建,成员中间用逗号隔开。对于初学者而言,成员的数据类型建议大家锁定在数字和字符串。
不建议使用其他数据类型,比如集合的成员不能是字典,会报TypeError: unhashable type: 'dict'
的错误,集合是通过哈希了成员之后,根据哈希值去重,字典无法哈希,进而报错,大家了解即可。
intfs = {'Eth1/1', 'Eth1/2', 'Eth1/3', 'Eth1/4','Eth1/3', 'Eth1/4'}
print(intfs) # 结果是{'Eth1/2', 'Eth1/4', 'Eth1/1', 'Eth1/3'},顺序每次都可能变化,因为集合是无序的一组数据。
allow_vlan = {200,200,201,203,204}
print(allow_vlan) # 结果是{200, 201, 203, 204}
# 会报TypeError: unhashable type: 'dict'的错误
# set是哈希了成员之后,根据哈希值去重,字典无法哈希,进而报错,大家了解即可
err_set = {{'intf_name':'Eth1/1','desc':'test'}}
对于初学者,可以使用集合进行去重。
Python对基础数据提供了类型转换,比如用int函数将数据转为整数,float将对象转为浮点数,str将对象转为字符串,list将对象转为列表,tuple将对象转为元组,set将对象转为集合。其中列表、元组、集合可以通过对应函数相互转换,但是可能会丢失部分信息,比如将列表转为结合的时候丢失了排序和一部分成员。
以上均不改变原来的数据的值,而是新返回一个数据,大家按需将返回值赋值给一个新的变量或者是赋值给原有的变量。
# type函数可输出变量的类型
a = '1'
a = int(a)
print(a, type(a)) # 输出1
a = '1'
a = float(a)
print(a, type(a)) # 输出1
a = 100
a = str(a)
print(a, type(a)) # 输出1
a = (1, 2, 3)
a = list(a)
print(a, type(a)) # 输出 [1, 2, 3]
a = [1, 2, 3]
a = tuple(a)
print(a, type(a)) # 输出 (1, 2, 3)
a = [1, 2, 3, 3, 3]
a = set(a)
print(a, type(a)) # 输出{1, 2, 3} ,丢失了成员,顺序也无法保证。
python的判断语句使用if开始进行,搭配使用elif或者else。
if后面进阶条件判断表达式和冒号,条件表达式是能得到布尔值的一段表达式,符合条件则进入对应的代码块(注意缩进,相对if缩进4个空格)。
elif是在条件表达式不满足后继续进行一种判断。
else是用于以上条件均不满足的情况。
在判断中,if必须有,elif和else根据实际需要添加,且else一般在最后。
其形式如:
if <条件表达式1>:
<代码块1>
elif <条件表达式2>:
<代码块2>
elif <条件表达式3>:
<代码块3>
else:
<代码块4>
示例:
intf_status = 'up'
if intf_status == 'up':
print('端口up,正常')
elif intf_status == 'down':
print('端口up,异常')
else:
print('未知端口状态')# 写两行,缩进体现出这两行是一段代码块
print('端口状态{}'.format(intf_status)) # 写两行,缩进体现出这两行是一段代码块
if intf_status == 'up':
print('端口up,正常')
else:
print('端口up,正常')
结合字符串的一些方法,对于新手而言,可以解析很多网络配置信息,当然这不是解析网络配置的最优解,对于新手而言,是最快捷最易掌握的方法,或者在一些特别简单的情况下,我们通过字符串的操作就可以提取出信息,而无需动用正则或者textfsm(后续篇章会讲)。
line = '''Eth1/1 1 eth trunk up none 1000(D) 11'''
if line.startswith('Eth'):
intf_info = line.split()
intf_name = intf_info[0]
intf_stauts = intf_info[4]
# 在以下行输出端口名称和状态
print('intf_name:{},satus:{}'.format(intf_name, intf_stauts))
else:
print('此行,未发现端口信息')
这段代码中,我们通过对一行show命令的回显进行了信息的提取,通过字符串的startswith方法判断是否为端口的回显,再用split方法切割字符串,将返回的字符串列表赋值给一个新的列表变量,然后通过列表的索引获取端口中的信息,将端口名称和状态打印出来,实际可以在后续输出到表格中,同时对状态进行一些判断实现类似巡检的功能。
诸如此类,我们可以提取出网络设备的软件版本,同时与基线比对,进行巡检等等。打印只是初学阶段用于调试和执行结果反馈的一种方法,更高级的方法是将获取的信息输出到表格,甚至是数据库中,大家一定要有这个意识。
在判断的示例中我们进行了一行回显的信息提取,在实际工作中,我们的回显一定是多行的,这个时候我们提取信息,可以通过循环处理每行文本进行信息的提取。
在Python中有两种循环,一种是for循环,一种是while循环。
Python的for循环可以不通过索引非常方便的遍历访问序列(包含但不局限于字符串、列表、元组、集合、字典)。
其形式如:
for i in <序列>:
<代码块>
其中每次循环i会被自动赋值此次访问的成员(字典的遍历i每次是key),并且i是一个局部的变量,严格意义上只有在此代码块内才可以被访问使用,否则容易引起程序报错,或者程序未按预期执行。
a = [1, 2, 3]
# a = (1, 2, 3)
# a = {1, 2, 3}
for i in a:
print(i)
'''输出结果如下:
1
2
3
'''
字典的for循环比较特殊,它访问的是key。如果要取对应的value,需要通过key去访问。
dev_info = {'ip': '192.168.1.1', 'name': 'as01'}
for i in dev_info:
print(i)
print(dev_info[i]) # 将对应i作为key传入,取出对应的value。
'''输出结果如下:
ip
192.168.1.1
name
as01'''
我们也可以通过字典的一个特殊方法items,它会返回一个元组的列表(这个描述并不准确,但是方便理解),这个列表的成员时一个元组,key与value,我们通过非常方便的赋值方法给k,v这两个局部变量。
dev_info = {'ip': '192.168.1.1', 'name': 'as01'}
for k, v in dev_info.items():
print(k)
print(v)
'''输出结果如下:
ip
192.168.1.1
name
as01'''
print(dev_info.items(),type(dev_info.items()))
'''输出结果如下,其实返回的是一个特殊的数据结构较多dict_items,但是我们可以简单理解为返回了元组的列表
dict_items([('ip', '192.168.1.1'), ('name', 'as01')])
'''
while的行使如下:
while <条件表达式>:
<代码块>
只要条件满足(条件表达式为True),就不断循环,条件不满足时退出循环。
示例为一个计算1到100的和的代码。
i = 1
end = 100
sum = 0
while i <= end:
sum = sum + i
i = i + 1
print(sum) # 输出结果5050
我们定义了起始值i,结束值100,和初始值为0。然后进入while循环,判断条件时起始值i小于等于结束值。在代码块内,我们先求和,用上次循环的和加上这次i的值,然后把i向下一个数字移动。
循环中经常还会用到两个关键字continue与break(二者只出现在循环中):
这两个关键字我们在后续的代码演示中体会。
练习:要查询某交换机up的端口及其信息
# 计数器i
i = 0
# 端口数目
intfs_num = len(intfs)
# up的端口列表初始化值为空列表
up_intfs = []
# 进行循环,当计数器小于端口数目时可以循环
while i < intfs_num:
intf = intfs[i]
if intf['status'] == 'up':
# up端口追加成员
up_intfs.append(intf)
# 对计数器进行累加
i = i + 1 # 等同于 i += 1 ,这是一种简便写法,python中不同其他语言,无i++这种写法。
print(up_intfs) # 输出结果为[{'name': 'Eth1/1', 'status': 'up'}, {'name': 'Eth1/2', 'status': 'up'}, {'name': 'Eth1/4', 'status': 'up'}]
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,我们把常用的一个功能封装成函数,这样在需要使用这个功能时调用对应的函数即可,它的出现使得代码的开发效率大大提高。比如登录设备执行多条命令,结果返回的是回显的文本,这样我们批量对几十上百台设备进行相关功能时,每个设备执行一次这个函数即可。函数的封装可以让代码更加简洁,从而提高可读性。
函数的定义方式如下:
def func_name(<参数1>,<参数2>,..,<参数N>):
<代码块>
return <返回值>
其中参数可以是零个,也可以是多个。参数可以给默认值,默认值的参数一定要在无默认值参数的后面。
def gen_intf_desc_configs(intf_name,description='NetDevOps'):
# 模板字符串
intf_desc_config_tmpl = f'''interface {intf_name}
description {description}
'''
# 通过format函数格式化出配置
configs = intf_desc_config_tmpl.format(intf_name=intf_name,description=description)
# 配置以行为单位,整理成列表,返回配置
return configs.splitlines()
函数要先定义才能调用,调用的时候直接函数名,给各个参数赋值即可。
赋值的逻辑有两种,按位置赋值和按参数名赋值。两种方式可以结合,按位置赋值的方法排在按参数赋值的方法
按位置赋值,就是按照参数的位置一一对应,Python会帮我们自动对齐参数和所赋值,一定要注意顺序,顺序不对可能导致代码错误,示例:
intf_name = 'Eth1/1'
description = 'configed by netdevops'
configs = gen_intf_desc_configs(intf_name,description)
有默认值的参数我们可以不赋值,但是函数会使用默认值。
# 这个时候函数的description参数会使用默认值NetDevOps
configs = gen_intf_desc_configs(intf_name)
按参数名赋值,顺序可以调整,但建议尽量按顺序赋值,我们显示的告诉函数每个参数我们赋值为多少,示例
configs = gen_intf_desc_configs(intf_name=intf_name,description=description)
# 等同于
configs = gen_intf_desc_configs(description=description,intf_name=intf_name)
我们也可以按位置和按关键字混合使用,但对于新手最佳实践我们总结:
configs = gen_intf_desc_configs(intf_name,description=description)
面向对象编程是一门稍微深奥一点的知识了,最初学阶段,我们做简单讲解,方便后一些其他包的理解与使用,不做过深入的讲解。
面向对象首先要搞清楚的是类(Class)与对象(Object)。类是对一类事物的抽象总称,比如人类、交换机、网络设备、端口。对象是类的一个实例化,是一个相对而言的实体,比如小明是具体的一个人,as01交换机是具体的一个交换机,as01交换机的Eth1/1端口是一个具体的端口,这些都是具体的,是对象。在编程中,根据类创建一个对象的过程,我们称之为实例化。
类的定义与对象的实例化,我们理解即可。
首先一个比较简单的类的定义如下:
class Switch(object):
description = '提供交换能力的网络设备'
def __init__(self, ip, name, username, password):
self.ip = ip
self.name = name
self.username = username
self.password = password
self.connect() # 调用方法进行登录连接
def connect(self):
print('使用用户:{}登录交换机完成'.format(self.username))
def send_commad(self, cmds):
for cmd in cmds:
print('发送命令{}成功'.format(cmd))
类首先要按照class <类标识符>的形式进行类的定义。
description时候类属性,这类事物所共同拥有的一个属性。
__init__
是构造方法,类中的函数称为方法(method)。实例化的时候调用此方法进行赋值初始化。
self.ip
等变量是对象属性,属于实例化后的对象拥有的属性,不共享。
在初始化方法中可以执行一些复杂的方法或者函数。
connect和send_commad这种以self开头的方法,是对象方法,只有实例化后才可以调用,其中self是指实例化的那个对象,是一种约定俗称的写法,调用对象方法的时候self无需也不能赋值,这个是由Python自动完成的,我们只需要关注self以后的参数。
定义好类之后,我们就可以使用类名进行实例化出一个对象。
# 实例化,<类名>(<__init__方法的参数赋值>)
dev = Switch(ip='192.168.1.1',name='as01',username='admin',password='admin123')
# 使用点来访问对象属性和类属性,<对象>.<属性名>
dev_ip = dev.ip
# 使用点来调用对象方法 <对象>.<对象方法>(<参数>)
cmds = ['show version','show clock']
dev.send_commad(cmds)
对象实例化只需通过类名结合__init__
方法的调用即可实例化,形如<类名>(<__init__方法的参数赋值>)
访问对象属性和类属性,直接在对象后使用标点.
就可以访问。
调用对象的方法也是通过点.
接方法名,跳过self这个参数,赋值其他参数即可。
以上这个类是一段伪代码,用于大体描述一个交换机的类,实际情况与远比这个复杂。大家可能有疑问,已经有了函数可以提高我们的编程效率,为什么还要搞出一个对象呢?面向对象编程有什么好处吗?
这个问题实际非常复杂,初学者可以不必过多纠结,在日后的使用中慢慢体会,不严谨的简单理解:面向对象编程的抽象层次更高,开发效率更高。函数式编程可以做的事情面向对象都可以搞定,但是反之,是不可以的。
作为初学者,日常脚本的编写在函数式编程即可,我们也会以函数式编程为重点讲解,在初学阶段简单了解面向对象编程,可以让我们对自己一些工具包的使用了解的更加清晰。在Python中万物皆为对象,我们前面讲的基础数据结构也是对象,只不过是实例化和调用方法时Python帮我们处理好,不同于普通的对象实例化。在了解了面向对象编程之后,回过头去再看字符串的方法,我们可能了解的更清楚,且为我们后面学习更高阶的知识做好准备。
网络日常运维与纯文本的相关处理是十分常见的操作,纯文本可以简单理解为记事本类应用可以打开、显示出来字符串的文档,包括Python代码纯从编辑的角度来看也是一个纯文本文件。这些文件有别于Excel表格、Word文档、JPG图片等有特殊数据结构和格式的文件,它们需要特殊的软件才能打开。
Python也提供了非常便利的函数,让我们可以访问文件(纯文本类和复杂类),其方式是通过open函数,我们本节只讨论纯文本类,对其操作也局限在字符串层面的相关内容。
关于open函数的参数说明如下:
open(name=<文件名>,mode=<读写模式>,encoding=<字符集>)
r
、w
与a
,即读、写、追加(可以理解为写的一种特殊模式),对应单词read、write与append,方便记忆。读模式文件不存在,代码会报错。写模式与追加模式,如果文件不存在,Python会自动创建一个文件。日过文件存在,写模式会覆盖,而追加模式会在文本后追加我们写要入的内容。在使用这个函数之前,我们先定几个重要的结论,再来看代码,可以有效避坑:
对文件操作我们使用open函数创建一个文件对象,实际上是一个句柄指向了操作系统的文件对象,我们执行read方法,可以对文本文件操作完成之后,我们需要关闭文件句柄(调用文件对象的close方法),以防止读写冲突之类的事情放生。
示例代码:
首先我们创建一个文本netdevops.txt,内容如下:
this is a book about “NetDevOps”!
这是一本关于NetDevOps的书!
代码部分如下:
# 我们用IDE创建一个文件,叫做netdevops.txt,编码采用utf8字符集
f = open('netdevops.txt', mode='r', encoding='utf8')
print(f, type(f))
# 上述会输出<_io.TextIOWrapper name='netdevops.txt' mode='r' encoding='utf8'>
content = f.read()
print(content) # 输出我们文件的内容,字符串
f.close() # 关闭文件对象
但是这个过程显得比较麻烦,因为我们每次要显式地关闭,这段代码没问题,但是不够pythonic,所以我们使用with来实现更加pythonic的代码。
with open('netdevops.txt', mode='r', encoding='utf8') as f:
content = f.read()
print(content) # 输出我们文件的内容,字符串
with后面接open函数,使用as 关键字将文件对象赋值给f,然后是一个冒号,后面与判断循环格式一样,需要缩进控制代码块,当代码离开了with管辖的代码块,Python会自动执行对象的close方法。如果我们读取一个已经关闭的文件,程序会报错,示例如下:
with open('netdevops.txt', mode='r', encoding='utf8') as f:
content = f.read()
print(content) # 输出我们文件的内容,字符串
f.read() # 此处会报错,ValueError: I/O operation on closed file.
read方法会一次性读取文本的全部内容,返回一个字符串。如果我们按行处理的时候需要使用字符串的splitlines方法,它会自动帮我们切割成一行一行的字符串列表,然后我们可以结合之前所学,逐行去处理文本提取有用信息。这种一次性读取的方式日常使用没问题,但当文本文件非常大的时候,使用这种方法会出现内存溢出的风险,比如我们收集了防火墙很长一段时间的访问记录,可能是10多个G,如果使用read方法无法胜任。
遇到文本文件特别大的时候,我们可以使用for循环逐行读取内容,这也是一种非常pythonic的方法。
with open('netdevops.txt', mode='r', encoding='utf8') as f:
for line in f:
print(line)
这个输出结果是
this is a book about “NetDevOps”!
这是一本关于NetDevOps的书!
原因是每行的换行都保留了,实际不影响我们进行信息提取,大家知晓其原理即可:print函数会默认换行,for循环逐行读取文本,文本末尾的换行也保留。
open函数处理文本字符串写入的时候,只需要将模式mode按需赋值为w
或者a
,此处我们仅演示w
模式即可,一定要注意字符集采用utf8,然后调用文件对象的write即可。
with open('netdevops_w.txt', mode='w', encoding='utf8') as f:
content = '''this is a book about “NetDevOps”!
这是一本关于NetDevOps的书!'''
f.write(content)
write方法负责将字符串写入到指定的文本文件中,若文本不存在则创建,若文件存在w
模式会覆盖,a
模式会追加。以上代码会创建一个netdevops_w.txt
的文本文件。
在NetDevOps自动化的开始阶段,在我们不能熟练操作数据库的时候(当然我们直至比较后期也可以不掌握),表格是一种非常理想的数据承载形式,比如用于记录我们的网络设备清单,以便后续自动化,用来存储我们巡检的结果,比如每台设备的端口清单、软件版本、开启的特性等等。
其实我们一直我们做NetDevOps特别容易陷入一个误区,想通过自己编写脚本来实现配置的批量推送,减少自己的工作量。而在实际工作中,尤其是在一些非常重要的网络区域,这是风险非常高的事情。我们不如换个思路,我们可以通过NetDevOps脚本来实现设备信息的提取与整理加工,这是一件性价比高、风险低的事情,且日常信息的统计维护也是我们网络运维占比非常大的一件事情。
Python本身提供了CSV的表格的相关工具包,且也有很多第三方包可以处理表格。我们的核心还是希望大家聚焦网络业务逻辑,所以我更推荐大家使用pandas这个第三方工具包,因为在不关心格式的情况下,它对表格的读与写在代码层面真是达到了极简,且可以非常完美地与初学者所掌握的基础数据类型、知识结合。
pandas其实是一个科学计算、数据分析的包,这二者涉及到很多表格数据的导入导出,所以pandas非常好得实现了对表格的处理。
由于它是一个第三方Python工具包,所以需要安装到我们的环境中,所使用的工具是Python自带的包管理工具——pip,只要我们按照前期文章指导安装了Python且设置了环境变量。打开一个命令行窗口,执行pip install pandas openpyxl即可,它会基于我们的环境和Python版本帮助我们自动下载最适合、最新的版本。
注:pip install <包名>
即可下载最新版,或者pip install <包名>==<版本>
可以安装指定版本。多个包名用空格隔开。。本书所使用的pandas版本为1.4.2。
安装好pandas,如果我们想使用需要将pandas导入,通过import语句即可:
import pandas as pd
如果我们觉得某个包名或者导入整个路径比较长的时候,可以考虑使用as给这个引入的模块起个别名,这段代码里我们通过且只能通过别名使用模块。
我们创建两个设备列表的表格inventory.csv和inventory.xlsx,其数据格式如下:
name | hostname | platform | port | username | password | city | model | netmiko_timeout | netmiko_secret |
---|---|---|---|---|---|---|---|---|---|
netdevops01 | 192.168.137.201 | cisco_ios | 22 | netdevops | admin123! | bj | catalyst3750 | 60 | admin1234! |
netdevops02 | 192.168.137.202 | cisco_ios | 22 | netdevops | admin123! | shanghai | catalyst3750 | 60 | admin1234! |
我们先用pandas的read_excel函数读取Excel表格,然后将数据加载成字典的列表:
import pandas as pd
devs_df = pd.read_excel('inventory.xlsx')
devs = devs_df.to_dict(orient='records')
print(devs)
'''结果输出一个字典的列表,截取部分
[{'name': 'netdevops01', 'hostname': '192.168.137.201'... 'netmiko_secret': 'admin1234!', 'netmiko_fast_cli': 0}]
'''
这段代码记住即可,我们不必深究pandas的其他使用。读取表格的时候传入表格文件名称,默认会读取第一个sheet的数据,我们也可以指定sheet。在read_excel函数中赋值sheet_name为对应的页签排序,当然还是从0开始排序的,也可以将sheet_name赋值为我们想读取sheet的名字。
表格数据中的数字类pandas会优先帮我们转为整数或浮点数,这点大家使用中需要注意,大家可能按需使用对应函数做类型转换。
pandas的read_excel函数读取表格将数据加载成了dataframe(pandas特有的数据结构),我们调用dataframe的to_dict的方法,并赋值orient为records即可,
这样就会返回一个列表数据,其成员为字典,没个成员对应一条数据。
读取csv文件使用的是函数read_csv,对于初学者,只需要传入csv文件名即可:
devs_df = pd.read_csv('inventory.csv')
devs = devs_df.to_dict(orient='records')
print(devs)
'''结果输出一个字典的列表,截取部分
[{'name': 'netdevops01', 'hostname': '192.168.137.201'... 'netmiko_secret': 'admin1234!', 'netmiko_fast_cli': 0}]
'''
比如我们想对一堆设备批量执行自动化,我们可以将设备写入如上表格,通过pandas读取设备列表,然后使用for循环一台一台的进行自动化。这里面我们没有使用并发,对于初学者,我不太建议使用并发,建议在逐渐熟练后学习掌握并发,因为其学习成本偏高且代码会比较丑陋。退一步说,没有并发也并不妨碍自动化可以提高运维效率这个事实,大批量的动作代码一定比人省事,我们可以处理其他事情。随着技术精进,我更推荐大家使用自动化框架Nornir来实现并发部分,这个后续章节我们展开讲。
在进行NetDevOps开发的过程中,我们也会提取出很多信息,尤其是像端口类这种多条数据的,我们就可以通过pandas将结果写入到表格中,用于许多其他场景,比如资源统计类、巡检类、查询类等。
pandas进行数据写入的操作,我们也基于字典的列表这种形式,因为我们提取信息时,一般都是将一个对象的多个字段信息放在一个字典中,实际pandas支持多种方式的加载。
首先我们通过字典的列表创建一个Dataframe的数据对象,然后调用这个对象的to_excel
函数即可写入到Excel文件中。如下:
import pandas as pd
raw_data = [{'name': 'Eth1/1', 'desc': 'netdevops1'},
{'name': 'Eth1/2', 'desc': 'netdevops2'}]
intf_df = pd.DataFrame(raw_data)
print(intf_df)
''' Dataframe从打印的结果可以看到 是一种二维矩阵的数据,非常符合我们的使用习惯
name desc
0 Eth1/1 netdevops1
1 Eth1/2 netdevops2
'''
intf_df.to_excel('as01_info.xlsx', sheet_name='interfaces', index=False)
to_excel
函数,第一个传入的是文件名(我们可以这么简单理解,实际情况比较复杂),另外两个重要的参数是sheet_name和index。
sheet_name告诉我们数据写在哪一个sheet里,我们可以用数字(代表第几页,从零开始排序),也可以用字符串创建并写入指定的sheet。
index一定要置为False,index是pandas中的一个索引值,如同代码中打印的每行数据前的数字0和1,如果我们用默认参数,则会在输出的表格文件中多出一列数据,从0开始,不符合我们的使用习惯。