Python 日期时间处理完全指南:datetime、zoneinfo、dateutil 与性能优化

Python 日期时间处理终极指南:从 datetime, timedelta, timezone 基础到 zoneinfo 时区管理、dateutil 智能解析、Arrow 库实战。包含企业级报表日期处理、跨时区转换、性能优化技巧与完整代码示例。

日期时间处理是 Python 开发中最常见却也最容易出错的领域之一。本文从字符串解析到跨时区计算,从性能陷阱到企业级最佳实践,给你一个完整答案。


目录

  1. 为什么日期时间容易出错
  2. datetime 模块核心概念
  3. 日期时间对象创建与基本操作
  4. 格式化字符串:strftime vs strptime
  5. 时区处理:从 pytz 到 zoneinfo
  6. timedelta:时间差计算
  7. dateutil:智能日期解析
  8. 常见日期场景实战
  9. 性能优化与陷阱
  10. 日期处理速查表

1. 为什么日期时间容易出错

在正式学习之前,先看几个常见的坑:

陷阱错误示例后果
时区缺失datetime.now() 没有时区跨时区系统数据混乱
字符串拼日期"2024-01-" + str(day)02 月没有 30 号却拼出 "2024-02-30"
夏令时美国东部 3 月 10 日 2:00 AM2024-03-10 02:30 根本不存在
精度丢失毫秒级时间戳转 float精度丢失导致排序错误

核心原则:始终使用时区感知的 datetime 对象,永远不要信任没有时区信息的字符串或时间戳。


2. datetime 模块核心概念

Python 的 datetime 模块包含四个主要类型:

类型描述示例
date日期(年-月-日)2024-08-12
time时间(时-分-秒-微秒)14:30:00.123456
datetime日期 + 时间的组合2024-08-12 14:30:00
timedelta两个日期时间的差值3 days, 2:30:00
from datetime import date, time, datetime, timedelta
import time as time_module

# 创建各种日期时间对象
today = date.today()                          # 2024-08-12
now_time = time(14, 30, 0)                   # 14:30:00
now_dt = datetime.now()                       # 2024-08-12 14:30:15.123456

3. 日期时间对象创建与基本操作

3.1 获取当前时间

from datetime import datetime, date

# 当前本地时间(无时区信息!)
now_naive = datetime.now()
print(now_naive)   # 2024-08-12 14:30:15.123456

# 当前 UTC 时间(仍无时区)
utc_now = datetime.utcnow()
print(utc_now)     # 2024-08-12 06:30:15.123456

# ⭐ 推荐:带时区的当前 UTC 时间(Python 3.2+)
from datetime import timezone
utc_aware = datetime.now(timezone.utc)
print(utc_aware)   # 2024-08-12 06:30:15.123456+00:00

# 纯日期
today = date.today()
print(f"今天是 {today.year}{today.month}{today.day} 日")

3.2 指定时间创建

from datetime import datetime, date

# 直接构造
dt = datetime(2024, 8, 12, 14, 30, 0)
print(dt)   # 2024-08-12 14:30:00

# 从时间戳创建(UTC 秒数)
ts = 1723452600  # 假设的时间戳
dt_from_ts = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt_from_ts)   # 2024-08-12 06:30:00+00:00

# 从时间戳(毫秒)
ts_ms = 1723452600000
dt_from_ms = datetime.fromtimestamp(ts_ms / 1000, tz=timezone.utc)

# 纯日期构造
d = date(2024, 8, 12)

3.3 日期比较与替换

from datetime import datetime

dt1 = datetime(2024, 8, 12, 14, 0, 0)
dt2 = datetime(2024, 8, 13, 10, 0, 0)

print(dt1 < dt2)        # True
print(dt1 == dt2)       # False

# 替换部分字段(返回新对象,原对象不变)
dt_new = dt1.replace(year=2025, hour=9)
print(dt_new)   # 2025-08-12 09:00:00

# 转换为 date 或 time
d = dt1.date()          # 2024-08-12
t = dt1.time()          # 14:00:00

4. 格式化字符串:strftime vs strptime

这是日期时间操作中最常用的两个方法,也是面试高频考点。

4.1 strftime:datetime → 字符串(format)

from datetime import datetime

dt = datetime(2024, 8, 12, 14, 30, 0)

