第八章 NetDevOps 与数据格式

数据格式是描述数据保存的一种规则,保存的位置可以是文件、内存,保存的格式可以是文本、二进制。我们在日常工作中经常会听到一些数据格式的名词,比如在聊 RESTFul API (一种基于 HTTP  的接口规范)的时候会接触到 JSON、xml,在聊 Ansible (一款开源的配置工具)或者 IaC(基础设施即代码)的时候会接触到 yaml。

这些都是我们在运维开发中可能要接触到的、常见的数据格式。数据格式存在的重要意义之一,就是制定标准规范,在不同程序之间基于统一的规范进行数据交换。每个程序语言内部都是有自己的数据类型的,我们想让两个不同的程序之间通信,可能会发生驴唇不对马嘴的情况,解决方法就是通过统一的数据格式进行数据交换。比如我们用 Java 写的程序和用 Python 写的程序进行数据交换,可以通过 HTTP 的管道,传送基于 JSON 标准的文本,这样两种语言之间就可以互相通信了。这个过程中,数据通过各种通道,在不同的语言中传输转换的过程,我们可以统称为序列化与反序列化。

其中特定编程语言中的数据转化为可传输字节流的过程,我们称之为序列化。反之,将有序的字节流还原为特定编程语言中的数据对象的过程,我们称之为反序列化。

在 NetDevOps 的世界中,我们也不可避免地接触到各类数据格式,用于序列化与反序列化的相关操作,但基本也主要是 JSON、YAML 和 XML 这三种数据格式。虽然它们对自己有不同的定义,JSON 称自己为数据交换格式,YAML 称自己为序列化语言,XML 称自己为标记语言,但是我们这里结合使用而言,统称它们为数据格式。

接下来我们将为大家分别讲解三种数据格式的规范,以及如何使用 Python 去处理这三种数据格式的数据。三者的诞生时间先后顺序是 XML、JSON、YAML,但是在这里我们根据实际频率、重要程度,依次为大家介绍 JSON 、YAML 和 XML。

8.1 JSON

JSON ( JavaScript Object Notation )是一种轻量级的数据交换格式。它易于人们读写的同时,也易于被程序解析和生成。它最早是基于 JavaScript 的一个 ECMA-262 第三版的一个子集,它采用完全独立于编程语言的文本格式来存储和表示数据。然后逐渐被推广到其他编程语言,作为一种理想的数据交换语言。

早期的比较通用的用于数据交换的数据格式以 XML 为主,但是其可读性稍微差一点。JSON 更易读易写,加之其书写格式相对 XML 而言会节省很多字符,进而可以提高传输效率,所以 JSON 逐渐流行起来。JSON 的实际使用场景主要是用于HTTP API ,用户的请求和响应的数据都封装在 JSON 字符串当中。我们也可以把一些不是过于复杂的 Python 对象存以 JSON 的数据格式存储到文本文件中。

8.1.1 JSON的规范

JSON 只能储存两种数据——对象(object)和数组(array),二者可以简单类比 Python 中的字典和列表。我们可以参考如下两个示例。

示例1,对象类型的 JSON 数据:

{"name": "netdevops01","ip":"192.168.137.201"}

示例2,数组类型的 JSON 数据:

["192.168.137.201","192.168.137.202"]

对象类的数据类似 Python 中的字典,它是由若干个键值对( name/value )组成。这种数据使用大括号将数据包裹住,以 {   开始,以 } 结束。每个键值对之间用英文逗号隔开。键和值之间使用英文的冒号隔开。键必须是用双引号包裹的字符串(string),不允许用其他类型的数据格式,值可以为 object、array、string 、number、true 、false、null。

数组类的数据类似 Python 中的列表,它由若干个值( value )组成,使用方括号将成员们包裹,以 [ 开始, ] 结束,成员之间使用英文逗号隔开。成员(即值)的类型同上同对象数据类型中的值的类型,可以为 object、array、string 、number、true 、false、null。

对象和数组中都涉及到值,JSON 值的类型可以与Python中的基础数据类型有一定对照关系,可以参考下表(我们适当调整了顺序):

Python

JSON

说明

str

string

