日期时间 datetime 模块

处理日期时间是程序员最经常”看起来简单实际处处是坑”的领域——时区、闰秒、夏令时、格式化、跨月加减……任何一个疏忽都能引发生产事故。Python 的 datetime 模块把这些抽象成了几个类:datetimedatetimetimedeltatzinfo。这一篇讲清楚怎么”对”地用它们,以及现代 Python 的 zoneinfo

一、四个核心类

  • date:日期(年月日)
  • time:时间(时分秒微秒)
  • datetime:日期时间(date + time)
  • timedelta:时长(差值)
1
2
3
4
5
6
from datetime import date, time, datetime, timedelta

d = date(2025, 9, 25)
t = time(14, 30, 0)
dt = datetime(2025, 9, 25, 14, 30, 0)
delta = timedelta(days=7, hours=3)

二、获取当前时间

1
2
3
4
5
6
from datetime import date, datetime

date.today() # 2025-09-25
datetime.now() # 2025-09-25 14:30:15.123456
datetime.utcnow() # UTC 时间(不带时区,不推荐用)
datetime.now(tz=...) # 带时区的(推荐)

优先用 datetime.now(tz=...),不带时区的对象在跨时区场景会出问题。

三、时间戳

1
2
3
4
5
6
# datetime → 时间戳
datetime.now().timestamp() # 1758787815.123456 秒

# 时间戳 → datetime
datetime.fromtimestamp(1758787815)
datetime.fromtimestamp(1758787815, tz=timezone.utc)

四、格式化与解析

strftime = string format time(datetime → 字符串);strptime = string parse time(字符串 → datetime)。

1
2
3
4
5
6
7
8
dt = datetime.now()

# 格式化
dt.strftime("%Y-%m-%d %H:%M:%S")
# '2025-09-25 14:30:15'

# 解析
datetime.strptime("2025-09-25 14:30", "%Y-%m-%d %H:%M")

常用格式符

符号 含义
%Y 4 位年 2025
%m 月(补零) 09
%d 日(补零) 25
%H 24 小时(补零) 14
%M 30
%S 15
%f 微秒 123456
%A 星期全称 Thursday
%a 星期缩写 Thu
%B 月全称 September
%b 月缩写 Sep
%Z 时区名 UTC
%z 时区偏移 +0800

ISO 8601(推荐格式)

1
2
3
4
5
6
7
8
# 输出
dt.isoformat() # '2025-09-25T14:30:15.123456'

# 解析
datetime.fromisoformat("2025-09-25T14:30:15")

# date 也支持
date.fromisoformat("2025-09-25")

存到数据库、发到 API 时永远用 ISO 8601 格式——无歧义、易解析、跨语言。

五、timedelta:时间差

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

delta = timedelta(days=7, hours=3, minutes=30)
print(delta) # 7 days, 3:30:00
print(delta.total_seconds()) # 613800.0

# 加减
now = datetime.now()
tomorrow = now + timedelta(days=1)
last_week = now - timedelta(weeks=1)

# 两个 datetime 相减 → timedelta
diff = datetime.now() - datetime(2025, 1, 1)
print(diff.days)

注意:timedelta 支持 days/seconds/microseconds/milliseconds/minutes/hours/weeks,但不支持 months 和 years(因为月长度不定,年可能闰)。要加月/年用 dateutil.relativedelta 或手动。

1
2
3
from dateutil.relativedelta import relativedelta
next_month = datetime.now() + relativedelta(months=1)
next_year = datetime.now() + relativedelta(years=1)

六、时区处理(重头戏)

Python 3.9+ 内置 zoneinfo(IANA 时区数据库):

1
2
3
4
5
6
7
8
9
10
11
12
13
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 建立带时区的时间
utc_now = datetime.now(tz=timezone.utc)
beijing = datetime.now(tz=ZoneInfo("Asia/Shanghai"))
tokyo = datetime.now(tz=ZoneInfo("Asia/Tokyo"))

print(utc_now.isoformat()) # 2025-09-25T06:30:00+00:00
print(beijing.isoformat()) # 2025-09-25T14:30:00+08:00

# 时区之间转换
utc_now.astimezone(ZoneInfo("America/New_York"))

准则

  • 存储用 UTC(时间戳或 ISO 8601 with +00:00);
  • 展示时转当地时区
  • 拒绝 naive datetime(不带时区的 datetime)参与跨时区比较。

Python 3.8 及更早需要用第三方 pytzpython-dateutil

七、naive vs aware

  • naive datetime:不带时区信息(tzinfo 是 None);
  • aware datetime:带时区信息。
1
2
3
4
naive = datetime.now()
aware = datetime.now(tz=timezone.utc)

naive < aware # ❌ TypeError: 不能比较

规则别混用。要么全 naive(本地时间,只在单机小工具用),要么全 aware(生产系统)。

八、time 模块(相对底层)

除了 datetime,还有 time 模块——更”贴近系统”的接口:

1
2
3
4
5
6
7
8
9
import time

time.time() # 当前 UNIX 时间戳(秒,float)
time.time_ns() # 纳秒

