1
2
3
4
5
6
7
作者:李晓辉

联系方式:

1. 微信:Lxh_Chat

2. 邮箱:939958092@qq.com
  1. Python 系列(一) 从零开始认识 Python,入门其实没那么难
  2. Python 系列(二) 学 Python 的第一步,把开发环境搭建好
  3. Python 系列(三) 基础语法避坑指南,缩进、大小写、续行与注释
  4. Python 系列(四) Python 输入输出详解,print()、input() 与常见避坑指南
  5. Python 系列(五) 数值类型与运算符详解,整除、浮点精度与进制转换实战
  6. Python 系列(六) 字符串处理实战,查找、替换、切片与日志分析
  7. Python 系列(七) 类型转换详解,int()、float()、str() 与金额计算避坑
  8. Python 系列(八) Python 流程控制入门,掌握 if、elif 和 else
  9. Python 系列(九) Python 逻辑运算符详解,and、or、not 一篇搞懂
  10. Python 系列(十) while、for 循环实战,彻底搞懂 range、break 与 continue
  11. Python 系列(十一) list、tuple、set、dict,到底该怎么选?
  12. Python 系列(十二) List 列表实战,搞定数据增删改查与批量处理
  13. Python 系列(十三) Tuple 元组实战,搞懂不可变特性与 enumerate() 遍历
  14. Python 系列(十四) Set 集合实战,搞定数据去重与集合运算
  15. Python 系列(十五) Dict 字典详解,掌握快速查找与键值操作
  16. Python 系列(十六) sort() 和 sorted() 详解,掌握数据排序技巧
  17. Python 系列(十七) 自定义函数入门,让重复代码一次编写多次使用
  18. Python 系列(十八) 函数参数详解,搞懂位置参数、关键字参数和默认参数
  19. Python 系列(十九) 从 *args 到 **kwargs,让函数调用更灵活
  20. Python 系列(二十) 变量作用域是什么?一文搞懂 global 和 nonlocal
  21. Python 系列(二十一) 搞懂变量、对象和引用,理解函数参数传递机制
  22. Python 系列(二十二) map() 和 filter() 详解,轻松实现批量数据处理与筛选
  23. Python 系列(二十三) 嵌套函数与 lambda 表达式详解,搞懂作用域与闭包
  24. Python 系列(二十四) 搞懂递归函数,从基线条件到实际应用
  25. Python 系列(二十五) 聊聊模块与 import,让代码告别重复编写
  26. Python 系列(二十六) Python 模块入门,从 import 到模块复用
  27. Python 系列(二十七) Python dir() 函数详解,快速查看模块中的函数与属性
  28. Python 系列(二十八) 不会用 Python 标准库?教你快速查找模块和函数
  29. Python 系列(二十九) 搞懂 Python sys 模块,让脚本更适合自动化运维
  30. Python 系列(三十) 为什么 0.1 + 0.2 不等于 0.3?彻底搞懂 Decimal
  31. Python 系列(三十一) 搞懂 datetime 日期时间,轻松处理时区与时间计算
  32. Python 系列(三十二) 面向对象编程入门,从零理解类、对象、封装与继承

在日常工作中,时间是一个绕不开的话题。

比如,Linux 服务器上的日志通常带有时间戳,我们需要根据时间定位故障;定时任务需要判断下一次执行时间;监控脚本需要统计任务运行了多久;自动化运维程序还可能需要检查 SSL/TLS 证书距离过期还有多少天。

这些需求,都离不开日期和时间处理。

Python 标准库提供了 datetime 模块,可以帮助我们获取当前日期、处理时间、计算时间间隔,以及将日期转换为指定格式的字符串。

不过,日期时间处理也有一些容易踩坑的地方。

例如,为什么 datetime 和 timedelta 不是同一种类型?为什么计算程序运行耗时,推荐使用 time.perf_counter(),而不是简单地记录两次当前时间?如果证书使用 UTC 时间,而服务器使用本地时间,又该如何比较?

