【从零开始NetDevOps】第二章网工速通Python

《从零开始NetDevOps》是本人8年多的NetDevOps实战总结的一本书(且称之为书,通过公众号连载的方式,集结成册,希望有天能以实体书的方式和大家相见)。

NetDevOps是指以网络工程师为主体,针对网络运维场景进行自动化开发的工作思路与模式,是2014年左右从国外刮起来的一股“网工学Python"的风潮,最近几年在国内逐渐兴起。本人在国内某大型金融机构的数据中心从事网络自动化开发8年之久,希望能通过自己的知识分享,给大家呈现出一个不同于其他人的实战为指导、普适性强、善于抠细节、知其然知其所以然风格、深入浅出的NetDevOps知识体系,给大家一个不同的视角,一个来自于实战中的视角。

由于时间比较仓促,文章中难免有所纰漏,敬请谅解,同时笔者也会在每个章节完成后进行修订再发布,欢迎大家持续关注

本系列文章会连载于“NetDevOps加油站”公众号,欢迎大家点赞关注
【从零开始NetDevOps】第二章网工速通Python_第1张图片

第二章 Python基础

​ 在第一章,为大家分享了我对NetDevOps的认知,NetDevOps发展的几个重要阶段,同时简单介绍了Python这门开发语言,本章节开始我们要开始硬核之旅,带领大家从零开始掌握Python,只有掌握了Python这门开发编程语言,我们才能够按照我们的需求编写脚本乃至工具平台。

​ Python是一门十分简单容易上手的编程语言,对于网络工程师而言,掌握Python这件事并不苦难,大家所要做的就是按部就班地掌握每个小节的知识点,并亲自将它敲出来!

​ 本章节,我会从网络运维的知识体系,将Python的为大家讲解清楚,讲解最核心的部分。当新手面对一门语言,个人认为,我们需要尽快地掌握最基础、最核心的知识,快速动手编写有成效的脚本,这样才会有行进下去的动力。很多初学者最容易犯的错误,可能就是买了一本非常好的书,从头看到尾学完之后,说懂吧,写不出来有用的代码(减少自己的工作量的脚本),说不懂吧,他还能写出来一些简单的脚本,比如比数字大小、猜数字等。这也与NetDevOps的理念不合,我们并没有把开发应用到日常运维之中。如何避免这种窘境?最佳的实践方法就是掌握最核心最基础的语法,以解决实际工作中遇到的问题为行进方向,不要被庞大的编程语言体系淹没在学习的迷雾之中。通过小步快跑的方式,以基础语法为核心,以解决实际问题为宗旨,不断扩展这么语言的学习范围和深度,实现滚雪球的效应,在一个个简单的脚本中看到光,在一个个问题的解决之后找到自信!

​ 同时在学习知识的过程中要学会举一反三,触类庞通。比如很多书籍以实际生活中的例子来讲基础语法,我们作为网络工程师,就要学会将这些情景映射到网络运维之中,类比网络运维。

​ 工欲善其事必先利其器,首先让我们一起动手来搭建一个Python的开发环境吧!

2.1 版本选择与环境搭建

我该选择哪个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,而对于初学者而言,往往搞不清楚自己究竟用的是哪个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代码。

总结
  1. 新手建议选择Python3.9。
  2. 新手建议电脑中只有一套Python环境。

2.2 IDE的推荐与设置

安装好了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有很多的便利用法,我们在后续文章中逐步展开。

2.3 速学Python的基础知识

在正式学习Python之前,我们先准备一些基础的知识,以便我们后续章节的讲解。

这部分内容,同时又需要在后续编程中不断加深理解。

2.3.1 标识符及变量

​ 标识符是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

2.3.2 保留字

​ 每门语言运行都会有一些特殊的语法,比如用if去做判断,我们不能用if来作为变量名(包括其他标识符也不行),这类我们称之为保留字(其他语言也称关键字)。Python有33个保留字,如下表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lHKmgea9-1658882259416)(assets/image-20220527233446959.png)]

2.3.3 缩进

​ 有别于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个空格。

2.3.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)

2.3.5 输出

​ 输出的方式千千万,我们这里讲的是用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;

2.4 基础数据类型

每个编程语言都有自己独特的基础数据类型,我们耳熟能详的比如整数、字符串、数组等等,其他所有的复杂对象都是从这些基础的数据结构衍生出来的。。

