Python 中的日期和时间

日期和时间处理是编程中常见的需求——日志记录、数据统计、任务调度、业务逻辑计算,几乎每个项目都离不开它。Python 提供了多个内置模块来处理日期和时间,其中 datetime 是最核心、最常用的模块。

datetime 模块核心类

datetime 模块包含以下几个核心类:

说明 示例
date 日期(年、月、日) date(2026, 5, 21)
time 时间(时、分、秒、微秒) time(14, 30, 0)
datetime 日期+时间 datetime(2026, 5, 21, 14, 30)
timedelta 时间差 timedelta(days=7)
timezone 时区 timezone(timedelta(hours=8))

创建日期和时间对象

from datetime import date, time, datetime, timedelta

# 创建日期
d = date(2026, 5, 21)
print(d)           # 2026-05-21
print(d.year)      # 2026
print(d.month)     # 5
print(d.day)       # 21

# 创建时间
t = time(14, 30, 15, 500000)
print(t)           # 14:30:15.500000
print(t.hour)      # 14
print(t.minute)    # 30
print(t.second)    # 15

# 创建日期时间
dt = datetime(2026, 5, 21, 14, 30, 0)
print(dt)          # 2026-05-21 14:30:00

获取当前时间

from datetime import date, datetime

# 当前日期
today = date.today()
print(today)  # 2026-05-21

# 当前日期时间
now = datetime.now()
print(now)    # 2026-05-21 14:30:00.123456

# 当前 UTC 时间
utc_now = datetime.utcnow()
print(utc_now)  # 2026-05-21 06:30:00.123456(假设 UTC+8)

日期格式化:strftime 和 strptime

strftime — 日期转字符串

strftime 将日期时间对象格式化为字符串:

from datetime import datetime

now = datetime.now()

# 常用格式化
print(now.strftime("%Y-%m-%d"))           # 2026-05-21
print(now.strftime("%Y年%m月%d日"))        # 2026年05月21日
print(now.strftime("%H:%M:%S"))           # 14:30:00
print(now.strftime("%Y-%m-%d %H:%M:%S"))  # 2026-05-21 14:30:00
print(now.strftime("%A"))                 # Thursday
print(now.strftime("%a"))                 # Thu
print(now.strftime("%B"))                 # May
print(now.strftime("%j"))                 # 141(年中第几天)

strptime — 字符串转日期

strptime 将字符串解析为日期时间对象:

from datetime import datetime

# 解析常见格式
dt1 = datetime.strptime("2026-05-21", "%Y-%m-%d")
print(dt1)  # 2026-05-21 00:00:00

dt2 = datetime.strptime("2026/05/21 14:30:00", "%Y/%m/%d %H:%M:%S")
print(dt2)  # 2026-05-21 14:30:00

dt3 = datetime.strptime("2026年05月21日", "%Y年%m月%d日")
print(dt3)  # 2026-05-21 00:00:00

格式化符号速查表

符号 说明 示例
%Y 4位年份 2026
%y 2位年份 26
%m 月份(01-12) 05
%d 日期(01-31) 21
%H 小时(00-23) 14
%I 小时(01-12) 02
%M 分钟(00-59) 30
%S 秒(00-59) 00
%f 微秒(6位) 123456
%A 星期全名 Thursday
%a 星期缩写 Thu
%B 月份全名 May
%b 月份缩写 May
%j 年中第几天 141
%w 星期几(0=周日) 4
%Z 时区名 CST

timedelta:日期时间算术

timedelta 表示两个日期时间之间的差值,可以用于日期运算:

from datetime import datetime, timedelta, date

now = datetime.now()

# 加减天数
yesterday = now - timedelta(days=1)
tomorrow = now + timedelta(days=1)
print(f"昨天: {yesterday}")
print(f"明天: {tomorrow}")

# 加减小时/分钟/秒
one_hour_later = now + timedelta(hours=1)
half_hour_ago = now - timedelta(minutes=30)
print(f"一小时后: {one_hour_later}")
print(f"半小时前: {half_hour_ago}")

# 组合使用
next_week = now + timedelta(weeks=1, hours=2)
print(f"下周同一时间(加2小时): {next_week}")

# 计算时间差
start = datetime(2026, 1, 1)
end = datetime(2026, 5, 21)
delta = end - start
print(f"相差天数: {delta.days}")           # 相差天数
print(f"相差秒数: {delta.total_seconds()}")  # 总秒数

timedelta 的实用计算

from datetime import date