time.sleep(2) # 阻塞 2 秒
time.monotonic() # 单调时钟,测耗时用
time.perf_counter() # 高精度性能计时器(推荐用来测代码耗时)
time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())

测代码耗时

1
2
3
4
5
6
import time

start = time.perf_counter()
do_stuff()
elapsed = time.perf_counter() - start
print(f"用了 {elapsed:.4f}s")

九、UNIX 时间戳与 datetime 互转

1
2
3
4
5
6
7
8
# 时间戳 → naive datetime(本地时区)
datetime.fromtimestamp(1758787815)

# 时间戳 → aware datetime(UTC)
datetime.fromtimestamp(1758787815, tz=timezone.utc)

# datetime → 时间戳
dt.timestamp() # naive 会被当作本地时间;aware 会用其时区

十、日期算术:谨慎处理月和年

1
2
3
4
5
6
7
8
9
10
11
12
13
# 加一天:安全
d + timedelta(days=1)

# 加一月:不安全!月长度不定
# 用 relativedelta
from dateutil.relativedelta import relativedelta
d + relativedelta(months=1)

# 计算月末
from calendar import monthrange
year, month = 2025, 9
_, last_day = monthrange(year, month) # (0, 30)
month_end = date(year, month, last_day)

跨月边缘的 bug 特别多——比如 1 月 31 日加一个月,应该是 2 月 28 还是 3 月 3?不同库处理不同,一定要读文档。

十一、常见操作汇总

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 今天开始/结束
today_start = datetime.combine(date.today(), time.min) # 00:00:00
today_end = datetime.combine(date.today(), time.max) # 23:59:59.999999

# 本周一
today = date.today()
monday = today - timedelta(days=today.weekday()) # 0=周一, 6=周日

# 本月 1 号
first_of_month = today.replace(day=1)

# 两个日期之间的天数
(date(2025, 12, 31) - date.today()).days

# 遍历日期区间
def date_range(start, end):
d = start
while d < end:
yield d
d += timedelta(days=1)

for d in date_range(date(2025, 9, 1), date(2025, 9, 5)):
print(d)

十二、格式化与国际化

strftime("%A") 输出星期名,取决于 locale:

1
2
3
4
5
6
import locale
locale.setlocale(locale.LC_TIME, "zh_CN")
datetime.now().strftime("%A") # '星期四'

locale.setlocale(locale.LC_TIME, "en_US")
datetime.now().strftime("%A") # 'Thursday'

f-string 内部也能用格式说明符:

1
f"{datetime.now():%Y-%m-%d %H:%M:%S}"

十三、第三方库

  • python-dateutil:更强解析、relativedelta
  • arrow:更”人性化”的 API;
  • pendulum:类似 arrow,更完整;
  • whenever(新兴):类型安全的时区处理。

日常业务用内置 datetime + zoneinfo 就够。特殊需求再上第三方。

十四、一个完整例子:会议时区转换

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
# meeting.py
"""把一个会议时间显示给多个时区的参与者。"""

from datetime import datetime
from zoneinfo import ZoneInfo


def format_for(dt_utc: datetime, tz_name: str) -> str:
tz = ZoneInfo(tz_name)
local = dt_utc.astimezone(tz)
return local.strftime(f"%Y-%m-%d %H:%M ({tz_name})")


def schedule_meeting():
# 组织者在北京,定 2025-09-30 14:00 北京时间
beijing_time = datetime(2025, 9, 30, 14, 0, tzinfo=ZoneInfo("Asia/Shanghai"))

# 转 UTC 存储
utc_time = beijing_time.astimezone(ZoneInfo("UTC"))
print("存储用:", utc_time.isoformat())

# 展示给不同参与者
zones = [
"Asia/Shanghai",
"Asia/Tokyo",
"Europe/London",
"America/New_York",
"America/Los_Angeles",
]
for z in zones:
print(format_for(utc_time, z))


if __name__ == "__main__":
schedule_meeting()

十五、常见陷阱

  1. naive 与 aware 混用:TypeError。生产系统统一 aware。
  2. datetime.utcnow() 返回 naive:Python 3.12 已弃用它,用 datetime.now(tz=timezone.utc)
  3. .timestamp() 在 naive 上按本地时区解释:跨时区时结果不对。
  4. 加一个月用 days=30:不同月长度不同,用 relativedelta
  5. strptime 格式不匹配ValueError。加 try 或校验。
  6. %f 精度:Python 只到微秒(6 位),没有纳秒 datetime。要更精细用 time.time_ns
  7. 夏令时(DST)ZoneInfo 会自动处理,但边界时(”2 点重复”或”跳过 2 点”)依然有陷阱。

十六、小结与延伸阅读

  • 四大类:date / time / datetime / timedelta;
  • 存 UTC,展示时转当地时区;
  • zoneinfo(3.9+)或 pytz(3.8-);
  • ISO 8601 格式是万能通用格式;
  • 加减用 timedelta,跨月用 relativedelta;
  • 测代码性能用 time.perf_counter
  • 别混用 naive 和 aware;
  • datetime.utcnow() 已弃用。

延伸阅读:

下一篇 JSON 数据处理 我们讲怎么优雅地处理 JSON 数据。