本篇我们就从基础开始,逐步解决这些问题,并结合 Linux 运维场景编写实用脚本。

认识 Python 中的日期与时间类型

在学习具体函数之前,我们先认识 datetime 模块中几个最常用的类型。

Python 并没有把日期、时间、时间间隔全部塞进同一个类型中,而是提供了不同的类型,分别表示不同的含义。

类型表示什么典型用途
date年、月、日维护日期、备份日期
time时、分、秒及微秒等时间字段每天的巡检时间、计划执行时间
datetime完整的日期和时间日志时间、任务开始时间
timedelta两个日期或时间之间的间隔计算剩余时间、日期差

可以简单理解为:

  • date 只关心哪一天。
  • time 只关心一天中的什么时间。
  • datetime 同时关心日期和时间。
  • timedelta 关心两个时间点相差多久。

先运行下面的代码,看看这些类型的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from datetime import date, time, datetime, timedelta

today = date.today()

clock = time(14, 30, 0)

now = datetime.now()

interval = timedelta(days=1, hours=2)

print(today)
print(clock)
print(now)
print(interval)

输出示例:

1
2
3
4
2026-10-16
14:30:00
2026-10-16 10:30:25.123456
1 day, 2:00:00

以上时间仅为示例,实际结果取决于运行环境。

这里有一个值得注意的地方:time(14, 30, 0) 表示一天中的 14 点 30 分,并不包含具体日期;timedelta(days=1, hours=2) 表示一个持续 26 小时的时间间隔,也不是某个具体的时间点。

理解这些区别后,后面的日期计算就容易多了。

使用 datetime 获取当前日期和时间

如果想获取当前日期和时间,可以使用 datetime.now()。

1
2
3
4
5
from datetime import datetime

now = datetime.now()

print(now)

运行结果可能如下:

1
2026-10-16 10:30:25.123456

这里使用了:

1
from datetime import datetime

这种导入方式直接从 datetime 模块中导入 datetime 类。

初学者可能会觉得奇怪:为什么模块名和类名都叫 datetime?

它们实际上不是同一个东西:

  • datetime 模块:Python 标准库中的模块。
  • datetime 类:模块中用于表示日期和时间的类。

导入类之后,我们就可以直接调用 datetime.now() 获取当前本地日期和时间。

分别获取日期和时间

如果只需要日期或时间,可以从 datetime 对象中提取对应部分。

1
2
3
4
5
6
from datetime import datetime

now = datetime.now()

print(now.date())
print(now.time())

输出示例:

1
2
2026-10-16
10:30:25.123456

其中:

  • now.date():返回一个 date 对象。
  • now.time():返回一个 time 对象,不包含日期和时区信息。

例如,编写每日巡检脚本时,我们可能只需要记录巡检日期;分析某项任务每天几点执行时,则可能只关心时分秒。

单独获取年、月、日、时、分、秒

如果需要提取具体字段,可以直接访问相应属性。

1
2
3
4
5
6
7
8
9
10
from datetime import datetime

now = datetime.now()

print(f"年:{now.year}")
print(f"月:{now.month}")
print(f"日:{now.day}")
print(f"时:{now.hour}")
print(f"分:{now.minute}")
print(f"秒:{now.second}")

输出示例:

1
2
3
4
5
6
年:2026
月:10
日:16
时:10
分:30
秒:25

这些属性可以帮助我们提取日期时间中的具体信息。

例如,可以根据当前日期判断是否到了每月的巡检日,也可以根据当前小时决定是否执行某项任务。

需要注意,datetime.now() 默认获取运行环境的本地时间。如果服务器时区配置不正确,程序获取到的时间也可能与预期不一致。

使用 timedelta 计算时间间隔

获取当前时间只是第一步。