def days_until_birthday(birthday: date) -> int:
    """计算距离下一次生日还有多少天"""
    today = date.today()
    # 今年的生日
    next_birthday = date(today.year, birthday.month, birthday.day)
    # 如果今年生日已过,计算到明年
    if next_birthday < today:
        next_birthday = date(today.year + 1, birthday.month, birthday.day)
    return (next_birthday - today).days

# 计算距离下一次生日
birthday = date(1990, 10, 1)
print(f"距离生日还有 {days_until_birthday(birthday)} 天")

时区处理

时区感知 vs 时区 naive

datetime 对象分为两类:

  • naive(朴素):不含时区信息
  • aware(感知):包含时区信息
from datetime import datetime, timezone, timedelta

# naive datetime
naive = datetime.now()
print(naive.tzinfo)  # None

# aware datetime(UTC)
utc_dt = datetime.now(timezone.utc)
print(utc_dt.tzinfo)  # UTC

# 创建带时区的 datetime
tz_shanghai = timezone(timedelta(hours=8))
shanghai_dt = datetime(2026, 5, 21, 14, 30, tzinfo=tz_shanghai)
print(shanghai_dt)  # 2026-05-21 14:30:00+08:00

使用 zoneinfo(Python 3.9+)

Python 3.9 引入了 zoneinfo 模块,使用 IANA 时区数据库:

from datetime import datetime
from zoneinfo import ZoneInfo

# 获取不同时区的时间
now_utc = datetime.now(ZoneInfo("UTC"))
now_shanghai = datetime.now(ZoneInfo("Asia/Shanghai"))
now_tokyo = datetime.now(ZoneInfo("Asia/Tokyo"))
now_newyork = datetime.now(ZoneInfo("America/New_York"))
now_london = datetime.now(ZoneInfo("Europe/London"))

print(f"UTC:       {now_utc}")
print(f"上海:      {now_shanghai}")
print(f"东京:      {now_tokyo}")
print(f"纽约:      {now_newyork}")
print(f"伦敦:      {now_london}")

# 时区转换
shanghai_time = datetime(2026, 5, 21, 14, 30, tzinfo=ZoneInfo("Asia/Shanghai"))
# 转换为纽约时间
ny_time = shanghai_time.astimezone(ZoneInfo("America/New_York"))
print(f"上海 14:30 = 纽约 {ny_time.strftime('%H:%M')}")

常见时区名称(IANA)

Asia/Shanghai      中国标准时间 (UTC+8)
Asia/Tokyo         日本标准时间 (UTC+9)
Asia/Singapore     新加坡时间 (UTC+8)
Asia/Kolkata       印度标准时间 (UTC+5:30)
Europe/London      格林威治时间 (UTC+0/UTC+1)
Europe/Berlin      中欧时间 (UTC+1/UTC+2)
America/New_York   美东时间 (UTC-5/UTC-4)
America/Chicago   美中时间 (UTC-6/UTC-5)
America/Los_Angeles 美西时间 (UTC-8/UTC-7)
Australia/Sydney   澳大利亚东部时间 (UTC+10/UTC+11)
UTC                协调世界时

pytz 兼容方案

对于 Python 3.8 及更早版本,可以使用 pytz

# pip install pytz
import pytz
from datetime import datetime

tz_shanghai = pytz.timezone("Asia/Shanghai")
shanghai_now = datetime.now(tz_shanghai)
print(shanghai_now)

# 时区转换
utc_time = datetime.now(pytz.UTC)
shanghai_time = utc_time.astimezone(pytz.timezone("Asia/Shanghai"))
print(f"UTC: {utc_time}")
print(f"上海: {shanghai_time}")

使用 dateutil 进行高级解析

dateutil 库可以解析自然语言格式的日期(需单独安装):

pip install python-dateutil
from dateutil import parser
from dateutil.relativedelta import relativedelta

# 自动解析各种格式
print(parser.parse("2026-05-21"))
print(parser.parse("05/21/2026"))
print(parser.parse("May 21, 2026"))
print(parser.parse("21 May 2026 14:30:00"))
print(parser.parse("2026-05-21T14:30:00Z"))
print(parser.parse("明天", fuzzy=True))  # 部分支持中文

# relativedelta: 比 timedelta 更强大
from datetime import date

today = date.today()
# 计算下个月的同一天
next_month = today + relativedelta(months=1)
print(f"下个月: {next_month}")

# 计算下周五
next_friday = today + relativedelta(weekday=4)  # 4 = Friday
print(f"下周五: {next_friday}")

# 计算两个日期之间的年月日差
start = date(2020, 1, 1)
end = date(2026, 5, 21)
delta = relativedelta(end, start)
print(f"相差 {delta.years}{delta.months}{delta.days} 天")