JSON 中必须以英文双引号包裹住字符串内容,如"netdevops",而不能是'netdevops'

int, float

number

JSON 中整数和小数的书写格式与 Python 一致,会自动判断其为整数还是浮点数

True

true

JSON 中无布尔值,用 true 代表真,请注意不要加双引号

False

false

JSON 中无布尔值,用 false 代表假,请注意不要加双引号

None

null

JSON 使用 null 代表空值,请注意不要加双引号

dict

object

JSON 中的 object 可以转化为 Python 中的字典

list, tuple

array

Python 中的列表和元组都会被转为 JSON 中的 array,它是一个有序的数组。

表 8-1  JSON 数据类型与 Python 数据类型的转化关系表

在了解到 JSON 的基本规范后,我们编写一个稍微复杂一点的 JSON 数据来进行巩固练习,我们用一个 JSON 数据来去描述一台网络设备,参考如下 JSON 文本:

{
  "name": "netdevops01",
  "ip": "192.168.137.1",
  "vendor": "huawei",
  "online": true,
  "rack": "0101",
  "start_u": 20,
  "end_u": 21,
  "interface_usage": 0.67,
  "interfaces": ["eth1/1","eth1/2","eth1/3"],
  "uptime": null
}

在这里我们写一个描述网络设备的 object,它的所有字段均用且只能用字符串表示,我们列举了几个常用的字段:name(名称)、ip(IP地址)、vendor(厂商)、online(是否在线)、rack(机柜编号)、start_u(开始 U 位)、end_u(结束 U 位)、interface_usage(端口使用率)、interfaces(端口列表)、uptime(运行时长)。这几个字段,几乎覆盖了所有的 JSON 数据类型。JSON 本质是一个字符串,为了可读性,我们适当调整了缩进,适当使用了换行。实际这段 JSON 文本全部写到一行中,把所有的间隔符号(冒号和逗号)之间的空格去掉,也是合法的。同时不同于 Python 的字典的写法,在 JSON 中我们最后一个键值对不允许有逗号。

大家一定要注意的是,在赋值某字段为 true、false、null 的时候,一定不要添加双引号,否则它代表的就是字符串,就不是这些特殊值本身了。

在实际编写中,我们有若干台网络设备,我们也可以将其编写到一个数组 array 中,这个数组的成员都是类似上述示例中的 object,可以参考如下 JSON 文本:

[
  {
    "name": "netdevops01",
    "ip": "192.168.137.1",
    "vendor": "huawei",
    "online": true,
    "rack": "0101",
    "start_u": 20,
    "end_u": 21,
    "interface_usage": 0.67,
    "interfaces": ["eth1/1", "eth1/2", "eth1/3"],
    "uptime": null
  },
  {
    "name": "netdevops02",
    "ip": "192.168.137.2",
    "vendor": "huawei",
    "online": true,
    "rack": "0101",
    "start_u": 20,
    "end_u": 21,
    "interface_usage": 0.67,
    "interfaces": ["eth1/1", "eth1/2", "eth1/3"],
    "uptime": null
  }
]

书写过程中,我们可以通过 IDE 对 JSON 文本进行一个格式化处理,调整缩进,让 JSON 更加易读。比如 Pycharm 调整代码的格式化快捷键是 Ctrl+Alt+L 的组合键。但是我们需要清除,JSON 对缩进、换行并不敏感,它更关注我们的键值对或者数组成员的书写规范。

8.1.2 Python 与 JSON 数据

Python 内置了 json 模块用于处理 JSON 数据与 Python 基础数据的相互转化。

json.dumps 函数