# 常用格式
print(dt.strftime("%Y-%m-%d"))           # 2024-08-12
print(dt.strftime("%Y/%m/%d %H:%M"))     # 2024/08/12 14:30
print(dt.strftime("%A, %B %d, %Y"))      # Monday, August 12, 2024
print(dt.strftime("%c"))                 # Mon Aug 12 14:30:00 2024 (本地格式)
print(dt.isoformat())                    # 2024-08-12T14:30:00(ISO 8601 标准)

4.2 strptime:字符串 → datetime(parse)

from datetime import datetime

date_string = "2024-08-12 14:30:00"
format_string = "%Y-%m-%d %H:%M:%S"

dt = datetime.strptime(date_string, format_string)
print(dt)       # 2024-08-12 14:30:00
print(type(dt)) # <class 'datetime.datetime'>

4.3 常用格式化代码速查

代码含义示例
%Y四位年份2024
%y两位年份24
%m月份(01-12)08
%b / %B月份缩写/全称Aug / August
%d日期(01-31)12
%H小时 24 制(00-23)14
%I小时 12 制(01-12)02
%pAM/PMPM
%M分钟(00-59)30
%S秒(00-59)00
%f微秒(000000-999999)123456
%zUTC 偏移+0800
%Z时区名称CST
%A / %a星期全称/缩写Monday / Mon
%j一年中的第几天(001-366)225
%W一年中的第几周(00-53)32
%c本地日期时间表示Mon Aug 12 14:30:00 2024

4.4 ISO 8601 标准格式(推荐)

from datetime import datetime, timezone

# ISO 格式字符串(推荐用于 API 和存储)
dt = datetime(2024, 8, 12, 14, 30, 0, tzinfo=timezone.utc)
iso_str = dt.isoformat()
print(iso_str)        # 2024-08-12T14:30:00+00:00

# 从 ISO 格式解析(Python 3.7+)
parsed = datetime.fromisoformat("2024-08-12T14:30:00")
print(parsed)         # 2024-08-12 14:30:00

# 带 Z(UTC 标记)的 ISO 字符串
from datetime import timezone
dt_z = datetime.fromisoformat("2024-08-12T14:30:00Z".replace("Z", "+00:00"))
print(dt_z)           # 2024-08-12 14:30:00+00:00

# 更简便的方式(Python 3.11+)
import datetime
try:
    dt_z = datetime.datetime.fromisoformat("2024-08-12T14:30:00+00:00")
except ValueError:
    pass

5. 时区处理:从 pytz 到 zoneinfo

5.1 为什么需要时区?

假设你记录了一个事件为 2024-08-12 14:30:00。这到底是什么时区?

  • 如果是北京时间(UTC+8),对应的 UTC 时间是 06:30
  • 如果是纽约时间(UTC-4,夏令时),对应的 UTC 时间是 18:30

没有时区信息的时间叫做 “naive”,有信息的是 “aware”。在生产环境中,永远使用 aware datetime。

5.2 Python 3.9+ 推荐:zoneinfo

Python 3.9 引入了标准库的 zoneinfo,不再需要 pytz:

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

# 创建带时区的时间
beijing = ZoneInfo("Asia/Shanghai")
dt_bj = datetime(2024, 8, 12, 14, 30, tzinfo=beijing)
print(dt_bj)   # 2024-08-12 14:30:00+08:00

# UTC 时间
utc = timezone.utc
dt_utc = datetime(2024, 8, 12, 6, 30, tzinfo=utc)
print(dt_utc)  # 2024-08-12 06:30:00+00:00

# 时区转换
dt_ny = dt_bj.astimezone(ZoneInfo("America/New_York"))
print(dt_ny)   # 2024-08-12 02:30:00-04:00

# 获取所有可用时区
import zoneinfo
available = list(zoneinfo.available_timezones())
print(f"共有 {len(available)} 个时区")   # 约 600 个
print("Asia/Shanghai" in available)      # True

5.3 常见时区转换场景

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

def convert_timezone(dt_str, from_tz, to_tz, fmt="%Y-%m-%d %H:%M:%S"):
    """将日期时间从一个时区转换到另一个时区"""
    # 解析原时间(假设为 from_tz 时区)
    naive_dt = datetime.strptime(dt_str, fmt)
    
    # 附加源时区信息
    from_zone = ZoneInfo(from_tz)
    aware_dt = naive_dt.replace(tzinfo=from_zone)
    
    # 转换到目标时区
    to_zone = ZoneInfo(to_tz)
    return aware_dt.astimezone(to_zone)