时间戳与 time 模块

datetime 与时间戳互转

from datetime import datetime
import time

# 当前时间戳
ts = time.time()
print(f"当前时间戳: {ts}")

# 时间戳转 datetime
dt = datetime.fromtimestamp(ts)
print(f"时间戳转日期: {dt}")

# datetime 转时间戳
ts_back = dt.timestamp()
print(f"日期转时间戳: {ts_back}")

# UTC 时间戳转 datetime
utc_dt = datetime.utcfromtimestamp(ts)
print(f"UTC 时间: {utc_dt}")

time 模块常用函数

import time

# sleep:暂停执行
print("开始等待...")
time.sleep(2)  # 暂停2秒
print("继续执行")

# 性能计时
start = time.perf_counter()
# 执行一些操作
sum(range(1_000_000))
end = time.perf_counter()
print(f"执行耗时: {end - start:.6f}秒")

# 格式化当前时间
print(time.strftime("%Y-%m-%d %H:%M:%S"))

# 解析时间字符串
struct_time = time.strptime("2026-05-21", "%Y-%m-%d")
print(struct_time.tm_year)   # 2026
print(struct_time.tm_mon)    # 5
print(struct_time.tm_yday)   # 141

calendar 模块

import calendar

# 打印月历
cal = calendar.month(2026, 5)
print(cal)

# 判断闰年
print(calendar.isleap(2024))   # True
print(calendar.isleap(2025))   # False

# 某个月的天数
print(calendar.monthrange(2026, 5))  # (4, 31) => 周4开始,共31天

# 某天是星期几
print(calendar.weekday(2026, 5, 21))  # 3 (0=周一, 3=周四)

实战案例

1. 任务调度计算

from datetime import datetime, timedelta
from typing import Optional

def calculate_next_run(
    current_time: Optional[datetime] = None,
    interval_hours: int = 6,
    work_hours: tuple = (9, 18)
) -> datetime:
    """
    计算下一次任务执行时间。
    只在工作时间(9:00-18:00)内执行,非工作时间顺延到次日9点。
    """
    if current_time is None:
        current_time = datetime.now()

    next_time = current_time + timedelta(hours=interval_hours)

    # 如果不在工作时间内,调整到下一个工作日的 9:00
    work_start = next_time.replace(hour=9, minute=0, second=0, microsecond=0)
    work_end = next_time.replace(hour=18, minute=0, second=0, microsecond=0)

    if next_time > work_end:
        # 顺延到下一个工作日
        next_time = work_start + timedelta(days=1)
    elif next_time < work_start:
        next_time = work_start

    return next_time

now = datetime(2026, 5, 21, 15, 0, 0)
print(f"当前: {now}")
print(f"下次执行: {calculate_next_run(now)}")

2. 年龄计算

from datetime import date

def calculate_age(birth_date: date) -> int:
    """计算实足年龄"""
    today = date.today()
    age = today.year - birth_date.year
    # 如果今年生日还没过,减一岁
    if (today.month, today.day) < (birth_date.month, birth_date.day):
        age -= 1
    return age

# 测试
birth = date(1990, 10, 1)
print(f"出生日期: {birth}")
print(f"当前年龄: {calculate_age(birth)} 岁")

3. 工作日计算

from datetime import date, timedelta

def add_business_days(start: date, days: int) -> date:
    """给指定日期加上 N 个工作日"""
    current = start
    added = 0
    while added < days:
        current += timedelta(days=1)
        if current.weekday() < 5:  # 周一到周五
            added += 1
    return current

def count_business_days(start: date, end: date) -> int:
    """计算两个日期之间的工作日数量"""
    count = 0
    current = start
    while current <= end:
        if current.weekday() < 5:
            count += 1
        current += timedelta(days=1)
    return count

today = date(2026, 5, 21)  # 周四
print(f"{today} 起10个工作日: {add_business_days(today, 10)}")
print(f"5月工作日数: {count_business_days(date(2026, 5, 1), date(2026, 5, 31))}")

4. 时区转换工具

from datetime import datetime
from zoneinfo import ZoneInfo
from typing import Optional

def convert_timezone(
    dt_str: str,
    from_tz: str = "Asia/Shanghai",
    to_tz: str = "America/New_York",
    fmt: str = "%Y-%m-%d %H:%M:%S"
) -> Optional[str]:
    """时区转换工具"""
    try:
        # 解析输入时间
        naive_dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M")
        # 标记源时区
        source_dt = naive_dt.replace(tzinfo=ZoneInfo(from_tz))
        # 转换为目标时区
        target_dt = source_dt.astimezone(ZoneInfo(to_tz))
        return target_dt.strftime(fmt)
    except Exception as e:
        return f"转换失败: {e}"

