23、Python 教程 - Python中的Time与Datetime模块

前言
在Python开发中,时间处理是一个看似简单却暗藏杀机的领域。很多开发者从入门到入职,写过无数次time.time()和datetime.now(),却在真正的企业项目中栽过跟头:线上日志时间对不上、跨时区订单错乱、夏令时导致定时任务重复执行、数据库存的时间查出来差了8小时。这些问题的根源,往往不是业务逻辑写错了,而是对Python的time和datetime模块理解不够透彻。
本文将从底层原理到企业实战,系统拆解Python时间处理的全部核心知识点,配合可直接运行的代码和真实项目场景,让你读完不仅能上手,还能在面试和工作中从容应对各类时间相关的疑难杂症。
文章目录
- [23、Python 教程 - Python中的Time与Datetime模块](#23、Python 教程 - Python中的Time与Datetime模块)
-
- 前言
- 一、痛点场景:那些年我们踩过的时间坑
-
- [1.1 场景一:线上订单时间凭空消失8小时](#1.1 场景一:线上订单时间凭空消失8小时)
- [1.2 场景二:定时任务在夏令时当天执行了两次](#1.2 场景二:定时任务在夏令时当天执行了两次)
- [1.3 场景三:第三方API返回的时间解析失败](#1.3 场景三:第三方API返回的时间解析失败)
- [1.4 场景四:时间戳精度混乱导致缓存击穿](#1.4 场景四:时间戳精度混乱导致缓存击穿)
- 二、痛点解决方案:四条黄金法则
- 三、是什么:Python时间处理的两大核心模块
-
- [3.1 time模块:贴近操作系统的底层时间接口](#3.1 time模块:贴近操作系统的底层时间接口)
-
- [3.1.1 时间戳(Timestamp)](#3.1.1 时间戳(Timestamp))
- [3.1.2 结构化时间(struct_time)](#3.1.2 结构化时间(struct_time))
- [3.1.3 字符串时间(Formatted String)](#3.1.3 字符串时间(Formatted String))
- [3.2 datetime模块:面向对象的高级时间接口](#3.2 datetime模块:面向对象的高级时间接口)
- 四、为什么用:time与datetime的分工与选择
- 五、演进历史:Python时间处理的三十年变迁
-
- [5.1 1991年:Python诞生,time模块是唯一选择](#5.1 1991年:Python诞生,time模块是唯一选择)
- [5.2 2003年:Python 2.3引入datetime模块](#5.2 2003年:Python 2.3引入datetime模块)
- [5.3 2016年:Python 3.6的PEP 495------解决夏令时歧义](#5.3 2016年:Python 3.6的PEP 495——解决夏令时歧义)
- [5.4 2020年:Python 3.9的PEP 615------zoneinfo内置时区支持](#5.4 2020年:Python 3.9的PEP 615——zoneinfo内置时区支持)
- [5.5 当下:UTC存储 + zoneinfo处理成为最佳实践](#5.5 当下:UTC存储 + zoneinfo处理成为最佳实践)
- 六、怎么用:从基础到进阶的完整教程
-
- [6.1 time模块:三种时间形式的相互转换](#6.1 time模块:三种时间形式的相互转换)
-
- [6.1.1 时间戳 -> 结构化时间](#6.1.1 时间戳 -> 结构化时间)
- [6.1.2 结构化时间 -> 时间戳](#6.1.2 结构化时间 -> 时间戳)
- [6.1.3 结构化时间 -> 字符串](#6.1.3 结构化时间 -> 字符串)
- [6.1.4 字符串 -> 结构化时间](#6.1.4 字符串 -> 结构化时间)
- [6.1.5 时间戳 -> 字符串](#6.1.5 时间戳 -> 字符串)
- [6.1.6 程序休眠与性能计时](#6.1.6 程序休眠与性能计时)
- [6.2 datetime模块:核心类的使用](#6.2 datetime模块:核心类的使用)
-
- [6.2.1 date类:只处理日期](#6.2.1 date类:只处理日期)
- [6.2.2 time类:只处理时间](#6.2.2 time类:只处理时间)
- [6.2.3 datetime类:日期+时间的组合](#6.2.3 datetime类:日期+时间的组合)
- [6.2.4 timedelta类:时间间隔的运算](#6.2.4 timedelta类:时间间隔的运算)
- [6.2.5 时间戳与datetime的互转](#6.2.5 时间戳与datetime的互转)
- [6.2.6 字符串与datetime的互转](#6.2.6 字符串与datetime的互转)
- [6.3 时区处理:从pytz到zoneinfo的迁移](#6.3 时区处理:从pytz到zoneinfo的迁移)
-
- [6.3.1 传统方式:pytz(老项目维护用)](#6.3.1 传统方式:pytz(老项目维护用))
- [6.3.2 现代方式:zoneinfo(Python 3.9+推荐)](#6.3.2 现代方式:zoneinfo(Python 3.9+推荐))
- [6.3.3 pytz与zoneinfo的核心区别](#6.3.3 pytz与zoneinfo的核心区别)
- 七、企业项目实战:真实场景的代码实现
-
- [7.1 实战一:统一时间工具类](#7.1 实战一:统一时间工具类)
- [7.2 实战二:日志系统的时间戳处理](#7.2 实战二:日志系统的时间戳处理)
- [7.3 实战三:定时任务的时区安全实现](#7.3 实战三:定时任务的时区安全实现)
- [7.4 实战四:订单超时自动取消](#7.4 实战四:订单超时自动取消)
- [7.5 实战五:跨时区会议调度系统](#7.5 实战五:跨时区会议调度系统)
- [7.6 实战六:数据统计中的时间窗口聚合](#7.6 实战六:数据统计中的时间窗口聚合)
- 八、竞品对比:四大时间处理库的优劣势
-
- [8.1 各库的典型使用场景](#8.1 各库的典型使用场景)
- [8.2 选型建议](#8.2 选型建议)
- 九、常用场景速查手册
-
- [9.1 获取当前时间的各种形式](#9.1 获取当前时间的各种形式)
- [9.2 时间格式转换速查](#9.2 时间格式转换速查)
- [9.3 常见业务场景代码片段](#9.3 常见业务场景代码片段)
- 十、面试官高频面试题
-
- 面试题1:time.time()和datetime.now()有什么区别?
- [面试题2:什么是naive datetime和aware datetime?为什么不能混用?](#面试题2:什么是naive datetime和aware datetime?为什么不能混用?)
- 面试题3:如何处理跨时区问题?pytz和zoneinfo怎么选?
- 面试题4:strftime和strptime的区别是什么?%m和%M有什么不同?
- 面试题5:timedelta可以表示一个月吗?为什么?
- 面试题6:datetime.utcnow()为什么不推荐使用?
- 面试题7:如何测量代码执行时间?为什么不用time.time()?
- 面试题8:什么是Unix时间戳?2038年问题是什么?
- 面试题9:如何将"3分钟前"这种人性化时间字符串转换为datetime?
- 面试题10:数据库中时间字段应该用什么类型?datetime还是timestamp?
- 十一、避坑指南:十个最常见的时间处理错误
- 十二、总结
一、痛点场景:那些年我们踩过的时间坑
1.1 场景一:线上订单时间凭空消失8小时
某电商平台后端工程师小王,在本地开发时一切正常,订单创建时间显示准确。代码部署到阿里云ECS(服务器时区为UTC)后,用户下单时间在数据库里全部少了8小时。运营反馈"凌晨的订单显示成了前一天下午",客服电话被打爆。
问题根源 :代码中使用了datetime.now()获取本地时间,而服务器时区与开发者本地机器不一致。datetime.now()返回的是naive datetime(无时区信息),它依赖运行环境的系统时区,换台机器结果就变了。
1.2 场景二:定时任务在夏令时当天执行了两次
某跨境SaaS平台有一个每日凌晨2:30执行的报表生成任务。在北美东部时区进入夏令时的那天,任务执行了两次;在退出夏令时的那天,任务直接跳过了一次。财务对账发现当天数据重复或缺失。
问题根源:使用naive datetime做定时调度,没有考虑DST(夏令时)切换导致的本地时间歧义或缺失。Python 3.6之前甚至无法区分"回拨"后重复的那一个小时。
1.3 场景三:第三方API返回的时间解析失败
某系统对接外部支付网关,对方返回的时间格式为"2024-03-15T14:30:00+08:00"(ISO 8601带时区),开发同学用datetime.strptime(s, "%Y-%m-%d %H:%M:%S")解析,直接抛出ValueError。临时写正则截取前19个字符,又把时区信息丢了,后续对账时时间对不上。
问题根源 :对strptime的格式化符不熟悉,不知道%z可以解析时区偏移量,也不知道Python 3.7+的datetime.fromisoformat()已经原生支持ISO 8601解析。
1.4 场景四:时间戳精度混乱导致缓存击穿
某缓存系统用int(time.time())作为缓存key的时间维度,另一个模块用datetime.now().timestamp()生成时间戳,两者在毫秒级场景下精度不一致,导致缓存命中率骤降,数据库被打满。
问题根源 :没有统一时间戳的精度规范(秒级还是毫秒级),不同模块各自为政,time.time()返回浮点秒,而某些语言(如Java)习惯用毫秒级long类型时间戳,跨语言对接时极易出错。
二、痛点解决方案:四条黄金法则
针对上述痛点,业界总结出四条时间处理的黄金法则,在任何企业项目中都应该严格遵守:
法则一:存储统一用UTC,展示才转本地时区
数据库、缓存、消息队列中存储的时间,一律使用UTC时间戳或带时区的aware datetime。只有在前端展示给用户看的时候,才转换为用户所在时区的本地时间。这就像全球统一用格林威治标准时间对表,每个人再根据自己所在时区换算,不会因为"你说你的时间、我说我的时间"而产生混乱。
法则二:永远使用aware datetime,禁止naive datetime
datetime.now()返回的是naive datetime(tzinfo=None),它不知道自己属于哪个时区,就像一个没有写国籍的护照,到了边境就会被拦住。正确做法是datetime.now(timezone.utc)或datetime.now(ZoneInfo("Asia/Shanghai")),让每一个datetime对象都携带明确的时区信息。
法则三:API交互统一使用ISO 8601格式
系统之间传递时间,统一使用"2024-03-15T14:30:00+08:00"这种ISO 8601带时区偏移量的字符串格式,不要用"2024/3/15 2:30 PM"这种歧义格式。Python 3.7+的datetime.fromisoformat()和datetime.isoformat()可以直接完成双向转换。
法则四:时间戳精度全项目统一
在项目启动时就约定好时间戳精度:秒级(10位整数)还是毫秒级(13位整数)。所有模块、所有服务、所有跨语言对接都遵循同一约定,并在代码注释和接口文档中明确标注。

三、是什么:Python时间处理的两大核心模块
3.1 time模块:贴近操作系统的底层时间接口
time模块是Python标准库中最早的时间处理模块,它本质上是对C语言标准库中time.h的封装,提供与操作系统底层交互的时间函数。
专业解释 :time模块围绕三种时间表示形式展开------时间戳(timestamp)、结构化时间(struct_time)、字符串时间(formatted string),并提供它们之间的转换函数。
大白话 :time模块就像一块老式电子表,功能基础但直接和硬件对话。它告诉你"从1970年1月1日零点到现在过了多少秒",也能把这个秒数翻译成"2024年3月15日14点30分"这样的人类可读格式。
生活案例 :你去银行办业务,柜员的电脑屏幕上显示的是一串数字1710484200(时间戳),这是机器内部用的;打印在回执单上的是2024-03-15 14:30:00(字符串时间),这是给你看的;而银行系统内部处理时,可能用的是一个包含年、月、日、时、分、秒等9个字段的结构体(struct_time)。
3.1.1 时间戳(Timestamp)
时间戳是一个浮点数,表示从1970年1月1日00:00:00 UTC(Unix纪元)到当前时刻经过的秒数。它是机器最友好的时间表示形式,因为就是一个数字,存储和比较都极其高效。
python
import time
# 获取当前时间戳(秒级,浮点数)
timestamp = time.time()
print(f"当前时间戳: {timestamp}")
# 输出示例: 当前时间戳: 1710484200.123456
# 毫秒级时间戳(Java等语言常用)
millis = int(time.time() * 1000)
print(f"毫秒级时间戳: {millis}")
# 输出示例: 毫秒级时间戳: 1710484200123
为什么是1970年? 这是Unix操作系统的诞生年代,Ken Thompson和Dennis Ritchie在1969年开始开发Unix,1970年1月1日被选定为时间纪元的起点。这个约定后来被所有编程语言和操作系统继承,成为了行业标准。就像公历以耶稣诞生年为公元元年一样,计算机世界以Unix诞生年为时间元年。
3.1.2 结构化时间(struct_time)
struct_time是一个包含9个字段的命名元组,把时间拆成了年、月、日、时、分、秒、星期、一年中第几天、是否夏令时等独立字段,方便按需取用。
python
import time
# 获取本地时区的结构化时间
local = time.localtime()
print(f"本地结构化时间: {local}")
# 输出: time.struct_time(tm_year=2024, tm_mon=3, tm_mday=15,
# tm_hour=14, tm_min=30, tm_sec=0, tm_wday=4, tm_yday=75, tm_isdst=0)
# 获取UTC时区的结构化时间
utc = time.gmtime()
print(f"UTC结构化时间: {utc}")
# 按字段访问
print(f"年份: {local.tm_year}")
print(f"月份: {local.tm_mon}")
print(f"日期: {local.tm_mday}")
print(f"小时: {local.tm_hour}")
print(f"星期(0=周一): {local.tm_wday}")
print(f"一年中第几天: {local.tm_yday}")
struct_time的9个字段说明:
| 字段 | 含义 | 取值范围 |
|---|---|---|
| tm_year | 年 | 如2024 |
| tm_mon | 月 | 1-12 |
| tm_mday | 日 | 1-31 |
| tm_hour | 时 | 0-23 |
| tm_min | 分 | 0-59 |
| tm_sec | 秒 | 0-61(60/61为闰秒) |
| tm_wday | 星期 | 0-6(0=周一) |
| tm_yday | 一年中第几天 | 1-366 |
| tm_isdst | 是否夏令时 | 0=否, 1=是, -1=未知 |
3.1.3 字符串时间(Formatted String)
字符串时间是人类最易读的形式,通过strftime函数可以将结构化时间格式化为任意样式的字符串。
python
import time
# ctime: 时间戳 -> 固定格式字符串
print(time.ctime())
# 输出示例: Fri Mar 15 14:30:00 2024
# asctime: struct_time -> 固定格式字符串
print(time.asctime(time.localtime()))
# 输出示例: Fri Mar 15 14:30:00 2024
# strftime: 自定义格式
now = time.localtime()
print(time.strftime("%Y-%m-%d %H:%M:%S", now))
# 输出: 2024-03-15 14:30:00
print(time.strftime("%Y/%m/%d", now))
# 输出: 2024/03/15
print(time.strftime("%H:%M:%S", now))
# 输出: 14:30:00
常用格式化符速查表:
| 符号 | 含义 | 示例 |
|---|---|---|
| %Y | 四位年份 | 2024 |
| %y | 两位年份 | 24 |
| %m | 月份(01-12) | 03 |
| %d | 日期(01-31) | 15 |
| %H | 小时(24小时制,00-23) | 14 |
| %I | 小时(12小时制,01-12) | 02 |
| %M | 分钟(00-59) | 30 |
| %S | 秒(00-59) | 00 |
| %p | AM/PM | PM |
| %A | 星期全称 | Friday |
| %a | 星期缩写 | Fri |
| %B | 月份全称 | March |
| %b | 月份缩写 | Mar |
| %z | 时区偏移量 | +0800 |
| %Z | 时区名称 | CST |
| %j | 一年中第几天 | 075 |
| %W | 一年中第几周 | 10 |
记忆口诀 :strftime中的f代表format(格式化输出),strptime中的p代表parse(解析输入)。一个是把时间对象变成字符串,一个是把字符串变成时间对象,方向相反。
3.2 datetime模块:面向对象的高级时间接口
datetime模块是Python 2.3引入的面向对象时间处理库,提供了date、time、datetime、timedelta、tzinfo等类,比time模块更符合Python的面向对象编程风格,也是企业项目中的首选。
专业解释 :datetime模块定义了以下核心类:date(日期)、time(时间)、datetime(日期时间组合)、timedelta(时间间隔)、tzinfo(时区信息抽象基类)、timezone(固定偏移时区实现)。Python 3.9起,标准库新增zoneinfo.ZoneInfo提供IANA时区数据库支持。
大白话 :如果说time模块是老式电子表,那datetime模块就是智能手表。它把年、月、日、时、分、秒封装成了对象,你可以直接对两个时间做减法得到间隔,可以给时间加上几天几小时,可以比较两个时间的先后,操作起来更直观、更Pythonic。
生活案例:你用日历APP安排行程,APP里每一个事件都是一个"日期+时间"的对象(datetime),你可以问"这个会议距离现在还有多久"(timedelta),可以把纽约时间的会议换算成北京时间(时区转换),这些操作用datetime模块一行代码就能搞定。

四、为什么用:time与datetime的分工与选择
很多初学者会困惑:Python为什么要有两个时间模块?该用哪个?
答案是:两者定位不同,互为补充。
| 维度 | time模块 | datetime模块 |
|---|---|---|
| 设计哲学 | 过程式,C风格封装 | 面向对象,Pythonic |
| 核心数据 | struct_time(元组) | datetime(对象) |
| 时间计算 | 需手动转换,不直观 | 支持直接加减比较 |
| 时区支持 | 弱,依赖系统时区 | 强,tzinfo可扩展 |
| 适用场景 | 性能敏感、底层系统调用 | 业务逻辑、日期运算 |
| 可读性 | 一般 | 好 |
选择建议:
- 需要获取当前时间戳、让程序休眠(
time.sleep())、测量代码执行时间(time.perf_counter())------用time模块 - 需要做日期运算、时间比较、格式化输出、时区转换------用
datetime模块 - 企业项目中,业务层代码优先使用
datetime,底层工具函数可使用time
五、演进历史:Python时间处理的三十年变迁
Python的时间处理能力并非一蹴而就,而是经历了长达三十年的逐步演进。了解这段历史,能帮助你理解为什么有些API设计成现在这个样子,以及为什么老代码和新代码的写法差异巨大。

5.1 1991年:Python诞生,time模块是唯一选择
Python最初版本只有time模块,直接封装C标准库的time.h。那时候处理时间就是和struct_time、时间戳打交道,写法非常底层。这就像早期手机只有拨号和短信功能,简单但够用。
5.2 2003年:Python 2.3引入datetime模块
随着Python应用场景扩大,过程式的time模块越来越难以满足复杂业务需求。Python 2.3正式引入datetime模块,带来了面向对象的时间处理方式。datetime类支持直接的加减运算和比较,timedelta让时间间隔计算变得优雅。
但此时的datetime有一个致命缺陷:时区支持极其薄弱 。tzinfo只是一个抽象基类,标准库没有提供任何具体实现,开发者必须自己写时区类,或者依赖第三方库pytz。
5.3 2016年:Python 3.6的PEP 495------解决夏令时歧义
在实行夏令时的地区,每年秋天时钟会从凌晨2点拨回1点,这意味着"凌晨1:30"这个本地时间会出现两次。Python 3.6之前的datetime无法区分这两个时刻,可能导致定时任务重复执行或时间计算错误。
PEP 495为datetime和time对象新增了fold属性:fold=0表示第一次出现的那个时刻,fold=1表示第二次出现的那个时刻。这就像给重复的时间点贴上了"第一次"和"第二次"的标签。
python
from datetime import datetime, timezone, timedelta
# 模拟夏令时回拨场景(以美国东部时区为例)
# fold=0: 第一次出现的1:30(夏令时期间)
# fold=1: 第二次出现的1:30(标准时间期间)
dt1 = datetime(2024, 11, 3, 1, 30, fold=0)
dt2 = datetime(2024, 11, 3, 1, 30, fold=1)
print(f"fold=0: {dt1}")
print(f"fold=1: {dt2}")
5.4 2020年:Python 3.9的PEP 615------zoneinfo内置时区支持
这是Python时间处理史上最重要的一次升级。PEP 615将zoneinfo模块加入标准库,原生支持IANA时区数据库(又称Olson数据库,是全球时区规则的权威来源)。
在此之前,处理时区必须安装第三方库pytz;在此之后,from zoneinfo import ZoneInfo一行代码就能使用全球任意时区。pytz的官方文档也明确建议:Python 3.9+项目应使用标准库zoneinfo,pytz仅用于向后兼容老代码。
5.5 当下:UTC存储 + zoneinfo处理成为最佳实践
经过三十年演进,业界终于形成了共识:内部存储统一用UTC时间戳或aware datetime,时区转换统一用zoneinfo,API交互统一用ISO 8601。这套方案能彻底杜绝99%的时间相关bug。
六、怎么用:从基础到进阶的完整教程
6.1 time模块:三种时间形式的相互转换

6.1.1 时间戳 -> 结构化时间
python
import time
ts = time.time()
# 时间戳 -> 本地时区struct_time
local_struct = time.localtime(ts)
print(f"本地结构化时间: {local_struct}")
# 时间戳 -> UTC时区struct_time
utc_struct = time.gmtime(ts)
print(f"UTC结构化时间: {utc_struct}")
6.1.2 结构化时间 -> 时间戳
python
import time
struct = time.localtime()
ts = time.mktime(struct)
print(f"结构化时间转时间戳: {ts}")
注意:mktime只能将本地时区的struct_time转为时间戳。如果传入的是UTC的struct_time,结果会出错。
6.1.3 结构化时间 -> 字符串
python
import time
struct = time.localtime()
# 固定格式
print(time.asctime(struct))
# 输出: Fri Mar 15 14:30:00 2024
# 自定义格式
print(time.strftime("%Y-%m-%d %H:%M:%S", struct))
# 输出: 2024-03-15 14:30:00
6.1.4 字符串 -> 结构化时间
python
import time
date_str = "2024-03-15 14:30:00"
struct = time.strptime(date_str, "%Y-%m-%d %H:%M:%S")
print(f"字符串转结构化时间: {struct}")
6.1.5 时间戳 -> 字符串
python
import time
ts = time.time()
print(time.ctime(ts))
# 输出: Fri Mar 15 14:30:00 2024
6.1.6 程序休眠与性能计时
python
import time
# 程序休眠1秒
time.sleep(1)
# 测量代码执行时间(推荐用perf_counter,精度更高)
start = time.perf_counter()
# 执行一些操作
total = sum(range(1_000_000))
end = time.perf_counter()
print(f"执行耗时: {end - start:.6f}秒")
# 单调时钟(不会因系统时间调整而回拨,适合计算间隔)
start = time.monotonic()
time.sleep(0.5)
end = time.monotonic()
print(f"单调时钟耗时: {end - start:.6f}秒")
企业级提醒 :测量代码性能时永远用time.perf_counter()而不是time.time()。因为time.time()是系统时钟,如果在测量期间系统时间被NTP同步或人为调整,结果就会出错。perf_counter()和monotonic()是单调时钟,只会往前走,不会回拨。
6.2 datetime模块:核心类的使用
6.2.1 date类:只处理日期
python
from datetime import date
# 获取今天
today = date.today()
print(f"今天: {today}")
print(f"年: {today.year}, 月: {today.month}, 日: {today.day}")
# 构造指定日期
d = date(2024, 3, 15)
print(f"指定日期: {d}")
# 日期的星期
print(f"星期(0=周一): {d.weekday()}")
print(f"星期(1=周一): {d.isoweekday()}")
# 替换年月日,返回新对象
d2 = d.replace(year=2025, month=6)
print(f"替换后: {d2}")
# 格式化输出
print(d.strftime("%Y年%m月%d日"))
# 输出: 2024年03月15日
# ISO格式
print(d.isoformat())
# 输出: 2024-03-15
# 从字符串解析
d3 = date.fromisoformat("2024-03-15")
print(f"解析ISO日期: {d3}")
# 从时间戳构造
import time
d4 = date.fromtimestamp(time.time())
print(f"从时间戳构造: {d4}")
6.2.2 time类:只处理时间
python
from datetime import time
# 构造时间
t = time(14, 30, 0)
print(f"时间: {t}")
print(f"时: {t.hour}, 分: {t.minute}, 秒: {t.second}, 微秒: {t.microsecond}")
# 带时区的time
from datetime import timezone, timedelta
t_tz = time(14, 30, tzinfo=timezone(timedelta(hours=8)))
print(f"带时区时间: {t_tz}")
print(f"时区偏移: {t_tz.tzinfo.utcoffset(None)}")
6.2.3 datetime类:日期+时间的组合
python
from datetime import datetime
# 获取当前本地时间(naive,不推荐在企业项目中使用)
now_naive = datetime.now()
print(f"当前本地时间(naive): {now_naive}")
# 获取当前UTC时间(aware,推荐)
from datetime import timezone
now_utc = datetime.now(timezone.utc)
print(f"当前UTC时间(aware): {now_utc}")
# 获取指定时区的当前时间(Python 3.9+)
from zoneinfo import ZoneInfo
now_shanghai = datetime.now(ZoneInfo("Asia/Shanghai"))
print(f"当前北京时间(aware): {now_shanghai}")
now_newyork = datetime.now(ZoneInfo("America/New_York"))
print(f"当前纽约时间(aware): {now_newyork}")
# 构造指定日期时间
dt = datetime(2024, 3, 15, 14, 30, 0)
print(f"构造的datetime: {dt}")
# 从date和time组合
from datetime import date, time
d = date(2024, 3, 15)
t = time(14, 30)
dt_combined = datetime.combine(d, t)
print(f"组合的datetime: {dt_combined}")
# 提取日期和时间部分
print(f"日期部分: {dt.date()}")
print(f"时间部分: {dt.time()}")
6.2.4 timedelta类:时间间隔的运算
timedelta是datetime模块中最实用的类之一,它表示两个时间点之间的差值,可以直接和datetime对象做加减运算。
python
from datetime import datetime, timedelta, timezone
now = datetime.now(timezone.utc)
# 构造时间间隔
one_day = timedelta(days=1)
three_hours = timedelta(hours=3)
thirty_minutes = timedelta(minutes=30)
one_week = timedelta(weeks=1)
complex_delta = timedelta(days=1, hours=2, minutes=30, seconds=45)
# 时间加法:3天后
future = now + timedelta(days=3)
print(f"3天后: {future}")
# 时间减法:7天前
past = now - timedelta(days=7)
print(f"7天前: {past}")
# 两个datetime相减得到timedelta
dt1 = datetime(2024, 3, 15, 14, 30, tzinfo=timezone.utc)
dt2 = datetime(2024, 3, 20, 10, 0, tzinfo=timezone.utc)
delta = dt2 - dt1
print(f"时间差: {delta}")
print(f"相差天数: {delta.days}")
print(f"相差秒数(不含天数): {delta.seconds}")
print(f"总秒数: {delta.total_seconds()}")
# timedelta的乘除
half_day = one_day / 2
print(f"半天: {half_day}")
double_week = one_week * 2
print(f"两周: {double_week}")
重要提醒 :timedelta(days=30)不等于"一个月"。因为每个月的天数不同(28/29/30/31天),用30天代替一个月会在长期计算中累积误差。需要按月计算时,应使用dateutil.relativedelta或手动处理月份进位。
6.2.5 时间戳与datetime的互转
python
from datetime import datetime, timezone
import time
# datetime -> 时间戳
dt = datetime(2024, 3, 15, 14, 30, 0, tzinfo=timezone.utc)
ts = dt.timestamp()
print(f"datetime转时间戳: {ts}")
# 时间戳 -> datetime(本地时区,naive)
dt_local = datetime.fromtimestamp(ts)
print(f"时间戳转本地datetime: {dt_local}")
# 时间戳 -> datetime(UTC,aware,推荐)
dt_utc = datetime.fromtimestamp(ts, tz=timezone.utc)
print(f"时间戳转UTC datetime: {dt_utc}")
# 时间戳 -> datetime(指定时区)
from zoneinfo import ZoneInfo
dt_shanghai = datetime.fromtimestamp(ts, tz=ZoneInfo("Asia/Shanghai"))
print(f"时间戳转上海datetime: {dt_shanghai}")
企业级坑点 :datetime.fromtimestamp(ts)不传tz参数时返回naive datetime,且使用系统本地时区。在容器化部署中,容器的系统时区可能是UTC,这会导致和本地开发环境的结果不一致。永远显式传入tz=timezone.utc。
6.2.6 字符串与datetime的互转
python
from datetime import datetime
# datetime -> 字符串(格式化)
now = datetime.now()
print(now.strftime("%Y-%m-%d %H:%M:%S"))
print(now.strftime("%Y/%m/%d"))
print(now.strftime("%Y-%m-%dT%H:%M:%S%z"))
# ISO 8601格式输出(推荐用于API)
print(now.isoformat())
# 字符串 -> datetime(解析)
dt = datetime.strptime("2024-03-15 14:30:00", "%Y-%m-%d %H:%M:%S")
print(f"解析结果: {dt}")
# 解析带时区的字符串
dt_tz = datetime.strptime("2024-03-15T14:30:00+0800", "%Y-%m-%dT%H:%M:%S%z")
print(f"解析带时区: {dt_tz}")
# Python 3.7+:直接解析ISO 8601格式(推荐)
dt_iso = datetime.fromisoformat("2024-03-15T14:30:00+08:00")
print(f"fromisoformat解析: {dt_iso}")
# Python 3.11+:fromisoformat支持Z后缀(UTC)
# dt_z = datetime.fromisoformat("2024-03-15T14:30:00Z")
6.3 时区处理:从pytz到zoneinfo的迁移
6.3.1 传统方式:pytz(老项目维护用)
python
# 安装: pip install pytz
import pytz
from datetime import datetime
# 创建时区对象
shanghai_tz = pytz.timezone("Asia/Shanghai")
newyork_tz = pytz.timezone("America/New_York")
# 注意:pytz不能直接用tzinfo参数构造datetime,必须用localize
# 错误写法(DST切换时会出错):
# dt = datetime(2024, 3, 15, 14, 30, tzinfo=shanghai_tz)
# 正确写法:用localize将naive datetime转为aware
naive_dt = datetime(2024, 3, 15, 14, 30)
aware_dt = shanghai_tz.localize(naive_dt)
print(f"pytz本地化: {aware_dt}")
# 时区转换
ny_dt = aware_dt.astimezone(newyork_tz)
print(f"转换为纽约时间: {ny_dt}")
# 获取当前时间
now_shanghai = datetime.now(shanghai_tz)
print(f"当前上海时间: {now_shanghai}")
6.3.2 现代方式:zoneinfo(Python 3.9+推荐)
python
from datetime import datetime
from zoneinfo import ZoneInfo
# 创建时区对象(直接用IANA时区名)
shanghai = ZoneInfo("Asia/Shanghai")
newyork = ZoneInfo("America/New_York")
utc = ZoneInfo("UTC")
# 直接用tzinfo参数构造(不需要localize,这是zoneinfo比pytz优雅的地方)
dt = datetime(2024, 3, 15, 14, 30, tzinfo=shanghai)
print(f"zoneinfo构造: {dt}")
# 获取当前指定时区时间
now_shanghai = datetime.now(shanghai)
now_newyork = datetime.now(newyork)
print(f"上海: {now_shanghai}")
print(f"纽约: {now_newyork}")
# 时区转换(astimezone)
dt_ny = dt.astimezone(newyork)
print(f"上海转纽约: {dt_ny}")
# 转UTC
dt_utc = dt.astimezone(utc)
print(f"上海转UTC: {dt_utc}")
# 列出所有可用时区
import zoneinfo
all_zones = zoneinfo.available_timezones()
print(f"可用时区数量: {len(all_zones)}")
print(f"包含Asia/Shanghai: {'Asia/Shanghai' in all_zones}")
zoneinfo的系统依赖 :zoneinfo优先使用操作系统自带的IANA时区数据(Linux/macOS通常自带,Windows可能没有)。如果系统没有时区数据,需要安装tzdata包:pip install tzdata。在Docker容器中,建议基础镜像安装tzdata包以确保时区数据完整。
6.3.3 pytz与zoneinfo的核心区别
| 特性 | pytz | zoneinfo(Python 3.9+) |
|---|---|---|
| 来源 | 第三方库,需pip安装 | 标准库内置 |
| 时区数据 | 自带IANA数据 | 使用系统时区数据或tzdata包 |
| 构造方式 | 必须用localize(),不能直接传tzinfo | 可直接传tzinfo参数 |
| DST处理 | 需注意localize时机 | 自动正确处理 |
| 官方推荐 | 仅用于老代码兼容 | 新项目首选 |
| 维护状态 | 维护模式,不再推荐 | 活跃维护,与CPython同步 |
七、企业项目实战:真实场景的代码实现

7.1 实战一:统一时间工具类
在中大型项目中,应该封装一个统一的时间工具类,所有模块通过它来获取和处理时间,避免各自为政。
python
# utils/time_utils.py
from datetime import datetime, timezone, timedelta
from zoneinfo import ZoneInfo
from typing import Optional
# 项目统一使用的时区
DEFAULT_TZ = ZoneInfo("Asia/Shanghai")
UTC = timezone.utc
class TimeUtils:
"""项目统一时间工具类"""
@staticmethod
def now_utc() -> datetime:
"""获取当前UTC时间(aware)"""
return datetime.now(UTC)
@staticmethod
def now_local() -> datetime:
"""获取当前项目默认时区时间(aware)"""
return datetime.now(DEFAULT_TZ)
@staticmethod
def now_timestamp() -> float:
"""获取当前UTC时间戳(秒级)"""
return datetime.now(UTC).timestamp()
@staticmethod
def now_timestamp_ms() -> int:
"""获取当前UTC时间戳(毫秒级)"""
return int(datetime.now(UTC).timestamp() * 1000)
@staticmethod
def to_utc(dt: datetime) -> datetime:
"""将任意aware datetime转为UTC"""
if dt.tzinfo is None:
raise ValueError("naive datetime不允许转换,请先附加时区")
return dt.astimezone(UTC)
@staticmethod
def to_local(dt: datetime) -> datetime:
"""将任意aware datetime转为项目默认时区"""
if dt.tzinfo is None:
raise ValueError("naive datetime不允许转换,请先附加时区")
return dt.astimezone(DEFAULT_TZ)
@staticmethod
def to_iso(dt: datetime) -> str:
"""将datetime转为ISO 8601字符串(带时区)"""
if dt.tzinfo is None:
raise ValueError("naive datetime不允许输出,请先附加时区")
return dt.isoformat()
@staticmethod
def from_iso(s: str) -> datetime:
"""从ISO 8601字符串解析datetime(aware)"""
dt = datetime.fromisoformat(s)
if dt.tzinfo is None:
# 如果解析结果是naive,默认当作项目时区处理
dt = dt.replace(tzinfo=DEFAULT_TZ)
return dt
@staticmethod
def from_timestamp(ts: float, tz: Optional[ZoneInfo] = None) -> datetime:
"""从时间戳构造aware datetime"""
return datetime.fromtimestamp(ts, tz=tz or UTC)
@staticmethod
def start_of_day(dt: datetime) -> datetime:
"""获取当天零点(保持原时区)"""
return dt.replace(hour=0, minute=0, second=0, microsecond=0)
@staticmethod
def end_of_day(dt: datetime) -> datetime:
"""获取当天最后一刻(保持原时区)"""
return dt.replace(hour=23, minute=59, second=59, microsecond=999999)
@staticmethod
def start_of_month(dt: datetime) -> datetime:
"""获取当月第一天零点"""
return dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# 使用示例
if __name__ == "__main__":
print(f"UTC现在: {TimeUtils.now_utc()}")
print(f"上海现在: {TimeUtils.now_local()}")
print(f"时间戳: {TimeUtils.now_timestamp()}")
print(f"毫秒时间戳: {TimeUtils.now_timestamp_ms()}")
local_now = TimeUtils.now_local()
print(f"今天零点: {TimeUtils.start_of_day(local_now)}")
print(f"今天结束: {TimeUtils.end_of_day(local_now)}")
print(f"ISO格式: {TimeUtils.to_iso(local_now)}")
7.2 实战二:日志系统的时间戳处理
企业级日志系统需要精确到微秒的时间戳,并且要保证多线程/多进程下的时间一致性。
python
# utils/logger_time.py
import logging
from datetime import datetime
from zoneinfo import ZoneInfo
class BeijingTimeFormatter(logging.Formatter):
"""自定义日志时间格式化器,统一使用北京时间"""
def formatTime(self, record, datefmt=None):
dt = datetime.fromtimestamp(record.created, tz=ZoneInfo("Asia/Shanghai"))
if datefmt:
return dt.strftime(datefmt)
# 默认格式:精确到毫秒
return dt.strftime("%Y-%m-%d %H:%M:%S") + f".{int(record.msecs):03d}"
# 配置日志
logger = logging.getLogger("myapp")
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
formatter = BeijingTimeFormatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.info("用户登录成功")
logger.warning("数据库连接池使用率超过80%")
logger.error("支付回调处理失败")
7.3 实战三:定时任务的时区安全实现
使用APScheduler(企业级定时任务库)时,必须显式指定时区,避免依赖系统时区。
python
# 安装: pip install apscheduler
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger
from zoneinfo import ZoneInfo
from datetime import datetime
scheduler = BackgroundScheduler(timezone=ZoneInfo("Asia/Shanghai"))
def daily_report():
"""每日凌晨2点生成报表"""
now = datetime.now(ZoneInfo("Asia/Shanghai"))
print(f"[{now}] 开始生成每日报表...")
# 业务逻辑...
def every_heartbeat():
"""每30秒心跳检测"""
print(f"[{datetime.now(ZoneInfo('Asia/Shanghai'))}] 心跳正常")
# 每天凌晨2点执行(显式指定时区)
scheduler.add_job(
daily_report,
trigger=CronTrigger(hour=2, minute=0, timezone=ZoneInfo("Asia/Shanghai")),
id="daily_report",
replace_existing=True
)
# 每30秒执行一次
scheduler.add_job(
every_heartbeat,
trigger="interval",
seconds=30,
id="heartbeat",
replace_existing=True
)
scheduler.start()
print("定时任务已启动")
# 保持主线程运行
try:
import time
while True:
time.sleep(60)
except (KeyboardInterrupt, SystemExit):
scheduler.shutdown()
7.4 实战四:订单超时自动取消
电商系统中,订单创建后30分钟未支付则自动取消。这是timedelta的经典应用场景。
python
# services/order_service.py
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
from typing import Dict
# 模拟订单存储
orders_db: Dict[str, dict] = {}
PAYMENT_TIMEOUT_MINUTES = 30
def create_order(order_id: str, user_id: str, amount: float) -> dict:
"""创建订单"""
now = datetime.now(timezone.utc)
order = {
"order_id": order_id,
"user_id": user_id,
"amount": amount,
"status": "pending", # pending: 待支付
"created_at": now,
"expire_at": now + timedelta(minutes=PAYMENT_TIMEOUT_MINUTES),
"paid_at": None
}
orders_db[order_id] = order
print(f"订单{order_id}已创建,支付截止时间: {order['expire_at']}")
return order
def pay_order(order_id: str) -> bool:
"""支付订单"""
order = orders_db.get(order_id)
if not order:
print(f"订单{order_id}不存在")
return False
now = datetime.now(timezone.utc)
if now > order["expire_at"]:
order["status"] = "cancelled"
print(f"订单{order_id}已超时,无法支付")
return False
order["status"] = "paid"
order["paid_at"] = now
print(f"订单{order_id}支付成功")
return True
def cancel_expired_orders():
"""扫描并取消超时订单(可放在定时任务中执行)"""
now = datetime.now(timezone.utc)
cancelled = []
for order_id, order in orders_db.items():
if order["status"] == "pending" and now > order["expire_at"]:
order["status"] = "cancelled"
cancelled.append(order_id)
print(f"订单{order_id}超时自动取消")
return cancelled
# 模拟使用
if __name__ == "__main__":
create_order("ORD001", "user123", 99.5)
create_order("ORD002", "user456", 199.0)
# 模拟ORD001立即支付
pay_order("ORD001")
# 模拟30分钟后扫描
print("\n--- 30分钟后扫描超时订单 ---")
cancel_expired_orders()
7.5 实战五:跨时区会议调度系统
跨国团队的会议调度系统需要处理多时区问题,让每个参会者看到自己本地时区的会议时间。
python
# services/meeting_scheduler.py
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from typing import List, Dict
class MeetingScheduler:
"""跨时区会议调度器"""
def __init__(self):
self.meetings: List[dict] = []
def schedule_meeting(
self,
title: str,
start_utc: datetime,
duration_minutes: int,
participants: List[Dict[str, str]]
) -> dict:
"""
创建会议,内部统一用UTC存储
participants: [{"name": "张三", "timezone": "Asia/Shanghai"}, ...]
"""
if start_utc.tzinfo is None:
raise ValueError("会议开始时间必须是aware datetime")
meeting = {
"title": title,
"start_utc": start_utc.astimezone(ZoneInfo("UTC")),
"end_utc": start_utc.astimezone(ZoneInfo("UTC")) + timedelta(minutes=duration_minutes),
"duration_minutes": duration_minutes,
"participants": participants
}
self.meetings.append(meeting)
return meeting
def get_meeting_for_user(self, meeting_idx: int, user_timezone: str) -> dict:
"""获取某个用户视角下的会议时间(转换为用户本地时区)"""
meeting = self.meetings[meeting_idx]
tz = ZoneInfo(user_timezone)
return {
"title": meeting["title"],
"start_local": meeting["start_utc"].astimezone(tz),
"end_local": meeting["end_utc"].astimezone(tz),
"duration_minutes": meeting["duration_minutes"]
}
def print_meeting_all_timezones(self, meeting_idx: int):
"""打印会议在所有参会者时区的时间"""
meeting = self.meetings[meeting_idx]
print(f"\n=== 会议: {meeting['title']} ===")
print(f"UTC时间: {meeting['start_utc'].strftime('%Y-%m-%d %H:%M')} ~ "
f"{meeting['end_utc'].strftime('%H:%M')}")
for p in meeting["participants"]:
tz = ZoneInfo(p["timezone"])
local_start = meeting["start_utc"].astimezone(tz)
local_end = meeting["end_utc"].astimezone(tz)
print(f" {p['name']} ({p['timezone']}): "
f"{local_start.strftime('%Y-%m-%d %H:%M')} ~ "
f"{local_end.strftime('%H:%M')}")
# 使用示例
if __name__ == "__main__":
scheduler = MeetingScheduler()
# 创建一个UTC时间上午9点的会议(即北京时间下午5点,纽约时间凌晨4点)
meeting_start = datetime(2024, 3, 15, 9, 0, tzinfo=ZoneInfo("UTC"))
scheduler.schedule_meeting(
title="Q2产品规划评审",
start_utc=meeting_start,
duration_minutes=60,
participants=[
{"name": "张三(北京)", "timezone": "Asia/Shanghai"},
{"name": "John(纽约)", "timezone": "America/New_York"},
{"name": "Hans(柏林)", "timezone": "Europe/Berlin"}
]
)
scheduler.print_meeting_all_timezones(0)
7.6 实战六:数据统计中的时间窗口聚合
在数据分析和报表系统中,经常需要按天、周、月聚合数据。以下是一个通用的时间窗口生成器。
python
# utils/time_window.py
from datetime import datetime, timedelta, date
from zoneinfo import ZoneInfo
from typing import List, Tuple
def get_date_range(start: date, end: date) -> List[date]:
"""获取两个日期之间的所有日期(含首尾)"""
days = []
current = start
while current <= end:
days.append(current)
current += timedelta(days=1)
return days
def get_week_range(dt: datetime) -> Tuple[datetime, datetime]:
"""获取指定日期所在周的起止时间(周一到周日)"""
tz = dt.tzinfo or ZoneInfo("Asia/Shanghai")
monday = dt - timedelta(days=dt.weekday())
week_start = monday.replace(hour=0, minute=0, second=0, microsecond=0)
week_end = week_start + timedelta(days=6, hours=23, minutes=59, seconds=59)
return week_start, week_end
def get_month_range(dt: datetime) -> Tuple[datetime, datetime]:
"""获取指定日期所在月的起止时间"""
tz = dt.tzinfo or ZoneInfo("Asia/Shanghai")
month_start = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# 下个月第一天
if dt.month == 12:
next_month = month_start.replace(year=dt.year + 1, month=1)
else:
next_month = month_start.replace(month=dt.month + 1)
month_end = next_month - timedelta(microseconds=1)
return month_start, month_end
def generate_daily_windows(
start: datetime, end: datetime
) -> List[Tuple[datetime, datetime]]:
"""生成按天切分的时间窗口列表"""
windows = []
current = start.replace(hour=0, minute=0, second=0, microsecond=0)
while current < end:
day_end = current + timedelta(hours=23, minutes=59, seconds=59, microseconds=999999)
windows.append((current, min(day_end, end)))
current += timedelta(days=1)
return windows
# 使用示例
if __name__ == "__main__":
tz = ZoneInfo("Asia/Shanghai")
now = datetime.now(tz)
week_start, week_end = get_week_range(now)
print(f"本周范围: {week_start} ~ {week_end}")
month_start, month_end = get_month_range(now)
print(f"本月范围: {month_start} ~ {month_end}")
# 生成最近7天的日窗口
seven_days_ago = now - timedelta(days=7)
windows = generate_daily_windows(seven_days_ago, now)
print(f"\n最近7天的日窗口数量: {len(windows)}")
for i, (s, e) in enumerate(windows, 1):
print(f" 第{i}天: {s.strftime('%Y-%m-%d')}")
八、竞品对比:四大时间处理库的优劣势

Python生态中,除了标准库的time和datetime,还有多个第三方时间处理库。以下是主流方案的横向对比:
| 对比维度 | 标准库datetime + zoneinfo | pytz | python-dateutil | pendulum |
|---|---|---|---|---|
| 是否内置 | 是(Python 3.9+) | 否,需pip安装 | 否,需pip安装 | 否,需pip安装 |
| 时区支持 | zoneinfo原生IANA时区 | 自带IANA时区数据 | 依赖系统或pytz | 自带时区数据 |
| DST处理 | 自动正确处理 | 需用localize,易出错 | 较好 | 自动正确处理 |
| API易用性 | 中等,需手动处理 | 较差,localize模式反直觉 | 好,parser强大 | 优秀,链式调用 |
| 不可变性 | datetime可变(replace返回新对象) | 同标准库 | 同标准库 | 完全不可变 |
| 人性化输出 | 需手动实现 | 需手动实现 | 需手动实现 | 内置humanize("3分钟前") |
| 日期解析 | fromisoformat仅支持ISO | 不支持模糊解析 | parser.parse支持几乎所有格式 | parse支持多种格式 |
| 相对日期 | 不支持(需手动算) | 不支持 | relativedelta支持"下个月" | 内置add/months等 |
| 性能 | 最好(C实现) | 好 | 中等 | 中等(纯Python封装) |
| 维护状态 | 活跃(CPython核心) | 维护模式,官方建议迁移 | 活跃 | 活跃 |
| 适用场景 | 所有新项目首选 | 老项目维护兼容 | 需要模糊解析和相对日期 | 需要人性化API和不可变性 |
8.1 各库的典型使用场景
标准库datetime + zoneinfo:企业级新项目的默认选择。零依赖、性能好、官方维护,配合项目内的工具类封装完全能满足95%的场景。
pytz:仅用于维护Python 3.9之前的老代码。如果你的项目还在用pytz,建议制定迁移计划逐步替换为zoneinfo。pytz官方文档明确写道:"Python 3.9+项目应使用标准库zoneinfo,pytz仅提供向后兼容"。
python-dateutil :当你需要解析格式未知的时间字符串(如用户输入、爬虫数据)时,dateutil.parser.parse()是最佳选择,它能智能识别几乎所有常见的日期格式。此外dateutil.relativedelta支持"下个月"、"第三个周五"等日历级别的相对日期计算,这是标准库timedelta做不到的。
python
# 安装: pip install python-dateutil
from dateutil import parser
from dateutil.relativedelta import relativedelta
from datetime import datetime
# 模糊解析各种格式
print(parser.parse("2024-03-15"))
print(parser.parse("15/03/2024"))
print(parser.parse("March 15, 2024 2:30 PM"))
print(parser.parse("20240315T143000"))
# 相对日期计算
now = datetime(2024, 3, 15)
print(f"下个月: {now + relativedelta(months=1)}")
print(f"下周五: {now + relativedelta(weekday=4)}") # 4=周五
print(f"三个月后的最后一天: {now + relativedelta(months=3, day=31)}")
pendulum :追求极致开发体验的团队可以选择。它提供了更人性化的API,如dt.add(days=3).subtract(hours=2)的链式调用、dt.diff_for_humans()输出"3分钟前"、完全不可变的对象设计等。但它继承自标准库datetime,某些边缘场景仍可能踩到标准库的坑,且引入了额外依赖。
python
# 安装: pip install pendulum
import pendulum
# 人性化API
now = pendulum.now("Asia/Shanghai")
print(f"3天后: {now.add(days=3)}")
print(f"2小时前: {now.subtract(hours=2)}")
print(f"人性化时差: {now.subtract(minutes=15).diff_for_humans()}")
# 输出: 15 minutes ago
# 周期迭代
period = pendulum.period(
pendulum.datetime(2024, 3, 1),
pendulum.datetime(2024, 3, 15)
)
for dt in period.range("days"):
print(dt.to_date_string())
8.2 选型建议
- 新项目 :标准库
datetime+zoneinfo,封装项目工具类。这是最稳妥、最易维护的方案。 - 老项目维护 :继续用
pytz,但新代码逐步迁移到zoneinfo。 - 需要模糊解析 :额外引入
python-dateutil,仅用其parser和relativedelta。 - 个人项目/快速原型 :可以尝试
pendulum,开发效率更高。 - 永远不要:在同一个项目中混用多个时间库(如pytz和zoneinfo混用),这会制造无穷无尽的bug。
九、常用场景速查手册
9.1 获取当前时间的各种形式
python
import time
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
# 时间戳
print(time.time()) # 1710484200.123
print(int(time.time())) # 1710484200(秒级整数)
print(int(time.time() * 1000)) # 1710484200123(毫秒级)
# datetime
print(datetime.now()) # 本地naive(不推荐)
print(datetime.now(timezone.utc)) # UTC aware(推荐)
print(datetime.now(ZoneInfo("Asia/Shanghai"))) # 北京时间aware
# 字符串
print(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"))
print(datetime.now(timezone.utc).isoformat()) # ISO 8601
9.2 时间格式转换速查
python
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
import time
ts = time.time()
# 时间戳 -> datetime(UTC)
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
# datetime -> 时间戳
ts2 = dt.timestamp()
# datetime -> 字符串
s = dt.strftime("%Y-%m-%d %H:%M:%S")
# 字符串 -> datetime
dt2 = datetime.strptime(s, "%Y-%m-%d %H:%M:%S")
# datetime(UTC) -> datetime(北京时间)
dt_bj = dt.astimezone(ZoneInfo("Asia/Shanghai"))
# datetime(北京时间) -> datetime(UTC)
dt_utc = dt_bj.astimezone(timezone.utc)
# ISO字符串 <-> datetime
iso_str = dt.isoformat()
dt3 = datetime.fromisoformat(iso_str)
9.3 常见业务场景代码片段
python
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
tz = ZoneInfo("Asia/Shanghai")
now = datetime.now(tz)
# 今天零点
today_start = now.replace(hour=0, minute=0, second=0, microsecond=0)
# 今天23:59:59
today_end = now.replace(hour=23, minute=59, second=59, microsecond=999999)
# 昨天
yesterday = now - timedelta(days=1)
# 本周一零点
monday = (now - timedelta(days=now.weekday())).replace(hour=0, minute=0, second=0, microsecond=0)
# 本月1号零点
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
# 本月最后一天
if now.month == 12:
next_month = now.replace(year=now.year + 1, month=1, day=1)
else:
next_month = now.replace(month=now.month + 1, day=1)
month_end = next_month - timedelta(microseconds=1)
# 两个日期相差几天
d1 = datetime(2024, 3, 1, tzinfo=tz)
d2 = datetime(2024, 3, 15, tzinfo=tz)
days_diff = (d2 - d1).days
# 判断是否为闰年
year = 2024
is_leap = (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0)
print(f"{year}年是否闰年: {is_leap}")
# 计算年龄
birth = datetime(1995, 6, 15)
today = datetime.now()
age = today.year - birth.year - ((today.month, today.day) < (birth.month, birth.day))
print(f"年龄: {age}")
十、面试官高频面试题

面试题1:time.time()和datetime.now()有什么区别?
参考答案:
time.time()返回的是从Unix纪元(1970-01-01 00:00:00 UTC)到当前时刻的浮点秒数,是一个时间戳,适合做精确的时间间隔测量和存储。
datetime.now()返回的是一个datetime对象,包含年、月、日、时、分、秒、微秒等字段,适合做日期运算、格式化输出和人类可读的时间展示。不传参数时返回naive datetime(依赖系统时区),传入tz参数可返回aware datetime。
在企业项目中,存储和计算用时间戳或UTC aware datetime,展示用格式化后的字符串。
面试题2:什么是naive datetime和aware datetime?为什么不能混用?
参考答案:
naive datetime是指tzinfo属性为None的datetime对象,它不包含时区信息,无法确定它代表的是哪个时区的时间。aware datetime则携带了明确的时区信息(tzinfo不为None)。
两者不能混用的原因是:对naive和aware datetime进行比较或运算时,Python会抛出TypeError: can't compare offset-naive and offset-aware datetimes。即使不报错,混用也会导致逻辑错误------你以为在比较同一个时区的时间,实际上一个是北京时间、一个是UTC时间,差了8小时。
企业级规范:整个项目统一使用aware datetime,默认时区为UTC,禁止naive datetime进入业务逻辑层。
面试题3:如何处理跨时区问题?pytz和zoneinfo怎么选?
参考答案:
处理跨时区的核心原则是:内部统一用UTC存储和计算,仅在展示层转换为用户本地时区。
具体做法:
- 数据库存储UTC时间戳或UTC aware datetime
- 业务逻辑中所有datetime对象都带时区信息
- API返回ISO 8601格式带时区偏移量的字符串
- 前端根据用户所在时区做最终展示
pytz和zoneinfo的选择:
- Python 3.9+项目:用标准库
zoneinfo,零依赖、API更优雅(可直接传tzinfo,不需要localize) - Python 3.8及以下或老项目维护:用
pytz,但必须用localize()方法而不是直接传tzinfo参数 - 新项目不要用pytz,pytz官方已建议迁移到zoneinfo
面试题4:strftime和strptime的区别是什么?%m和%M有什么不同?
参考答案:
strftime中的f代表format(格式化),作用是将datetime对象转换为字符串(时间对象 -> 字符串)。
strptime中的p代表parse(解析),作用是将字符串转换为datetime对象(字符串 -> 时间对象)。
记忆口诀:f = format(输出格式化),p = parse(输入解析),方向相反。
%m是月份(month),取值01-12;%M是分钟(minute),取值00-59。小写m对应月份,大写M对应分钟。这是初学者最容易写错的格式化符,面试中经常考察。
其他易混淆的格式化符:
%y两位年份(24)vs%Y四位年份(2024)%H24小时制(14)vs%I12小时制(02)%d日期(15)vs%j一年中第几天(075)
面试题5:timedelta可以表示一个月吗?为什么?
参考答案:
timedelta不能准确表示一个月。因为timedelta内部只存储days、seconds、microseconds三个字段,它表示的是一个绝对的时间长度(固定的秒数),而"一个月"的天数是不固定的(28/29/30/31天)。
timedelta(days=30)只是近似一个月,在长期计算中会累积误差。例如从1月31日加上timedelta(days=30)会得到3月2日(因为2月只有28或29天),而不是2月28日。
需要按月计算时,应该:
- 手动处理月份进位(
dt.replace(month=dt.month+1),注意12月进位到次年1月) - 使用
dateutil.relativedelta(months=1),它能正确处理日历级别的月份运算
面试题6:datetime.utcnow()为什么不推荐使用?
参考答案:
datetime.utcnow()返回的是naive datetime(tzinfo=None),虽然它的值确实是UTC时间,但因为没有时区标记,后续代码无法区分它是UTC还是本地时间。当它和其他aware datetime比较时会报错,和其他naive datetime(可能是本地时间)比较时会产生静默错误。
正确做法是使用datetime.now(timezone.utc),它返回的是aware datetime,携带明确的UTC时区信息,后续的比较、转换、存储都是安全的。
Python官方文档也已将utcnow()标记为"不推荐使用"(deprecated since version 3.12),建议统一使用datetime.now(timezone.utc)。
面试题7:如何测量代码执行时间?为什么不用time.time()?
参考答案:
测量代码执行时间应该使用time.perf_counter(),而不是time.time()。
原因:
time.time()返回的是系统时钟(wall-clock time),如果在测量期间系统时间被NTP同步、人为修改或夏令时调整,测量结果就会出错甚至出现负数time.perf_counter()返回的是单调时钟(monotonic clock),它只会往前走,不会回拨,精度更高,专门用于测量短时间间隔time.monotonic()也是单调时钟,但perf_counter通常精度更高
python
import time
start = time.perf_counter()
# 被测代码
result = sum(range(1_000_000))
end = time.perf_counter()
print(f"执行耗时: {end - start:.6f}秒")
面试题8:什么是Unix时间戳?2038年问题是什么?
参考答案:
Unix时间戳是从1970年1月1日00:00:00 UTC到当前时刻经过的秒数,是计算机系统中最常用的时间表示方式。
2038年问题(Year 2038 Problem)是指:在32位系统中,时间戳用有符号32位整数存储,最大值为2^31 - 1 = 2147483647,对应的时间是2038年1月19日03:14:07 UTC。超过这个时刻后,32位整数会溢出变成负数,导致时间计算错误。
不过Python的time.time()返回的是浮点数,不受32位整数限制,Python本身不存在2038年问题。但在与C语言交互、使用32位系统、或某些数据库(如MySQL的TIMESTAMP类型)时仍需注意。现代系统普遍使用64位时间戳,2038年问题已基本解决。
面试题9:如何将"3分钟前"这种人性化时间字符串转换为datetime?
参考答案:
标准库不直接支持人性化时间的解析,需要使用第三方库:
python-dateutil的parser.parse可以解析部分相对时间,但对"3分钟前"这种中文支持有限pendulum的parse方法支持更多人性化格式- 更通用的方案是使用正则表达式提取数字和单位,然后用
timedelta计算
python
import re
from datetime import datetime, timedelta, timezone
def parse_human_time(s: str, reference: datetime = None) -> datetime:
"""解析'3分钟前'、'2小时后'等中文人性化时间"""
reference = reference or datetime.now(timezone.utc)
patterns = {
r'(\d+)\s*秒前': lambda n: reference - timedelta(seconds=n),
r'(\d+)\s*分钟前': lambda n: reference - timedelta(minutes=n),
r'(\d+)\s*小时前': lambda n: reference - timedelta(hours=n),
r'(\d+)\s*天前': lambda n: reference - timedelta(days=n),
r'(\d+)\s*周前': lambda n: reference - timedelta(weeks=n),
r'(\d+)\s*秒后': lambda n: reference + timedelta(seconds=n),
r'(\d+)\s*分钟后': lambda n: reference + timedelta(minutes=n),
r'(\d+)\s*小时后': lambda n: reference + timedelta(hours=n),
r'(\d+)\s*天后': lambda n: reference + timedelta(days=n),
}
for pattern, func in patterns.items():
match = re.match(pattern, s.strip())
if match:
return func(int(match.group(1)))
raise ValueError(f"无法解析的时间格式: {s}")
print(parse_human_time("3分钟前"))
print(parse_human_time("2小时后"))
面试题10:数据库中时间字段应该用什么类型?datetime还是timestamp?
参考答案:
这取决于具体的数据库和业务需求:
MySQL:
DATETIME:范围'1000-01-01'到'9999-12-31',占8字节,不存储时区信息,按原样存储和检索TIMESTAMP:范围'1970-01-01'到'2038-01-19',占4字节,存储时转为UTC,检索时转为会话时区,存在2038年问题- 企业项目推荐用
DATETIME存储UTC时间,范围大且不受时区转换影响
PostgreSQL:
TIMESTAMP WITHOUT TIME ZONE:不存储时区,类似MySQL的DATETIMETIMESTAMP WITH TIME ZONE(TIMESTAMPTZ):内部存储UTC,输入输出时自动转换时区- 推荐用
TIMESTAMPTZ,PostgreSQL会自动处理时区转换
通用原则:
- 数据库内部统一存储UTC时间
- 应用层负责时区转换,不要依赖数据库的时区设置
- 如果需要精确到毫秒/微秒,选择支持小数秒的类型
- 避免使用MySQL的
TIMESTAMP类型(2038年问题)
十一、避坑指南:十个最常见的时间处理错误
- 用
datetime.now()代替datetime.now(timezone.utc):naive datetime是万恶之源 - 用
datetime.utcnow():返回naive datetime,已被官方标记为不推荐 - 混用naive和aware datetime:要么全用aware,要么全用naive,企业项目必须全用aware
timedelta(days=30)当作一个月:每月天数不同,会累积误差strptime中写错%m和%M:小写是月,大写是分pytz直接传tzinfo参数 :必须用localize(),否则DST处理错误- 数据库存本地时间:应该统一存UTC,展示时再转
- 用
time.time()测量性能 :应该用time.perf_counter() - 不同模块时间戳精度不统一:项目启动时约定秒级还是毫秒级
- 忽略闰秒和DST:在实行夏令时的地区部署时必须考虑
十二、总结
Python的time和datetime模块是每个开发者必须掌握的基础工具。从time模块的底层时间戳和struct_time,到datetime模块的面向对象API,再到Python 3.9引入的zoneinfo原生时区支持,Python的时间处理能力已经非常完善。
记住核心原则:UTC存储、aware计算、ISO交换、本地展示。只要严格遵守这四条黄金法则,99%的时间相关bug都可以避免。
在企业项目中,建议封装统一的时间工具类,所有模块通过工具类获取和处理时间,禁止直接调用datetime.now()等裸函数。这样既能保证一致性,也方便未来统一调整时区策略。
希望本文能帮助你彻底搞懂Python时间处理,在面试中从容应答,在工作中少踩坑。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。