# 示例:视频会议时间通知
meeting_time = "2024-08-12 10:00:00"
# 会议发起者在北京
bj_time = convert_timezone(meeting_time, "Asia/Shanghai", "Asia/Shanghai")
ny_time = convert_timezone(meeting_time, "Asia/Shanghai", "America/New_York")
print(f"北京时间: {bj_time}")   # 北京时间: 2024-08-12 10:00:00+08:00
print(f"纽约时间: {ny_time}")   # 纽约时间: 2024-08-11 22:00:00-04:00

5.4 夏令时(DST)注意事项

from datetime import datetime
from zoneinfo import ZoneInfo

# 夏令时转换示例:美国东部
eastern = ZoneInfo("America/New_York")

# 3 月 10 日 2:00 AM —— 时钟跳到 3:00 AM("spring forward")
dst_start = datetime(2024, 3, 10, 2, 30, tzinfo=eastern)
print(dst_start)   # 实际会变成 03:30:00-04:00

# 11 月 3 日 1:00 AM —— 时钟回拨 1 小时("fall back")
dst_end = datetime(2024, 11, 3, 1, 30, fold=1, tzinfo=eastern)
# fold=1 表示第二次出现的 1:30

6. timedelta:时间差计算

from datetime import datetime, timedelta

now = datetime(2024, 8, 12, 14, 0, 0)
print(f"当前时间: {now}")

# 加减时间
future = now + timedelta(days=7, hours=3)
past = now - timedelta(weeks=2)

print(f"7天3小时后: {future}")   # 2024-08-19 17:00:00
print(f"2周前: {past}")          # 2024-07-29 14:00:00

# 计算时间差
deadline = datetime(2024, 12, 31, 23, 59, 59)
remaining = deadline - now

print(f"距离年底还有: {remaining.days} 天")
print(f"还有 {remaining.total_seconds()} 秒")

# 时间差格式化
def format_duration(td: timedelta) -> str:
    """将 timedelta 格式化为人类可读的字符串"""
    days = td.days
    hours, remainder = divmod(td.seconds, 3600)
    minutes, seconds = divmod(remainder, 60)
    
    parts = []
    if days > 0:
        parts.append(f"{days} 天")
    if hours > 0:
        parts.append(f"{hours} 小时")
    if minutes > 0:
        parts.append(f"{minutes} 分钟")
    if seconds > 0 or not parts:
        parts.append(f"{seconds} 秒")
    
    return " ".join(parts) if parts else "0 秒"

print(format_duration(remaining))

实用的日期计算函数

from datetime import datetime, timedelta

def add_working_days(start_date, days):
    """添加指定工作日(跳过周末)"""
    current = start_date
    added = 0
    while added < days:
        current += timedelta(days=1)
        if current.weekday() < 5:  # 周一到周五是 0-4
            added += 1
    return current

def get_week_range(date):
    """获取指定日期所在周的起始和结束"""
    start = date - timedelta(days=date.weekday())
    end = start + timedelta(days=6)
    return start, end

def get_month_range(year, month):
    """获取指定月的起始和结束"""
    from calendar import monthrange
    first_day = datetime(year, month, 1)
    last_day_num = monthrange(year, month)[1]
    last_day = datetime(year, month, last_day_num, 23, 59, 59)
    return first_day, last_day

# 使用
today = datetime(2024, 8, 12)
print(f"3个工作日后: {add_working_days(today, 3)}")   # 跳过了周末
week_start, week_end = get_week_range(today.date())
print(f"本周: {week_start}{week_end}")

7. dateutil:智能日期解析

标准库的 strptime 需要严格匹配格式。生产环境中输入往往格式不统一,dateutil 可以智能解析多种常见格式。

# pip install python-dateutil
from dateutil import parser

# 自动识别格式
dt1 = parser.parse("2024-08-12 14:30:00")
dt2 = parser.parse("Aug 12, 2024 at 2:30pm")
dt3 = parser.parse("2024-08-12")           # 只有日期,时间默认为 00:00:00

print(dt1)   # 2024-08-12 14:30:00
print(dt2)   # 2024-08-12 14:30:00
print(dt3)   # 2024-08-12 00:00:00

# 处理中文格式(自定义解析器)
from dateutil.parser import parserinfo