实际工作中,我们经常需要计算未来或过去的时间,例如:

  • 计算明天的日期。
  • 获取 7 天后的时间。
  • 计算 30 天前的日期。
  • 判断某个任务距离计划执行时间还有多久。

这些需求可以通过 timedelta 实现。

timedelta 用于表示时间间隔,可以与日期或日期时间对象进行加减运算。

计算未来的时间

1
2
3
4
5
6
7
8
from datetime import datetime, timedelta

now = datetime.now()

future = now + timedelta(days=1)

print(f"当前时间:{now}")
print(f"明天此时:{future}")

假设当前时间为 2026-10-16 10:30:00,输出示例为:

1
2
当前时间:2026-10-16 10:30:00
明天此时:2026-10-17 10:30:00

这里的:

1
timedelta(days=1)

表示一天的时间间隔。

将这个时间间隔加到当前时间上,就可以得到一天后的时间。

计算过去的时间

如果想获取过去的时间,只需要使用减法。

1
2
3
4
5
6
7
8
9
10
from datetime import datetime, timedelta

now = datetime.now()

yesterday = now - timedelta(days=1)
last_week = now - timedelta(days=7)

print(f"当前时间:{now}")
print(f"昨天此时:{yesterday}")
print(f"一周前:{last_week}")

同样,我们还可以使用 hours、minutes 和 seconds 等参数。

1
2
3
4
5
6
7
from datetime import datetime, timedelta

now = datetime.now()

print(now + timedelta(hours=2))
print(now + timedelta(minutes=30))
print(now + timedelta(seconds=60))

这些操作在任务调度、监控系统和日志分析中都很常见。

需要注意,timedelta(days=1) 表示持续 24 小时的时间间隔,并不总是等价于跨越到下一个本地日历日的同一时刻。在涉及夏令时切换的地区,如果业务要求按照当地日历时间计算,应特别处理时区规则。

计算两个日期相差多少天

假设我们需要计算某个维护计划距离今天还有多少天。

1
2
3
4
5
6
7
8
from datetime import date

today = date.today()
maintenance_day = date(2026, 12, 1)

difference = maintenance_day - today

print(f"距离维护日期还有 {difference.days} 天")

两个 date 对象相减后,会得到一个 timedelta 对象。

通过 difference.days,就可以获取相差的整天数。

如果目标日期已经过去,结果就会是负数。因此,在实际使用中,可以根据业务需要判断日期是否已经到期。

使用 strftime 格式化日期时间

datetime.now() 默认输出的格式虽然直观,但有时并不符合我们的需求。

例如,我们希望日志文件名中包含日期:

1
server-check-20261016.log

或者希望巡检记录显示成:

1
2026-10-16 10:30:25

这时候就可以使用 strftime()。

strftime() 可以将日期时间对象转换为指定格式的字符串。

1
2
3
4
5
6
7
from datetime import datetime

now = datetime.now()

print(now.strftime("%Y-%m-%d %H:%M:%S"))
print(now.strftime("%Y%m%d"))
print(now.strftime("%Y年%m月%d日"))

输出示例:

1
2
3
2026-10-16 10:30:25
20261016
2026年10月16日

常用格式符如下:

格式符含义示例
%Y四位年份2026
%m两位月份10
%d两位日期16
%H24 小时制小时10
%M分钟30
%S秒25
%f微秒,六位数字123456
%A本地语言环境中的完整星期名称例如 Friday
%w星期数字,星期日为 05

注意,%M 表示分钟,而 %m 表示月份,两者不要混淆。

实战:生成带日期的备份文件名

在 Linux 运维脚本中,我们经常需要生成带日期的备份文件名。

1
2
3
4
5
6
7
from datetime import datetime

today = datetime.now().strftime("%Y%m%d")

backup_file = f"/backup/config-{today}.tar.gz"

print(backup_file)

假设当前日期为 2026 年 10 月 16 日,输出结果为:

1
/backup/config-20261016.tar.gz

