假设你是一名 Linux 运维工程师,需要管理几十台甚至几百台服务器。现在,你想编写一个 Python 程序,记录每台服务器的名称、IP 地址、服务端口和运行状态。

这些信息应该如何保存?如果以后还需要把数据交给其他程序使用,又该采用什么格式?

我们可以使用 JSON。

JSON 是一种非常常见的数据交换格式。在日常开发、Linux 运维、云平台管理以及调用 API 接口时,我们经常会接触 JSON 数据。学习 JSON,不仅能帮助我们理解程序之间如何传递数据,也能为后续使用 Python 的 json 模块打下基础。

什么是 JSON

JSON 是 JavaScript Object Notation 的缩写,中文通常称为 JavaScript 对象表示法。

虽然 JSON 的名字中包含 JavaScript,但它并不局限于 JavaScript。Python、Java、Go、JavaScript 等多种编程语言都可以处理 JSON 数据。

我们可以把 JSON 理解成一种通用的数据记录格式。不同的程序可以按照相同的规则组织和读取数据,从而实现信息交换。

例如,我们想记录一辆汽车的品牌和型号,可以写成下面这样:

1
2
3
4
{
"make": "GMC",
"model": "Canyon"
}

这段 JSON 表示一辆汽车:

  • make:表示汽车品牌,值为 GMC。
  • model:表示汽车型号,值为 Canyon。

这里的 make 和 model 是键(key),GMC 和 Canyon 是对应的值(value)。

键值对就是把一个名称与一个具体的数据关联起来。例如,服务器名称对应 web01,服务器 IP 地址对应 192.168.1.10。

JSON 使用键值对组织对象中的数据。理解这个概念,是学习 JSON 语法的第一步。

JSON 与 Python 有什么关系

学习 JSON 时,有一个很容易让初学者困惑的问题:JSON 对象和 Python 字典看起来非常相似,它们是不是同一种东西?

答案是:它们不是同一种东西,但可以相互转换。

JSON 是一种数据交换格式,而 Python 字典是 Python 语言提供的一种数据结构。

我们先看看它们各自是什么样子。

JSON 对象:

1
2
3
4
5
{
"name": "web01",
"port": 80,
"enabled": true
}

Python 字典:

1
2
3
4
5
server = {
"name": "web01",
"port": 80,
"enabled": True
}

两段内容看起来几乎一样,但存在一个明显区别:JSON 使用小写的 true,Python 使用大写开头的 True。

如果直接把 JSON 内容复制到 Python 代码中,Python 就可能因为语法错误而无法执行。

除了布尔值,空值等内容也存在类似的区别。因此,我们需要分别理解 JSON 的规则和 Python 的规则。

JSON 数据类型与 Python 数据类型的对应关系

JSON 支持字符串、数字、布尔值、空值、对象和数组。Python 也有对应的数据类型。

JSON 类型JSON 示例Python 对应类型Python 示例
字符串"web01"str"web01"
整数形式的数字80int80
小数形式的数字3.14float3.14
布尔值trueboolTrue
布尔值falseboolFalse
空值nullNoneTypeNone
对象{"name": "web01"}dict{"name": "web01"}
数组["nginx", "ssh"]list["nginx", "ssh"]

这里的 str、int、float、bool、dict 和 list 都是 Python 中的数据类型名称。

如果你暂时还不熟悉这些名称,可以先记住它们的用途:

  • str:保存文本,例如服务器名称。
  • int:保存整数,例如端口号。
  • float:保存小数,例如 CPU 使用率。
  • bool:表示真或假。
  • dict:保存具有名称的键值对。
  • list:保存一组有顺序的数据。
  • None:表示没有具体的值。

需要注意,JSON 的数字类型并没有严格区分整数和小数,表格中的 int 和 float 是 Python 解析数字时常见的对应结果。

为什么 JSON 和 Python 的写法不完全一样

我们通过几个例子来比较。

布尔值的区别

JSON:

1
2
3
4
{
"enabled": true,
"debug": false
}

Python:

1
2
3
4
server = {
"enabled": True,
"debug": False
}

JSON 中的 true 和 false 必须使用小写,而 Python 中的 True 和 False 必须按照 Python 的写法使用大写开头。

空值的区别

JSON:

1
2
3
{
"owner": null
}

Python:

1
2
3
server = {
"owner": None
}

JSON 使用 null 表示空值,Python 使用 None。

字符串的区别

JSON:

1
2
3
{
"name": "web01"
}

Python:

1
2
3
server = {
"name": "web01"
}

在这个例子中,两者的写法相同。不过,JSON 对象的键必须使用双引号,Python 字典的字符串键则可以使用单引号或双引号。

例如,下面是合法的 Python 字典:

1
2
3
server = {
'name': 'web01'
}

但如果把同样的内容直接作为 JSON 使用,就不符合 JSON 的语法要求。

最重要的一点是:不要把 JSON 当成 Python 代码执行,也不要把 Python 字典直接当成 JSON 文本使用。 后续我们会使用 Python 的 json 模块,在两种表示形式之间进行转换。

JSON 的基本语法

了解了 JSON 和 Python 的区别后,我们再来学习 JSON 自身的语法规则。

使用花括号表示对象

JSON 对象使用一对花括号 {} 表示。

例如:

1
2
3
4
5
{
"name": "web01",
"ip": "192.168.1.10",
"status": "running"
}

这段 JSON 记录了一台服务器的基本信息。

其中:

  • "name" 是键,对应的值是 "web01"。
  • "ip" 是键,对应的值是 "192.168.1.10"。
  • "status" 是键,对应的值是 "running"。

每个键和值之间使用冒号 : 分隔,不同的键值对之间使用逗号 , 分隔。

可以把它理解为一张简单的服务器信息表:

字段值
nameweb01
ip192.168.1.10
statusrunning

JSON 对象并不是 Excel 表格。它是一种结构化的数据表示方式,程序可以按照约定读取其中的字段和值。

需要注意的是,JSON 对象中的键必须是字符串,并且必须使用双引号包围。

下面是合法的 JSON:

1
2
3
4
{
"name": "web01",
"port": 80
}

下面的写法则不符合 JSON 语法:

1
2
3
4
{
'name': 'web01',
'port': 80,
}

原因有两个:

  • JSON 字符串必须使用双引号,不能像 Python 那样使用单引号。
  • JSON 不允许在最后一个键值对后面保留多余的逗号。

使用数组保存多个数据

如果我们需要记录多台服务器的信息,就不能只保存一台服务器的数据。

例如,有三台服务器需要管理:

  • web01:Web 服务器。
  • web02:Web 服务器。
  • db01:数据库服务器。

JSON 可以使用数组保存多个数据。数组使用方括号 [] 表示,数组中的多个元素使用逗号分隔。

例如:

1
2
3
4
5
[
"web01",
"web02",
"db01"
]

这个数组包含三个字符串,分别表示三台服务器的名称。

如果希望记录每台服务器的完整信息,也可以将多个对象放入一个数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[
{
"name": "web01",
"ip": "192.168.1.10"
},
{
"name": "web02",
"ip": "192.168.1.11"
},
{
"name": "db01",
"ip": "192.168.1.20"
}
]

这段 JSON 的整体结构是一个数组,数组中的每个元素都是一个对象。

在 Python 中,这种结构通常会被转换为一个列表,列表中的每个元素都是一个字典。

JSON 支持哪些数据类型

JSON 不仅可以保存字符串,还可以保存数字、布尔值、空值、对象和数组。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "web01",
"port": 80,
"enabled": true,
"maintenance": false,
"description": null,
"tags": ["web", "production"],
"location": {
"datacenter": "dc01",
"rack": 12
}
}

这段 JSON 记录了服务器的名称、端口、启用状态、维护状态、描述信息、标签和机房位置。

这里有三个细节需要特别注意。

数字不需要使用双引号。

1
2
3
{
"port": 80
}

这里的 80 是数字。如果写成 "80",它就变成了字符串。虽然它们看起来很相似,但程序处理时会将它们视为不同的数据类型。

布尔值必须使用小写。

JSON 使用 true 和 false 表示布尔值,而不是 Python 中的 True 和 False。

空值使用 null。

如果暂时不知道某台服务器的负责人,可以使用 null 表示没有具体值:

1
2
3
4
{
"name": "web01",
"owner": null
}

JSON 中的 null 与 Python 中的 None 对应,但两者的写法不同。

字符串和特殊字符

JSON 中的字符串必须使用双引号包围。

例如:

1
2
3
4
{
"name": "web01",
"message": "服务器运行正常"
}

如果字符串中包含双引号、换行符等特殊字符,就需要按照 JSON 的规则进行转义。

例如:

1
2
3
{
"message": "服务器提示:\"磁盘空间不足\""
}