Python的基础数据类型包含以下几种:

  1. 数字
  2. 字符串
  3. 列表
  4. 字典
  5. 元组
  6. 集合
  7. 布尔

下面我们依次展开讲解各个数据类型

另外还有一个特殊值,空值None,类似于其他语言中的NULL,它代表是一个空对象(它不是基础数据类型,但是是一个非常简单且需要掌握的数据类型),不做过多展开。

2.4.1 数字

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

2.4.2 字符串

​ 字符串是非常重要的一个数据类型,它是以引号括起来的一段文本。引号可以是单引号、双引号、三引号(三个单引号或者三个双引号),但是一定要成对出现,引号中间的文本内容是字符串承载的数据。字符串是严格区分大小写的。只写一对引号,内容为空,我们称之为空字符串。在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

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

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开发中可以用于判断回显是否包含关键字。

startswth

startswith方法用于判断是否以给定的字符串开始的,返回是真(True)或假(False)。

intf_show = 'Ethernet1/1 is up'
is_interface_line = intf_show.startswith('Ethernet')
print(is_interface_line)  # 输出结果是Ture
endswith

endswith方法用于判断是否以给定的字符串结束的,返回是真(True)或假(False)。

intf_show = 'Ethernet1/1 is up'
interface_up = intf_show.endswith('up')
print(interface_up)

find 、startswith、endswith主要用于在文本中发现是否有关键字,通过关键字我们可以判断一些状态,或者确定此行中是否有我们要提取的信息等等。

split

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方法用去去除字符串左右的指定字符串,不修改原来的字符串(因为字符串是不可修改的类型),返回一个新的字符串。

默认是去除左右的所有空白符。

我们也可以在方法内直接传入要去除的字符串,同时strip还有两个变种方法lstrip和rstrip,可以只去除左侧或者右侧的指定字符串,单这些NetDevOps脚本编写很少涉及,故不演示。

intf_show = '    Ethernet1/1 is up    '
result = intf_show.strip()
print(result)  # 结果是"Ethernet1/1 is up"
splitlines

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

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"

对于初学者而言,多用于按照一定格式拼接多个字符串。如果是这种情况,更推荐字符串格式化这种方式。

以上几个字符串的相关使用,结合判断、循环,我们就可以写出一些复杂的逻辑,对网络配置进行解析,提取出我们想要的信息。这个思路,我们在掌握了判断循环后再去编写代码实现。

2.4.3 列表

列表(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)

合并列表

两个列表合并有两种方式:

  1. 使用加法,两个列表合并成一个新的列表,原有的两个列表没有任何变化
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']
  1. 使用extend方法,将另外一个列表B批量追加到调用方法的列表A之后,只有调用方法的列表A发生变化,有了新的成员。
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)

2.4.4 字典

字典(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方法,它的逻辑是:

  1. 如果有对应的键值对,返回其值,
  2. 如果无则返回一个默认值,如果我们没指定默认值,则返回特殊的空值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进行赋值:

  1. key value对存在,再次赋值会更新原有的值
  2. key value对不存在,会创建新的k,v对
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)

在这个过程中我们也发现,新添加的成员位于字典的最后。

2.4.5 布尔

布尔(bool)只有真或假两个值,对应True和False,在进行判断的时候非常有用。

数字、字符串、列表、字典等有一些运算是可以得到一个布尔值的。比如比较数字的大小返回的就是布尔值,某成员是否在列表内,某key是否在字典里出现。

逻辑预算且、或、非

布尔值可以进行逻辑运算,也称布尔运算。

包含三种运算:

  1. 且 ,and连接左右布尔值,左右布尔值均为真,结果才为真,否则为假。
  2. 或 ,or连接左右布尔值,左右布尔值有一个为真,结果为真,否则为假。
  3. 非,not后接布尔值,取反操作,如果布尔值为真则结果为假,如果布尔值为假,则结果为真。
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 与 not in获得布尔值

字符串、列表、字典可以通过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

2.4.5 元组