这样,每次运行脚本时,就可以根据日期生成不同的备份文件名。

不过,这里只生成了文件名,并没有真正执行备份操作。如果同一天需要生成多份备份,还可以将时分秒或其他唯一标识加入文件名。

使用 strptime 将字符串转换为日期时间

前面我们学习了如何将日期时间对象转换为字符串。

但在实际工作中,数据经常以字符串形式出现。

例如,配置文件中保存着计划维护时间:

1
2026-12-01 23:30:00

如果需要计算这个时间距离现在还有多久,就需要先将字符串转换为日期时间对象。

这时候可以使用 strptime()。

strptime() 可以按照指定格式,将字符串解析为日期时间对象。

1
2
3
4
5
6
7
8
9
10
11
from datetime import datetime

time_text = "2026-12-01 23:30:00"

maintenance_time = datetime.strptime(
time_text,
"%Y-%m-%d %H:%M:%S"
)

print(maintenance_time)
print(type(maintenance_time))

输出结果:

1
2
2026-12-01 23:30:00
<class 'datetime.datetime'>

这里需要记住:

  • strftime():日期时间对象转换为字符串。
  • strptime():字符串解析为日期时间对象。

如果字符串格式不符合指定格式,例如月份写成无法识别的值,或者字符串与格式不匹配,程序就可能抛出 ValueError。

处理外部输入时,应该考虑异常处理,避免一个错误的日期字符串直接导致整个程序异常退出。

区分 datetime 与 timedelta:时间点和时间间隔

现在我们已经学习了 datetime 和 timedelta,再回头理解两者的区别就容易多了。

datetime 表示一个具体的日期时间,例如:

1
2
3
4
5
from datetime import datetime

maintenance_time = datetime(2026, 12, 1, 23, 30, 0)

print(maintenance_time)

输出:

1
2026-12-01 23:30:00

而 timedelta 表示一个时间间隔,例如:

1
2
3
4
5
from datetime import timedelta

interval = timedelta(days=3, hours=2)

print(interval)

输出:

1
3 days, 2:00:00

两者可以结合起来:

1
2
3
4
5
6
7
8
from datetime import datetime, timedelta

start = datetime(2026, 12, 1, 10, 0, 0)

end = start + timedelta(hours=3)

print(f"开始时间:{start}")
print(f"结束时间:{end}")

输出:

1
2
开始时间:2026-12-01 10:00:00
结束时间:2026-12-01 13:00:00

理解这个区别很重要:datetime 表示时间点,timedelta 表示时间间隔。

这也是计算任务执行时间、证书剩余有效期和维护窗口时的基础。

巡检耗时应该如何正确测量?

这里需要专门讨论一个容易混淆的问题。

假设我们想知道一段巡检程序运行了多久,可能会写出下面的代码:

1
2
3
4
5
6
7
8
9
from datetime import datetime

start = datetime.now()

# 执行巡检任务

end = datetime.now()

print((end - start).total_seconds())

这段代码可以计算两个本地日期时间之间的差值,但对于精确测量程序耗时,它并不是最理想的选择。

原因是 datetime.now() 获取的是系统日历时间。如果任务运行期间系统时间被手动修改、通过时间同步机制发生调整,或者系统时钟出现跳变,计算结果就可能受到影响。

因此,应该区分两种需求。

需求推荐方式原因
记录巡检开始和结束的实际日期时间datetime.now()方便写入日志和报告
测量程序执行耗时time.perf_counter()专门用于测量时间间隔,适合计算运行时长

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time
from datetime import datetime

# 记录日历时间
start_time = datetime.now()

# 记录耗时计时器
start_counter = time.perf_counter()

# 模拟巡检任务
sum(range(1_000_000))

# 记录结束时刻
end_counter = time.perf_counter()
end_time = datetime.now()

# 计算运行耗时
duration = end_counter - start_counter