其中,反斜杠 \ 用于转义后面的双引号,避免它被误认为字符串的结束位置。

JSON 中常见的转义写法包括:

写法含义
\"双引号
\\反斜杠
\n换行
\t制表符

JSON 不支持注释。不能像 Python 那样直接在 JSON 文件中添加 # 注释,也不能使用 JavaScript 风格的 // 注释。

使用 Python 校验 JSON 语法

前面我们学习了 JSON 的基本规则,但只靠肉眼检查,有时候很难发现错误。

例如,下面的 JSON 看起来没有什么问题:

1
2
3
4
{
"name": "web01",
"port": 80,
}

但实际上,80 后面多了一个逗号,不符合 JSON 语法。

如果配置文件比较长,包含几十甚至几百个字段,逐个检查会非常麻烦。

这时,我们可以使用 Python 自带的 json 模块检查 JSON 是否符合语法规则。

Python 的 json 模块属于标准库,通常不需要额外安装。它可以解析 JSON 文本,也可以把 Python 数据转换成 JSON 文本。

编写第一个 JSON 校验程序

先准备一段合法的 JSON 数据:

1
2
3
4
5
6
{
"name": "web01",
"ip": "192.168.1.10",
"port": 80,
"enabled": true
}

现在,我们希望 Python 帮我们检查这段数据能否被正确解析。

在电脑上新建一个名为 check_json.py 的文件,将下面的代码写进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import json

data = '''
{
"name": "web01",
"ip": "192.168.1.10",
"port": 80,
"enabled": true
}
'''

try:
result = json.loads(data)
print("JSON 语法正确")
print(result)
except json.JSONDecodeError as e:
print("JSON 语法错误")
print(e)

我们逐行理解这段代码。

import json 表示导入 Python 的 json 模块。导入之后,我们就可以使用模块提供的 JSON 处理功能。

data = '''...''' 用于把 JSON 文本保存到 Python 字符串变量 data 中。这里使用三引号,是为了方便在字符串中保留多行内容。三引号属于 Python 语法,不属于 JSON 语法本身。

json.loads(data) 会尝试解析 data 中的 JSON 文本,并将其转换为相应的 Python 数据结构。

例如,这段 JSON 对象会被转换为 Python 字典。

try 和 except 用于处理可能发生的异常。如果 JSON 文本格式错误,Python 就可以捕获 json.JSONDecodeError,而不是直接显示一大段未处理的错误信息。

其中,as e 表示把异常信息保存到变量 e 中,之后可以通过 print(e) 查看具体错误原因。

运行程序并查看输出

打开终端,进入保存 check_json.py 的目录,然后执行:

1
python check_json.py

如果你的系统使用 python3 命令,可以执行:

1
python3 check_json.py

对于前面给出的合法 JSON,预期输出如下:

1
2
JSON 语法正确
{'name': 'web01', 'ip': '192.168.1.10', 'port': 80, 'enabled': True}

为什么输出与原来的 JSON 不完全一样?

因为 json.loads() 不只是检查语法,还把 JSON 文本转换成了 Python 数据结构。

转换过程中:

  • JSON 对象变成了 Python 字典。
  • JSON 的 true 变成了 Python 的 True。
  • 数字 80 仍然是整数。
  • JSON 字符串仍然是 Python 字符串。

另外,print(result) 显示的是 Python 字典的表示形式,不是原始 JSON 文本。

这说明,JSON 与 Python 数据结构之间存在明确的转换关系,而 json.loads() 正是完成这种转换的函数之一。

故意制造错误,看看 Python 如何处理

理解程序最好的方式之一,就是主动修改代码,观察结果。

现在,我们把 data 中的 JSON 改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json

data = '''
{
"name": "web01",
"ip": "192.168.1.10",
"port": 80,
}
'''

try:
json.loads(data)
print("JSON 语法正确")
except json.JSONDecodeError as e:
print("JSON 语法错误")
print(e)

这段代码的问题在于,最后一个键值对后面多了一个逗号。

运行程序:

1
python check_json.py

预期输出类似下面这样:

1
2
JSON 语法错误
Expecting property name enclosed in double quotes: line 5 column 1 (char 66)

具体的行号、列号和字符位置会受到实际文本内容及空白字符的影响,因此不一定与示例完全一致。

这段错误信息的大意是:Python 在这里预期应该出现一个属性名称,也就是 JSON 对象中的下一个键,但实际遇到了不符合语法要求的内容。