元组(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. 方便一次性赋值给变量

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

有些函数或者方法会返回多个数据,这个时候会自动封装成元组。通过上述方法可以非常方便的进行赋值,而不需要索引访问。

2.4.6 集合

集合(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'}}

对于初学者,可以使用集合进行去重。

2.4.6 类型转换

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} ,丢失了成员,顺序也无法保证。

2.5 判断循环

2.5.1 判断

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方法切割字符串,将返回的字符串列表赋值给一个新的列表变量,然后通过列表的索引获取端口中的信息,将端口名称和状态打印出来,实际可以在后续输出到表格中,同时对状态进行一些判断实现类似巡检的功能。

诸如此类,我们可以提取出网络设备的软件版本,同时与基线比对,进行巡检等等。打印只是初学阶段用于调试和执行结果反馈的一种方法,更高级的方法是将获取的信息输出到表格,甚至是数据库中,大家一定要有这个意识。

2.5.2 循环

在判断的示例中我们进行了一行回显的信息提取,在实际工作中,我们的回显一定是多行的,这个时候我们提取信息,可以通过循环处理每行文本进行信息的提取。

在Python中有两种循环,一种是for循环,一种是while循环。

for循环

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的行使如下:

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(二者只出现在循环中):

  1. continue代表结束此次循环,既continue后的代码块不再执行
  2. 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'}]

2.6 函数

函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,我们把常用的一个功能封装成函数,这样在需要使用这个功能时调用对应的函数即可,它的出现使得代码的开发效率大大提高。比如登录设备执行多条命令,结果返回的是回显的文本,这样我们批量对几十上百台设备进行相关功能时,每个设备执行一次这个函数即可。函数的封装可以让代码更加简洁,从而提高可读性。

函数的定义方式如下:

  1. 以def开头
  2. 空一个编写函数名,函数名建议使用蛇形命名法
  3. 函数名后圆括号,括号内可以编写参数,也可以不写参数
  4. 圆括号后接冒号,回车,一个缩进(4个空格),编写函数内的代码块。
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)

我们也可以按位置和按关键字混合使用,但对于新手最佳实践我们总结:

  1. 参数相对比较少,可以使用按位置赋值的方式
  2. 复杂参数的情况下,第一个参数使用按位置赋值,即我们可以直接传入值,其余参数,按顺序显示地使用参数名赋值
configs = gen_intf_desc_configs(intf_name,description=description)

2.7 类与面向对象

面向对象编程是一门稍微深奥一点的知识了,最初学阶段,我们做简单讲解,方便后一些其他包的理解与使用,不做过深入的讲解。

面向对象首先要搞清楚的是类(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帮我们处理好,不同于普通的对象实例化。在了解了面向对象编程之后,回过头去再看字符串的方法,我们可能了解的更清楚,且为我们后面学习更高阶的知识做好准备。

2.8 文本的操作

网络日常运维与纯文本的相关处理是十分常见的操作,纯文本可以简单理解为记事本类应用可以打开、显示出来字符串的文档,包括Python代码纯从编辑的角度来看也是一个纯文本文件。这些文件有别于Excel表格、Word文档、JPG图片等有特殊数据结构和格式的文件,它们需要特殊的软件才能打开。

Python也提供了非常便利的函数,让我们可以访问文件(纯文本类和复杂类),其方式是通过open函数,我们本节只讨论纯文本类,对其操作也局限在字符串层面的相关内容。

关于open函数的参数说明如下:

open(name=<文件名>,mode=<读写模式>,encoding=<字符集>)
  1. name是文件名,我们直接要操作的文件的相对路径或者绝对路径,含文件名。
  2. mode是对文件操作的模式,对于初学者,我们只需记住三种rwa,即读、写、追加(可以理解为写的一种特殊模式),对应单词read、write与append,方便记忆。读模式文件不存在,代码会报错。写模式与追加模式,如果文件不存在,Python会自动创建一个文件。日过文件存在,写模式会覆盖,而追加模式会在文本后追加我们写要入的内容。
  3. encoding是字符集一定要显式地指定utf8,Python非常灵活,各种形式的字符集(大小写短横线等,如utf8、UTF8、UTF-8)它都会自动识别出。

在使用这个函数之前,我们先定几个重要的结论,再来看代码,可以有效避坑:

  1. 在我们自己编写的代码体系里一定要用utf8字符集
  2. 读取别人的文本的时候选择utf8字符集另存为供程序使用,后续讨论不再赘述其他字符集
  3. 一定要用上下文管理关键字with来打开文件对象
  4. 一定要显式的赋值读写的模式以及字符集名称

文本的读取

对文件操作我们使用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的文本文件。

2.9 表格的操作

在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开始,不符合我们的使用习惯。

你可能感兴趣的:(NetDevOps加油站,从零开始NetDevOps,python,运维开发,自动化,运维)