print(f"开始时间:{start_time}")
print(f"结束时间:{end_time}")
print(f"运行耗时:{duration:.6f} 秒")

这里分别使用两种工具完成不同的任务:

  • datetime.now() 负责记录人类可以阅读的开始时间和结束时间。
  • time.perf_counter() 负责测量经过的时间间隔。

这样,即使系统日历时间发生调整,也不会像直接用两次 datetime.now() 相减那样容易受到影响。

需要注意,perf_counter() 返回的是一个用于测量时间间隔的计数值,不能把它当成实际日期时间使用,也不应该将其解释为某个具体的日历时刻。

综合运维实战:生成完整的 Linux 服务器巡检报告

前面我们已经学习了获取时间、计算时间间隔、格式化日期,以及正确测量程序耗时。

现在,我们把这些知识结合起来,编写一个更完整的 Linux 运维脚本。

假设需要生成一份服务器巡检报告,要求完成以下任务:

  • 记录巡检开始时间和结束时间。
  • 检查根分区磁盘使用率。
  • 测量整个巡检过程的耗时。
  • 使用时间戳生成报告文件名。
  • 将巡检结果写入报告。
  • 在磁盘使用率达到阈值时产生告警。
  • 在检查失败或报告写入失败时输出错误信息,并返回非零退出状态码。

这个示例会实际读取当前运行环境的根分区使用情况,因此可以在 Linux 上运行,也可以在其他支持 shutil.disk_usage("/") 的系统上测试。

编写完整脚本

创建 inspection_report.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env python3

import shutil
import sys
import time
from datetime import datetime
from pathlib import Path

# 巡检参数
mount_point = "/"
threshold = 80.0
report_dir = Path("./reports")


def main():
# 记录日历时间和单调计时器
start_time = datetime.now()
start_counter = time.perf_counter()

results = []
exit_code = 0

results.append("Linux 服务器巡检报告")
results.append(f"检查挂载点:{mount_point}")
results.append(
f"巡检开始时间:{start_time.strftime('%Y-%m-%d %H:%M:%S')}"
)
results.append("")

# 检查磁盘使用情况
try:
usage = shutil.disk_usage(mount_point)

total_gib = usage.total / (1024 ** 3)
used_gib = usage.used / (1024 ** 3)
free_gib = usage.free / (1024 ** 3)

percent = usage.used / usage.total * 100

results.append(f"磁盘总容量:{total_gib:.2f} GiB")
results.append(f"已使用空间:{used_gib:.2f} GiB")
results.append(f"剩余空间:{free_gib:.2f} GiB")
results.append(f"磁盘使用率:{percent:.2f}%")

if percent >= threshold:
results.append(
f"告警:磁盘使用率达到 {threshold:.0f}% 阈值!"
)
exit_code = 1
else:
results.append("磁盘状态正常。")

except OSError as exc:
print(
f"错误:无法检查挂载点 {mount_point}:{exc}",
file=sys.stderr,
)
results.append(f"磁盘检查失败:{exc}")
exit_code = 2

# 记录结束时间并计算实际耗时
end_counter = time.perf_counter()
end_time = datetime.now()

duration = end_counter - start_counter

results.append("")
results.append(
f"巡检结束时间:{end_time.strftime('%Y-%m-%d %H:%M:%S')}"
)
results.append(f"巡检耗时:{duration:.6f} 秒")

# 生成报告文件名
timestamp = end_time.strftime("%Y%m%d-%H%M%S-%f")
report_file = report_dir / f"inspection-{timestamp}.log"

# 写入报告
try:
report_dir.mkdir(parents=True, exist_ok=True)
report_file.write_text(
"\n".join(results) + "\n",
encoding="utf-8",
)
except OSError as exc:
print(
f"错误:无法写入巡检报告:{exc}",
file=sys.stderr,
)
return 3

# 输出本次巡检结果
print("\n".join(results))
print(f"\n巡检报告已保存:{report_file}")