因为 JSON 对象中的每个逗号都应该分隔两个键值对。最后一个键值对后面不应该再有逗号,否则解析器会继续期待下一个键。

这时,json.loads() 抛出了 json.JSONDecodeError 异常,程序进入 except 分支,显示错误信息。

通过这个例子,我们可以看到:Python 不仅能够告诉我们 JSON 是否符合语法,还能提供定位错误的线索。

校验真实的 JSON 文件

前面的示例把 JSON 文本直接写在 Python 程序中。实际工作时,我们更经常需要检查独立的配置文件。

例如,我们创建一个名为 servers.json 的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"servers": [
{
"name": "web01",
"ip": "192.168.1.10",
"port": 80
},
{
"name": "db01",
"ip": "192.168.1.20",
"port": 3306
}
]
}

接着,在同一目录下创建 validate_json.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json

filename = "servers.json"

try:
with open(filename, "r", encoding="utf-8") as f:
data = json.load(f)

print(f"{filename} 的 JSON 语法正确")
print(f"服务器数量:{len(data['servers'])}")

except FileNotFoundError:
print(f"文件不存在:{filename}")

except json.JSONDecodeError as e:
print(f"{filename} 的 JSON 语法错误")
print(f"错误详情:{e}")

这段程序比前面的示例多了文件读取操作,我们逐步理解。

filename = "servers.json" 表示要检查的文件名。由于这里使用相对路径,程序会从当前工作目录寻找该文件。

open(filename, "r", encoding="utf-8") 用于以文本读取模式打开文件,并使用 UTF-8 编码读取内容。

with 语句会在代码块执行完毕后自动关闭文件,避免文件资源一直处于打开状态。

json.load(f) 与之前的 json.loads(data) 有什么区别?

它们的功能相似,但输入不同:

  • json.load(f):从文件对象中读取 JSON 并解析。
  • json.loads(data):解析已经保存在字符串中的 JSON 文本。

可以简单记忆:load 主要用于文件对象,loads 主要用于字符串。

这里还增加了一个 FileNotFoundError 异常处理分支。如果文件不存在,程序会显示文件不存在的信息;如果文件存在但 JSON 语法错误,则进入 json.JSONDecodeError 分支。

对于示例中的合法文件,预期输出为:

1
2
servers.json 的 JSON 语法正确
服务器数量:2

这里的 len(data['servers']) 用于计算 servers 列表中有多少个元素。因为列表中保存了两台服务器,所以结果是 2。

如果我们故意把 servers.json 的最后一个键值对改成带多余逗号的写法,程序就会报告 JSON 语法错误。

这类校验程序可以用于检查应用配置文件、自动化任务生成的 JSON 数据,以及程序运行前需要读取的配置内容。

需要注意的是,这里检查的是 JSON 语法是否正确。如果文件语法正确,但服务器 IP 地址写错了,或者端口号不符合业务要求,json.load() 仍然可能成功。业务数据是否合理,还需要额外编写检查逻辑。

JSON 在实际工作中有什么用

回到 Linux 运维场景,假设你需要维护一个包含多台服务器的清单。每台服务器都有名称、IP 地址、角色和运行状态。

可以把这些信息保存到 servers.json 文件中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"servers": [
{
"name": "web01",
"ip": "192.168.1.10",
"role": "web",
"status": "running"
},
{
"name": "web02",
"ip": "192.168.1.11",
"role": "web",
"status": "running"
},
{
"name": "db01",
"ip": "192.168.1.20",
"role": "database",
"status": "stopped"
}
]
}

这种格式具有几个实际优势:

  • 结构清晰: 每个字段都有明确的名称,不需要靠记忆判断某个值代表什么。
  • 方便维护: 可以按照相同的结构增加、修改或删除服务器信息。
  • 便于程序处理: Python 可以读取 JSON 文件,将其中的数据转换成 Python 对象,再执行检查、统计等操作。
  • 便于程序之间交换数据: 其他支持 JSON 的程序也可以按照相同的格式读取这些信息。

例如,后续我们可以使用 Python 编写脚本,读取服务器清单,统计运行中的服务器数量,或者找出已经停止的服务器。

在云计算和自动化运维场景中,JSON 也经常用于 API 请求与响应、云资源配置以及应用程序之间的数据交换。

需要注意的是,JSON 本身只负责组织和表示数据,并不会自动连接服务器、检查服务状态或者执行命令。这些操作需要由 Python 程序或其他工具完成。