json 模块的 dumps 函数的主要功能是将一个 Python 的数据对象转化导出成为 JSON 文本(即一个遵循 JSON 规范的字符串)。对于这个函数的参数,作为初学者我们只需要掌握以下三个即可:

  1. obj,Python 的数据对象,对于初学者而言,这个数据对象只能局限在表 8-1 中出现的 Python 基础数据类型,如果我们想将一个复杂对象转为 JSON 文本,需要先将其转化为符合上述标准的 Python 数据对象。比如时间 datetime 是一个复杂的数据对象,在默认情况下进行转换会报错,我们可以先讲时间对象转换为字符串后再去进行 JSON 的转换。当然这是针对初学者的建议,我们实际也可以编写一个编码类( Encoder )来处理复杂对象和转换为 JSON 文本,但对于初学者不建议如此使用。
  2. indent,缩进,默认是 None,即 JSON 文本会以最紧凑的方式进行展示。我们也可以适当调整其为整数 2 或者 4,这样可以让 JSON 文本有一定的缩进,可读性更好。
  3. ensure_ascii,是否使用 ASCII 编码,默认为 True。如果我们的数据对象中含有中文,将此值设置为 False,这样转换为 JSON 文本的时候,中文内容不会编码,提高可读性。

以上参数的使用,可以参考如下代码:

import json

python_data = {'name': 'netdevops01', 'ip': '192.168.137.1',
               'vendor': '华为', 'online': True, 'rack': '0101',
               'start_u': 20, 'end_u': 21, 'interface_usage': 0.67,
               'interfaces': ['eth1/1', 'eth1/2', 'eth1/3'],
               'uptime': None}

json_text = json.dumps(python_data, ensure_ascii=False, indent=4)
print(type(json_text))
print(json_text)

这段代码的输入内容如下:

<class 'str'>
{
    "name": "netdevops01",
    "ip": "192.168.137.1",
    "vendor": "华为",
    "online": true,
    "rack": "0101",
    "start_u": 20,
    "end_u": 21,
    "interface_usage": 0.67,
    "interfaces": [
        "eth1/1",
        "eth1/2",
        "eth1/3"
    ],
    "uptime": null
}

我们使用 dumps  函数,将一个 Python 数据对象导出成为了 JSON 文本的字符串。我们使用的数据对象 python_data 中我们特意设置了一个字段其内容为中文,在调用 dumps 函数的时候,赋值 ensure_ascii 为False,将其转换导出成为 JSON 文本的时候,中文字符串得以保留,而不是经过 ASCII 字符集编码,让人无法阅读。同时我们还调整了缩进,将 indent 赋值为 4,这样输出的格式会更加可读。

我们也可以修改以上两个参数,去观察这两个参数的作用,这也是我们在学习时经常使用的一个方法,比如 ensure_ascii 不进行赋值(即使用默认值 True),则 vendor 字段的内容“华为”转换为 JSON 时会显示为"\u534e\u4e3a"。

json.dump 函数

我们将 Python 数据对象转换为 JSON 文本不是最终目的,更多时候这是一种中间状态,我们是想将 JSON 文本写入到二进制的方式进行存储或者网络传输。对于文件的存储,json 模块也提供了比较便利的函数 dump,它提供了将指定 Python 数据对象写入文件流的功能。这个函数名与 dumps 只差一个字母,为了方便记忆,大家记住 s 代表的是 string,即将 Python 数据对象转换为包含 JSON文本的字符串。dump 函数相对而言有四个参数,需要我们着重记忆:

  1. obj,即 Python 数据对象,它是第一个参数,同 dumps 函数的 obj 参数。
  2. fp ,作为初学者,我们认为它是一个打开了的可以写的文件对象。
  3. indent,缩进,同 dumps 函数的 indent 参数。
  4. ensure_ascii,是否使用 ASCII 编码,同 dumps 函数的ensure_ascii参数。

dump 函数会调用文件对象的 write 方法,将 Python 数据对象转换为 JSON 文本的字符串并写入指定文件,但是它没有返回值,只要代码不报错,我们就认为写入成功。dump 函数的使用可以参考如下代码:

import json

python_data = {'name': 'netdevops01', 'ip': '192.168.137.1',
               'vendor': '华为', 'online': True, 'rack': '0101',
               'start_u': 20, 'end_u': 21, 'interface_usage': 0.67,
               'interfaces': ['eth1/1', 'eth1/2', 'eth1/3'],
               'uptime': None}
with open('data.json', mode='w', encoding='utf8') as f:
    json.dump(python_data, fp=f, ensure_ascii=True, indent=4)