return exit_code


if __name__ == "__main__":
sys.exit(main())

运行脚本

执行:

1
2
python3 inspection_report.py
echo $?

假设根分区使用率低于 80%,输出示例可能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Linux 服务器巡检报告
检查挂载点:/
巡检开始时间:2026-10-16 10:30:25

磁盘总容量:100.00 GiB
已使用空间:65.00 GiB
剩余空间:35.00 GiB
磁盘使用率:65.00%
磁盘状态正常。

巡检结束时间:2026-10-16 10:30:25
巡检耗时:0.000843 秒

巡检报告已保存:reports/inspection-20261016-103025-123456.log
0

以上是演示输出,容量、时间和耗时均取决于实际运行环境。

如果磁盘使用率达到 80%,脚本会产生告警并返回退出码 1;如果无法检查磁盘,返回退出码 2;如果报告文件无法写入,返回退出码 3;检查正常则返回 0。

这样,后续的 Shell 脚本或自动化平台就可以根据退出状态码判断巡检结果。

关键知识点解析

使用 perf_counter() 测量耗时

1
2
3
4
5
6
7
start_counter = time.perf_counter()

# 执行巡检任务

end_counter = time.perf_counter()

duration = end_counter - start_counter

这里测量的是两个计数点之间经过的时间。

如果任务执行时间很短,也可以保留小数位,方便观察实际耗时。

为什么日期时间和耗时要分开记录?

1
2
start_time = datetime.now()
start_counter = time.perf_counter()

前者适合记录巡检发生在什么时候,后者适合计算程序运行了多久。

这两者不能随意互换。

为什么报告文件名包含微秒?

1
timestamp = end_time.strftime("%Y%m%d-%H%M%S-%f")

只精确到秒时,如果脚本在同一秒内运行多次,就可能生成相同的文件名。加入 %f 可以降低这类冲突的可能性。

不过,如果多个进程并发写入,仍然应考虑使用唯一标识、独占创建等机制来避免文件冲突。

为什么使用 sys.exit() 返回退出状态码?

1
2
if __name__ == "__main__":
sys.exit(main())

main() 返回整数,sys.exit() 会将其作为程序退出状态码交给操作系统。

常见约定如下:

退出码含义
0巡检正常
1检查发现告警
2磁盘检查失败
3报告文件写入失败

这些退出码是本例自行约定的,实际项目可以根据团队规范定义。

异常处理实战:安全解析日期时间字符串

在自动化运维中,日期时间经常来自配置文件、用户输入、日志或接口。

如果输入内容格式不正确,直接调用 datetime.strptime() 就可能产生 ValueError。

例如:

1
2
3
4
5
6
7
8
from datetime import datetime

time_text = "2026-13-40 25:61:00"

parsed_time = datetime.strptime(
time_text,
"%Y-%m-%d %H:%M:%S",
)

这段代码会报错,因为月份、日期和时间字段不合法。

如果不处理异常,程序可能直接中断。

因此,在处理外部输入时,应该对解析过程进行异常处理。

编写完整的日期解析示例

创建 parse_datetime.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#!/usr/bin/env python3

import sys
from datetime import datetime


def main():
time_text = input(
"请输入维护时间(格式:YYYY-MM-DD HH:MM:SS):"
).strip()

if not time_text:
print("错误:维护时间不能为空。", file=sys.stderr)
return 2

try:
maintenance_time = datetime.strptime(
time_text,
"%Y-%m-%d %H:%M:%S",
)
except ValueError:
print(
"错误:时间格式不正确,请使用 YYYY-MM-DD HH:MM:SS。",
file=sys.stderr,
)
return 2

now = datetime.now()
difference = maintenance_time - now

if difference.total_seconds() < 0:
print("维护时间已经过去。")
return 1

print(f"维护时间:{maintenance_time}")
print(f"距离维护时间还有 {difference.days} 天")
print(
f"距离维护时间还有 "
f"{difference.total_seconds():.0f} 秒"
)