动手练习

假设你需要记录一台 Linux 服务器的信息,请编写一个 JSON 对象,至少包含以下字段:

  • hostname:服务器名称,设置为 web01。
  • ip:服务器 IP 地址,设置为 192.168.1.10。
  • port:服务端口,设置为数字 80。
  • enabled:服务是否启用,设置为布尔值 true。
  • owner:服务器负责人,暂时没有确定,设置为空值 null。
  • services:服务器运行的服务列表,包含 nginx 和 ssh。

完成后,可以对照下面的参考答案检查自己的写法。

1
2
3
4
5
6
7
8
9
10
11
{
"hostname": "web01",
"ip": "192.168.1.10",
"port": 80,
"enabled": true,
"owner": null,
"services": [
"nginx",
"ssh"
]
}

练习一:故意写错 JSON,观察校验结果

现在,我们故意在最后一个键值对后面添加一个多余的逗号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import json

data = '''
{
"hostname": "web01",
"port": 80,
}
'''

try:
json.loads(data)
print("JSON 语法正确")
except json.JSONDecodeError as e:
print("JSON 语法错误")
print(e)

将代码保存为 check_json.py,然后在终端中运行:

1
python check_json.py

如果使用 python3 命令,则运行:

1
python3 check_json.py

预期输出类似下面这样:

1
2
JSON 语法错误
Expecting property name enclosed in double quotes: line 5 column 1 (char 39)

具体的行号、列号和字符位置可能因实际内容中的空白字符而有所不同。

这里出现错误,是因为 JSON 对象的最后一个键值对后面不能有多余的逗号。解析器遇到逗号后,还会期待下一个键,但实际遇到了结束的花括号。

请删除多余的逗号,再运行程序,观察校验结果如何变化。

练习二:区分 JSON 的布尔值和 Python 的布尔值

在 JSON 中,布尔值必须写成小写的 true 和 false。

例如,下面是合法的 JSON:

1
2
3
4
{
"hostname": "web01",
"enabled": true
}

如果把 true 改成 True:

1
2
3
4
{
"hostname": "web01",
"enabled": True
}

这段内容就不符合 JSON 语法。

请使用下面的程序验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import json

data = '''
{
"hostname": "web01",
"enabled": True
}
'''

try:
result = json.loads(data)
print("JSON 语法正确")
print(result)
except json.JSONDecodeError as e:
print("JSON 语法错误")
print(e)

运行后,程序会输出 JSON 语法错误,并给出错误位置等信息。

这里需要区分两件事:

  • 在 JSON 文本中,应该使用 true。
  • 在 Python 代码中,布尔值应该使用 True。

例如,下面是合法的 Python 字典:

1
2
3
4
server = {
"hostname": "web01",
"enabled": True
}

但是,不能因为 Python 使用 True,就把 JSON 中的 true 也改成 True。

练习三:判断语法正确,不代表数据一定正确

再思考一个问题:如果我们把服务器端口写成 -1,JSON 校验会失败吗?

1
2
3
4
{
"hostname": "web01",
"port": -1
}

答案是:不会因为端口号为负数而产生 JSON 语法错误。

-1 是合法的 JSON 数字,因此 json.loads() 可以正常解析它。

但是,负数不是有效的 TCP 或 UDP 端口号。也就是说,这份数据虽然符合 JSON 语法,却不符合端口号的业务规则。

通过这个例子,我们可以理解:

  • 语法校验:检查数据是否符合 JSON 的格式规则。
  • 业务校验:检查数据是否符合实际业务要求。

在编写自动化运维程序时,这两种检查都很重要。

写在最后

JSON 是一种简单、通用的数据交换格式。它通过对象、数组、字符串、数字、布尔值和空值等基本结构,帮助我们清晰地组织数据。

对于 Python 初学者来说,学习 JSON 需要掌握三个重点:

  • 理解 JSON 的基本语法,知道对象、数组以及常见数据类型如何表示。
  • 理解 JSON 与 Python 数据类型之间的对应关系,特别是 true 与 True、null 与 None 的区别。
  • 学会使用 Python 的 json 模块校验 JSON 文本,并理解 json.load() 和 json.loads() 的基本用途。

JSON 语法校验只是第一步。接下来,我们还会学习如何读取 JSON 文件、修改数据,以及将 Python 字典和列表转换成 JSON 文本。

下一篇,我们将正式学习 Python 的 json 模块,把这些知识应用到实际程序中。