我们通过 with 上下文管理打开了一个文本文件,指定写模式、UTF8字符集编码,在调用 dump 函数的时候,第一个参数是 Python 数据对象,第二个是我们的文件对象。

代码成功运行后,在代码所在的目录会生成一个 data.json 的文本文件,后缀 json 是一种约定俗成的写法。我们在运行代码时的一些编程语言内部的数据可以转换为 JSON 并存储到文件中去,这个过程我们称之为序列化,这个名词大家了解即可。包括将编程语言内部的数据转换为 JSON 进行网络传输,也可以称之为序列化。

json.loads 函数

loads 函数可以将一个 JSON 数据的文本字符串转换加载为 Python 的数据对象。我们可以将其理解为 dumps 的逆操作。loads 函数的最重要的参数是第一个参数 s,作为初学者,我们就认为这个参数是字符串类型即可,s 代表 string。我们调用 loads 函数的时候,只需要赋值第一个参数,将要加载的 JSON 文本字符串传入即可,loads 函数就会将其转换加载为 Python 的数据对象。我们可以参考如下代码:

import json

json_text = """{
  "name": "netdevops01",
  "ip": "192.168.137.1",
  "vendor": "huawei",
  "online": true,
  "rack": "0101",
  "start_u": 20,
  "end_u": 21,
  "interface_usage": 0.67,
  "interfaces": ["eth1/1","eth1/2","eth1/3"],
  "uptime": null
}"""
data = json.loads(json_text)
print(type(data))
print(data)

其运行结果如下:

<class 'dict'>
{'name': 'netdevops01', 'ip': '192.168.137.1', 'vendor': 'huawei', 'online': True, 'rack': '0101', 'start_u': 20, 'end_u': 21, 'interface_usage': 0.67, 'interfaces': ['eth1/1', 'eth1/2', 'eth1/3'], 'uptime': None}

从输出结果我们发现,一个 JSON 文本字符串被 loads 函数加载成为了一个 Python 的字典对象。如果我们写的是数组形式的 JSON 数据,则会被加载成为一个 Python 的列表对象。

json.load 函数

在实际使用中,我们的 JSON 文本字符串可能来源于文本文件或者网络中的一组字节流。json 模块的 load 函数提供了将文件对象加载成为 Python 数据对象的功能。对于初学者,我们仅需关注它的第一个参数 fp,即打开的一个文件对象,笔者建议使用 "r" 模式打开(默认即是此模式),指定编码为 UTF8(前提是我们将 JSON 文本文件以 UTF8 字符集进行编码保存)。load 函数的使用可以参考如下代码:

import json

with open('data.json', encoding='utf8') as f:
    data = json.load(fp=f)
    print(type(data))
    print(data)

上述代码运行结果如下:

<class 'dict'>
{'name': 'netdevops01', 'ip': '192.168.137.1', 'vendor': '华为', 'online': True, 'rack': '0101', 'start_u': 20, 'end_u': 21, 'interface_usage': 0.67, 'interfaces': ['eth1/1', 'eth1/2', 'eth1/3'], 'uptime': None}

我们通过 with 上下文管理器结合 open 函数,打开了一个之前我们保存的 JSON 文件,这里我们一定要指明字符集为 UTF8,默认使用 “r” 模式只读打开。将文件对象赋值给 load 函数的第一个参数 fp (我们也可以不写参数名,直接传入文件对象 f ),这样就完成了将  JSON 文件加载成为 Python 数据对象的过程。

以上就是 Python 数据对象与 JSON 之间相互转换涉及到的四个重要函数,有 s 的就是将数据和 JSON 文本字符串之间进行相互转换,没有 s 的函数就是将 Python 数据对象和二进制文件(扩展开讲,其实是一个字节流)进行相互转换。

我们在 NetDevOps 开发过程中,如果调用自动化平台或者是 SDN 控制器的一些 API 时,返回的数据大多数以 JSON 数据格式返回,所以我们一定要了解 JSON 数据格式。同时在开发过程中,我们也可以将数据以 JSON 数据格式的文本文件进行保存,持久化计算的结果,为后续脚本提供一些结论性的数据等。 类似的使用场景还是很多的,大家要掌握其规范并能灵活使用。