return 0


if __name__ == "__main__":
sys.exit(main())

输入合法时间:

1
2
3
4
请输入维护时间(格式:YYYY-MM-DD HH:MM:SS):2026-12-01 23:30:00
维护时间:2026-12-01 23:30:00
距离维护时间还有 46 天
距离维护时间还有 4039175 秒

以上结果仅用于演示,实际剩余时间会随运行时刻变化。

如果输入不合法的时间,例如:

1
2
请输入维护时间(格式:YYYY-MM-DD HH:MM:SS):2026-13-40 25:61:00
错误:时间格式不正确,请使用 YYYY-MM-DD HH:MM:SS。

程序会给出明确提示,而不是直接显示完整的异常回溯。

这里还有一个细节:difference.days 是时间间隔中的整天数,而 difference.total_seconds() 返回总秒数。如果只想判断维护时间是否已到,使用 total_seconds() 或直接比较两个日期时间对象都可以。

本例中的输入不包含时区,因此假设输入的维护时间和 datetime.now() 都采用同一台机器的本地时间语义。跨时区场景不能直接套用这种写法。

综合运维实战:检查 TLS 证书到期时间

接下来,我们再看一个更加贴近实际运维工作的场景。

服务器上的 TLS 证书通常有有效期。证书即将到期时,如果没有及时续期,可能导致网站出现证书过期告警,甚至影响客户端访问。

假设我们已经从证书信息中获取了到期时间:

1
2026-12-31T23:59:59+00:00

这里的 +00:00 表示 UTC 时区。

现在,我们希望编写一个脚本,计算证书距离到期还有多少时间,并在剩余时间不足 30 天时输出警告。

先理解带时区和不带时区的 datetime

Python 中的 datetime 对象分为两类:

  • 不带时区信息的 datetime:称为 naive datetime,例如 datetime.now() 返回的本地时间对象。
  • 带时区信息的 datetime:称为 aware datetime,可以明确表示相应的时区或 UTC 偏移。

例如:

1
2
3
4
5
6
7
8
9
10
from datetime import datetime, timezone

local_now = datetime.now()
utc_now = datetime.now(timezone.utc)

print(local_now)
print(utc_now)

print(local_now.tzinfo)
print(utc_now.tzinfo)

local_now 默认不包含时区信息,即使它的数值来自本地系统时钟。

utc_now 则包含 UTC 时区信息。

如果将带时区的日期时间与不带时区的日期时间直接进行比较或相减,Python 通常会抛出 TypeError。这是为了避免程序在不知道时区语义的情况下,错误地比较两个时间点。

编写完整的证书到期检查示例

创建 certificate_expiry.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env python3

import sys
from datetime import datetime, timezone, timedelta


def main():
# 示例数据:证书到期时间为 UTC
expiry_text = "2026-12-31T23:59:59+00:00"

try:
expiry_time = datetime.fromisoformat(expiry_text)
except ValueError:
print(
"错误:证书到期时间格式不正确。",
file=sys.stderr,
)
return 2

# 确保证书到期时间带有时区信息
if expiry_time.tzinfo is None:
print(
"错误:证书到期时间缺少时区信息。",
file=sys.stderr,
)
return 2

# 统一转换为 UTC,避免时区混用
expiry_time = expiry_time.astimezone(timezone.utc)
now = datetime.now(timezone.utc)

remaining = expiry_time - now
remaining_seconds = remaining.total_seconds()

print(f"当前 UTC 时间:{now.isoformat()}")
print(f"证书到期时间:{expiry_time.isoformat()}")

if remaining_seconds <= 0:
print("告警:证书已经过期!")
return 1

print(f"剩余时间:{remaining}")

if remaining < timedelta(days=30):
print("告警:证书将在 30 天内到期,请及时续期!")
return 1

print("证书状态正常。")
return 0