class ChineseDateParser(parserinfo):
    MONTHS = [
        ("一月", "1月"), ("二月", "2月"), ("三月", "3月"),
        ("四月", "4月"), ("五月", "5月"), ("六月", "6月"),
        ("七月", "7月"), ("八月", "8月"), ("九月", "9月"),
        ("十月", "10月"), ("十一月", "11月"), ("十二月", "12月"),
    ]

dt_cn = parser.parse("2024年8月12日", parserinfo=ChineseDateParser())
print(dt_cn)   # 2024-08-12 00:00:00

# 相对时间(超实用!)
from dateutil.relativedelta import relativedelta

now = datetime(2024, 8, 12)
next_month = now + relativedelta(months=1)           # 下月同日
next_month_end = now + relativedelta(months=1, day=31)  # 下月末
next_year_jan = now + relativedelta(years=1, month=1, day=1)

print(f"下个月: {next_month}")
print(f"下个月末: {next_month_end}")     # 自动处理不同月份天数
print(f"明年 1/1: {next_year_jan}")

8. 常见日期场景实战

场景 1:日志时间戳解析与排序

from datetime import datetime
from dateutil import parser

logs = [
    "2024-08-12 14:30:15 [INFO] 服务启动",
    "2024/08/12 14:30:10 [INFO] 配置加载完成",
    "Aug 12, 2024 2:30:20 PM [WARN] 内存使用 80%",
]

def parse_log_timestamp(log_line):
    """从日志行中提取并解析时间戳"""
    # 尝试多种格式
    parts = log_line.split(" [", 1)
    if len(parts) == 2:
        time_str = parts[0]
        try:
            return parser.parse(time_str)
        except ValueError:
            pass
    return None

parsed_logs = [(parse_log_timestamp(log), log) for log in logs]
parsed_logs.sort(key=lambda x: x[0] or datetime.min)

print("按时间排序的日志:")
for dt, log in parsed_logs:
    print(f"  {dt}: {log}")

场景 2:生成日期范围报表

from datetime import datetime, timedelta

def date_range(start_date, end_date):
    """生成日期范围迭代器"""
    current = start_date
    while current <= end_date:
        yield current
        current += timedelta(days=1)

# 生成 8 月每一天的日报
dt_start = datetime(2024, 8, 1)
dt_end = datetime(2024, 8, 31)

print("8 月日报清单:")
for d in date_range(dt_start, dt_end):
    weekday_name = ["一", "二", "三", "四", "五", "六", "日"][d.weekday()]
    print(f"  {d.strftime('%Y-%m-%d')}{weekday_name}")

场景 3:API 接口的时间参数处理

from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo

def parse_api_datetime(dt_str: str, client_tz: str = "UTC") -> datetime:
    """
    解析客户端传来的时间字符串,统一转为 UTC
    支持的输入格式:
      - ISO 8601: 2024-08-12T14:30:00+08:00
      - 简单格式: 2024-08-12 14:30:00
    """
    try:
        # 尝试解析带时区的 ISO 格式
        if "Z" in dt_str:
            dt_str = dt_str.replace("Z", "+00:00")
        
        if any(c in dt_str for c in ['+', '-'] if dt_str.index(c) > 10):
            dt = datetime.fromisoformat(dt_str)
        else:
            # 无时区字符串 → 按客户端时区解析
            dt = datetime.strptime(dt_str, "%Y-%m-%d %H:%M:%S")
            dt = dt.replace(tzinfo=ZoneInfo(client_tz))
        
        # 统一转 UTC
        return dt.astimezone(timezone.utc)
    except Exception as e:
        raise ValueError(f"无法解析日期时间: {dt_str},错误: {e}")

# 客户端传来北京时间
client_dt = "2024-08-12 14:30:00"
utc_dt = parse_api_datetime(client_dt, client_tz="Asia/Shanghai")
print(f"客户端时间: {client_dt}")
print(f"UTC 时间: {utc_dt.strftime('%Y-%m-%dT%H:%M:%SZ')}")

场景 4:计算年龄与生日提醒

from datetime import datetime, date
from dateutil.relativedelta import relativedelta

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

def days_until_birthday(birth_date: date) -> int:
    """距离下次生日还有多少天"""
    today = date.today()
    next_birthday = birth_date.replace(year=today.year)
    if next_birthday < today:
        next_birthday = next_birthday.replace(year=today.year + 1)
    return (next_birthday - today).days

# 测试
birth = date(1995, 10, 15)
print(f"年龄: {calculate_age(birth)} 岁")
print(f"距离下次生日: {days_until_birthday(birth)} 天")