print(convert_timezone("2026-05-21 14:30", "Asia/Shanghai", "America/New_York"))
# 2026-05-21 02:30 (上海14:30 = 纽约02:30)

print(convert_timezone("2026-05-21 14:30", "Asia/Shanghai", "Europe/London"))
# 2026-05-21 07:30

5. 日志时间解析

from datetime import datetime
from zoneinfo import ZoneInfo
import re

def parse_log_timestamp(log_line: str) -> datetime:
    """解析日志中的时间戳并转换为上海时间"""
    # 匹配常见日志格式: [21/May/2026:10:30:25 +0000]
    pattern = r"\[(\d{2}/\w{3}/\d{4}:\d{2}:\d{2}:\d{2})\s([+-]\d{4})\]"
    match = re.search(pattern, log_line)
    if not match:
        raise ValueError("无法解析日志时间戳")

    dt_str, tz_str = match.groups()
    # 解析时间
    dt = datetime.strptime(dt_str, "%d/%b/%Y:%H:%M:%S")

    # 解析时区偏移
    tz_hours = int(tz_str[:3])
    tz_minutes = int(tz_str[0] + tz_str[3:])
    tz_offset = __import__("datetime").timedelta(hours=tz_hours, minutes=tz_minutes)

    from zoneinfo import ZoneInfo
    # 标记时区
    dt_with_tz = dt.replace(tzinfo=__import__("datetime").timezone(tz_offset))
    # 转换为上海时间
    shanghai_tz = ZoneInfo("Asia/Shanghai")
    return dt_with_tz.astimezone(shanghai_tz)

log = '192.168.1.1 - - [21/May/2026:10:30:25 +0000] "GET /api HTTP/1.1" 200'
parsed = parse_log_timestamp(log)
print(f"原始日志时间 (UTC): 10:30:25")
print(f"转换后 (上海时间): {parsed}")

6. 日期范围生成器

from datetime import datetime, timedelta
from typing import Iterator

def date_range(start: datetime, end: datetime, step: timedelta = timedelta(days=1)) -> Iterator[datetime]:
    """生成日期范围"""
    current = start
    while current <= end:
        yield current
        current += step

# 生成一周的日期
start = datetime(2026, 5, 18)
end = datetime(2026, 5, 24)

print("本周日期:")
for dt in date_range(start, end):
    print(f"  {dt.strftime('%A')}: {dt.strftime('%Y-%m-%d')}")

常见陷阱与注意事项

陷阱1: naive 和 aware datetime 不能混用

from datetime import datetime, timezone, timedelta

naive = datetime.now()
aware = datetime.now(timezone.utc)

# BAD: 会抛出 TypeError
# result = aware - naive

# GOOD: 统一转为同一种
result = aware - naive.replace(tzinfo=timezone.utc)  # 或
result = aware.replace(tzinfo=None) - naive

陷阱2:datetime.now() 返回本地时间

from datetime import datetime, timezone

# 不要用 datetime.now() 来表示"当前时间"用于存储
# 推荐使用 UTC 时间存储
now_utc = datetime.now(timezone.utc)

# 展示时再转换为本地时间
local_tz = __import__("zoneinfo").ZoneInfo("Asia/Shanghai")
local_time = now_utc.astimezone(local_tz)

陷阱3:strptime 的性能

from datetime import datetime

# BAD: 在循环中重复解析相同格式
dates = ["2026-05-21", "2026-05-22", "2026-05-23"]
for d in dates:
    dt = datetime.strptime(d, "%Y-%m-%d")  # 每次都要解析格式字符串

# GOOD: 先编译格式
fmt = "%Y-%m-%d"
for d in dates:
    dt = datetime.strptime(d, fmt)

小结

日期时间处理是每个 Python 程序员必备的技能。本章我们系统学习了 datetime 模块的五大核心类(datetimedatetimetimedeltatimezone),掌握了 strftime/strptime 的格式化和解析方法,了解了 timedelta 的日期算术运算、时区处理(naive vs aware、zoneinfo 的使用)、时间戳互转,以及 calendar 模块和 dateutil 库的高级功能。通过任务调度、年龄计算、工作日计算、时区转换、日志解析和日期范围生成六个实战案例,你应该已经能将日期时间知识应用到实际项目中。最后记住一个关键原则:存储用 UTC,展示转本地——这能避免绝大多数时区相关的 BUG。

Summary: datetime 类、时区、timedelta、格式化与实战案例。