if __name__ == "__main__":
sys.exit(main())

理解关键代码

首先,使用 datetime.fromisoformat() 解析带时区的 ISO 格式时间字符串:

1
expiry_time = datetime.fromisoformat(expiry_text)

当字符串包含 +00:00 时,解析结果会包含相应的 UTC 偏移信息。

接下来,统一转换为 UTC:

1
2
expiry_time = expiry_time.astimezone(timezone.utc)
now = datetime.now(timezone.utc)

这样,参与比较和相减的两个对象都带有时区信息,并且使用统一的 UTC 时区。

最后,计算剩余时间:

1
remaining = expiry_time - now

如果剩余时间小于或等于零,说明证书已经到期;如果剩余时间小于 30 天,就产生告警。

需要注意,本例中的证书到期时间是固定的演示数据。生产环境中应从真实证书中读取到期时间,并确认解析出的时区信息正确。

如果证书信息来自其他工具,例如 openssl,需要根据其输出格式进行解析,不能假定所有证书信息都采用同一种字符串格式。

使用 datetime 时需要注意什么?

学习日期时间模块时,还有几个常见问题需要特别留意。

不要把字符串直接当作日期时间计算

下面的写法不能实现预期的日期加法:

1
2
3
4
5
6
from datetime import timedelta

time_text = "2026-12-01 23:30:00"

# 错误示例:字符串不能直接与 timedelta 相加
# future = time_text + timedelta(days=1)

正确做法是先使用 datetime.strptime() 将字符串解析为日期时间对象,再进行时间计算。

不要混用带时区和不带时区的日期时间

如果一个对象是 naive datetime,另一个对象是 aware datetime,直接比较或相减会产生异常。

处理日志、证书、跨地域服务器和云平台接口时,应先确认数据是否带有时区,再统一时区后进行计算。

计算程序耗时优先使用 perf_counter()

如果目的是记录任务开始时间、结束时间,使用 datetime 很合适。

如果目的是测量一段程序执行了多久,推荐使用:

1
2
3
4
5
6
7
8
9
10
import time

start = time.perf_counter()

# 模拟任务
sum(range(1_000_000))

end = time.perf_counter()

print(f"程序耗时:{end - start:.6f} 秒")

这样可以避免系统日历时间调整对耗时测量产生的影响。

datetime 常用功能速查

功能用法说明
获取当前日期时间datetime.now()获取本地日期时间
获取 UTC 时间datetime.now(timezone.utc)获取带 UTC 时区信息的时间
获取当前日期date.today()获取本地日期
获取日期部分now.date()提取日期
获取时间部分now.time()提取时间,不保留时区信息
创建时间间隔timedelta(days=1)表示一天的时间间隔
日期时间加减now + timedelta(days=1)计算未来或过去的时间
格式化日期时间now.strftime("%Y-%m-%d")转换为字符串
解析日期字符串datetime.strptime(text, fmt)按指定格式解析字符串
解析 ISO 时间datetime.fromisoformat(text)解析 ISO 格式字符串
计算时间差end - start得到 timedelta
获取时间差总秒数duration.total_seconds()获取总秒数
测量程序耗时time.perf_counter()获取适合测量耗时的计数值

如果想继续学习更多功能,可以查看 Python 官方文档:

写在最后

本篇我们学习了 Python 标准库中的日期和时间处理方法。

通过 date、time、datetime 和 timedelta,我们认识了日期、时间、时间点和时间间隔之间的区别;通过 strftime() 和 strptime(),我们掌握了日期时间对象与字符串之间的转换;通过 time.perf_counter(),我们学会了更适合测量程序耗时的方法。

在运维实战中,我们进一步将这些知识应用到了服务器巡检报告生成、异常日期输入处理和 TLS 证书到期检查中。

日期时间处理看起来简单,但在真实环境中,还需要注意时区、时间格式、异常输入,以及系统时间变化等问题。