9. 性能优化与陷阱

9.1 datetime 对象的性能

import timeit
from datetime import datetime

# strftime vs 字符串拼接
now = datetime.now()

# 方式 1:f-string 拼接(最快)
def use_fstring():
    return f"{now.year}-{now.month:02d}-{now.day:02d}"

# 方式 2:strftime(慢但可读性好)
def use_strftime():
    return now.strftime("%Y-%m-%d")

print(f"f-string:   {timeit.timeit(use_fstring, number=100000):.4f}s")
print(f"strftime:   {timeit.timeit(use_strftime, number=100000):.4f}s")

结论:strftime 在单次操作中性能差异可忽略;在高频循环(如处理百万条日志)时考虑用 f-string。

9.2 常见陷阱清单

from datetime import datetime, timezone

# ❌ 陷阱 1:naive 和 aware 混用
dt_naive = datetime(2024, 8, 12)
dt_aware = datetime(2024, 8, 12, tzinfo=timezone.utc)
# dt_naive > dt_aware   # TypeError: can't compare offset-naive and offset-aware

# ✅ 解决:统一转为 aware
dt_naive_utc = dt_naive.replace(tzinfo=timezone.utc)

# ❌ 陷阱 2:%Y 和 %y 混淆
# datetime.strptime("24-08-12", "%Y-%m-%d")  # ValueError: 24 不是四位年份
# 应该用 %y

# ❌ 陷阱 3:月份和日期超出范围
# datetime(2024, 2, 30)   # ValueError: day is out of range for month

# ✅ 安全创建日期
def safe_date(year, month, day):
    """安全创建日期,超出范围返回月末"""
    from calendar import monthrange
    max_day = monthrange(year, month)[1]
    return datetime(year, month, min(day, max_day))

print(safe_date(2024, 2, 30))   # 2024-02-29 00:00:00(自动调整为闰年 29 日)

# ❌ 陷阱 4:时间戳精度丢失
ts = 1723452600.123456789
# float 转 int 会丢失小数部分
ts_int = int(ts)   # 丢失了毫秒信息

9.3 Arrow 库:更友好的时间处理

如果你觉得标准库太繁琐,可以使用 Arrow:

# pip install arrow
import arrow

now = arrow.now()
utc = arrow.utcnow()

# 人性化时间
print(now.humanize())                 # "a few seconds ago"
print(now.shift(hours=-3).humanize()) # "3 hours ago"

# 时区转换
ny = now.to("America/New_York")
print(ny.format("YYYY-MM-DD HH:mm:ss ZZZ"))   # 2024-08-12 02:30:00 EDT

# 解析任意格式
dt = arrow.get("2024-08-12", "YYYY-MM-DD")
print(dt)   # 2024-08-12T00:00:00+00:00

# 时间范围
start = arrow.get("2024-08-01")
end = arrow.get("2024-08-31")
for r in arrow.Arrow.range("day", start, end):
    print(r.format("YYYY-MM-DD dddd"))

10. 日期处理速查表

常用操作速查

需求代码
获取当前时间datetime.now() / datetime.now(timezone.utc)
格式化输出dt.strftime("%Y-%m-%d"))
字符串解析datetime.strptime(text, "%Y-%m-%d")
ISO 格式输出dt.isoformat()
ISO 格式解析datetime.fromisoformat(text)
时区转换dt.astimezone(ZoneInfo("Asia/Tokyo"))
加一天dt + timedelta(days=1)
加一月dt + relativedelta(months=1)
时间差(秒)(dt2 - dt1).total_seconds()
时间差(天数)(dt2 - dt1).days
转时间戳(秒)dt.timestamp()
从时间戳创建datetime.fromtimestamp(ts, tz=timezone.utc)
获取星期几dt.weekday() 0=周一, dt.isoweekday() 1=周一
获取是本年第几周dt.isocalendar().week

延伸阅读


时间处理没有银弹。标准库 datetime + zoneinfo 足以应对 90% 的场景;dateutil 在解析非标准格式时非常有用;当你需要更简洁的 API 时,Arrow 是一个好选择。始终记住:存储用 UTC,显示用本地时区。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「python」更多文章

  1. Python 高级异步编程:Trio 结构化并发与 AnyIO 兼容层
  2. Python 数据工程与 ETL 管道实战
  3. Python 元编程与动态特性深度解析