🐍 第07课:日期时间

—— 时间是最好的程序员

🏆 时区转换+工作日计算
✅ Python验证通过

📌 本课目标

1️⃣ datetime 核心类

from datetime import datetime, date, time, timedelta, timezone

# datetime - 日期+时间
now = datetime.now()
print(now)               # 2026-05-19 03:56:00.123456
print(now.year)          # 2026
print(now.month)         # 5
print(now.day)           # 19
print(now.hour)          # 3
print(now.weekday())     # 0=周一, 6=周日
print(now.isoweekday())  # 1=周一, 7=周日

# date - 仅日期
today = date.today()
print(today)             # 2026-05-19

# time - 仅时间
t = time(14, 30, 0)
print(t)                 # 14:30:00

# 构造指定日期时间
dt = datetime(2026, 1, 1, 0, 0, 0)
print(dt)                # 2026-01-01 00:00:00

2️⃣ 格式化与解析

from datetime import datetime

# datetime → 字符串
now = datetime.now()
print(now.strftime("%Y-%m-%d"))           # 2026-05-19
print(now.strftime("%Y年%m月%d日 %H:%M")) # 2026年05月19日 03:56
print(now.strftime("%A, %B %d, %Y"))    # Monday, May 19, 2026
print(now.isoformat())                         # 2026-05-19T03:56:00.123456

# 字符串 → datetime
dt1 = datetime.strptime("2026-01-15", "%Y-%m-%d")
dt2 = datetime.strptime("15/01/26 14:30", "%d/%m/%y %H:%M")

# 常用格式符
# %Y 四位年  %m 月  %d 日  %H 24时  %M 分  %S 秒
# %y 两位年  %b 月名缩写  %B 月名全称  %A 星期全称
# %I 12时制  %p AM/PM  %f 微秒  %z 时区

# f-string 直接格式化 datetime
now = datetime.now()
print(f"{now:%Y-%m-%d %H:%M:%S}")   # 更简洁!

3️⃣ timedelta 时间差

from datetime import datetime, timedelta

now = datetime.now()

# 时间加减
tomorrow = now + timedelta(days=1)
yesterday = now - timedelta(days=1)
next_week = now + timedelta(weeks=1)
two_hours_later = now + timedelta(hours=2)

# 计算两个日期差
birthday = datetime(2026, 12, 25)
diff = birthday - now
print(f"距离生日还有 {diff.days} 天")

# timedelta 属性
delta = timedelta(days=5, hours=3, minutes=30)
print(delta.days)          # 5
print(delta.seconds)       # 12600 (3h30m = 12600s)
print(delta.total_seconds())  # 469800.0

# 实用:计算某个日期后的第N个工作日
def add_business_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

4️⃣ 时区转换

from datetime import datetime, timezone, timedelta

# 创建时区
UTC = timezone.utc
CST = timezone(timedelta(hours=8))    # 中国标准时间 UTC+8
EST = timezone(timedelta(hours=-5))    # 美东时间 UTC-5
JST = timezone(timedelta(hours=9))     # 日本时间 UTC+9

# 获取带时区的当前时间
utc_now = datetime.now(UTC)
cst_now = datetime.now(CST)

# 时区转换
# 方法1: astimezone()
utc_time = datetime.now(UTC)
beijing = utc_time.astimezone(CST)
tokyo = utc_time.astimezone(JST)
new_york = utc_time.astimezone(EST)

print(f"UTC:   {utc_time:%Y-%m-%d %H:%M}")
print(f"北京:  {beijing:%Y-%m-%d %H:%M}")
print(f"东京:  {tokyo:%Y-%m-%d %H:%M}")
print(f"纽约:  {new_york:%Y-%m-%d %H:%M}")

# 方法2: replace() 为无时区时间加时区
naive = datetime(2026, 1, 1, 12, 0)  # 无时区信息
aware = naive.replace(tzinfo=CST)     # 标记为北京时间

# 使用 zoneinfo(Python 3.9+ 标准库)
from zoneinfo import ZoneInfo

shanghai = ZoneInfo("Asia/Shanghai")
tokyo_tz = ZoneInfo("Asia/Tokyo")
ny_tz = ZoneInfo("America/New_York")

# 自动处理夏令时!
summer_time = datetime(2026, 7, 1, 12, 0, tzinfo=ny_tz)

5️⃣ 工作日计算实战

from datetime import datetime, timedelta, date

def count_business_days(start: date, end: date) -> int:
    """计算两个日期之间的工作日数(不含节假日)"""
    if start > end:
        start, end = end, start
    days = 0
    current = start
    while current <= end:
        if current.weekday() < 5:  # 周一~周五
            days += 1
        current += timedelta(days=1)
    return days

def add_business_days(start: date, n: int) -> date:
    """从 start 起第 n 个工作日的日期"""
    current = start
    added = 0
    while added < n:
        current += timedelta(days=1)
        if current.weekday() < 5:
            added += 1
    return current

def next_weekday(d: date, weekday: int) -> date:
    """下一个指定的星期几(0=周一, 6=周日)"""
    days_ahead = weekday - d.weekday()
    if days_ahead <= 0:
        days_ahead += 7
    return d + timedelta(days=days_ahead)

# 中国法定节假日(示例)
HOLIDAYS_2026 = {
    date(2026, 1, 1),    # 元旦
    date(2026, 2, 17),   # 春节
    date(2026, 4, 5),    # 清明
    date(2026, 5, 1),    # 劳动节
    date(2026, 10, 1),   # 国庆
}

def is_workday(d: date) -> bool:
    """判断是否为工作日(排除周末和节假日)"""
    if d.weekday() >= 5:
        return False
    if d in HOLIDAYS_2026:
        return False
    return True

def deadline_from(start: date, business_days: int) -> date:
    """从 start 起,N个工作日后的截止日期"""
    current = start
    count = 0
    while count < business_days:
        current += timedelta(days=1)
        if is_workday(current):
            count += 1
    return current

6️⃣ 时间戳与 ISO 格式

from datetime import datetime, timezone

# datetime → 时间戳
now = datetime.now(timezone.utc)
ts = now.timestamp()
print(f"时间戳: {ts}")  # 1776678960.123456

# 时间戳 → datetime
dt = datetime.fromtimestamp(ts, tz=timezone.utc)
print(dt)

# ISO 8601 格式(推荐用于API)
iso_str = now.isoformat()
print(iso_str)  # 2026-05-19T03:56:00.123456+00:00

# 解析 ISO 格式
parsed = datetime.fromisoformat(iso_str)
print(parsed)

# Unix 时间戳转人类可读
def format_timestamp(ts, fmt="%Y-%m-%d %H:%M:%S"):
    return datetime.fromtimestamp(ts).strftime(fmt)

7️⃣ 验证脚本

#!/usr/bin/env python3
"""第07课 日期时间验证"""
from datetime import datetime, date, timedelta, timezone

def test_datetime_basics():
    """基本操作测试"""
    now = datetime.now()
    assert isinstance(now.year, int) and now.year >= 2026
    dt = datetime(2026, 6, 15, 10, 30)
    assert dt.month == 6 and dt.hour == 10
    print("✅ 基本操作测试通过")

def test_format_parse():
    """格式化与解析测试"""
    dt = datetime(2026, 5, 19, 14, 30, 0)
    s = dt.strftime("%Y-%m-%d %H:%M")
    assert s == "2026-05-19 14:30"
    parsed = datetime.strptime(s, "%Y-%m-%d %H:%M")
    assert parsed == dt
    print("✅ 格式化与解析测试通过")

def test_timedelta():
    """时间差测试"""
    d1 = date(2026, 5, 1)
    d2 = date(2026, 5, 10)
    diff = d2 - d1
    assert diff.days == 9
    delta = timedelta(days=5, hours=3)
    assert delta.total_seconds() == 5 * 86400 + 3 * 3600
    print("✅ 时间差测试通过")

def test_timezone():
    """时区转换测试"""
    UTC = timezone.utc
    CST = timezone(timedelta(hours=8))
    utc_time = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
    beijing = utc_time.astimezone(CST)
    assert beijing.hour == 8
    print("✅ 时区转换测试通过")

def test_business_days():
    """工作日计算测试"""
    def count_biz(start, end):
        days = 0
        current = start
        while current <= end:
            if current.weekday() < 5:
                days += 1
            current += timedelta(days=1)
        return days

    # 2026-05-18(周一) 到 2026-05-22(周五) = 5个工作日
    assert count_biz(date(2026, 5, 18), date(2026, 5, 22)) == 5

    # 周六到周一 = 1个工作日
    assert count_biz(date(2026, 5, 16), date(2026, 5, 18)) == 1

    print("✅ 工作日计算测试通过")

def test_timestamp():
    """时间戳测试"""
    dt = datetime(2026, 1, 1, tzinfo=timezone.utc)
    ts = dt.timestamp()
    dt2 = datetime.fromtimestamp(ts, tz=timezone.utc)
    assert dt == dt2
    print("✅ 时间戳测试通过")

if __name__ == "__main__":
    test_datetime_basics()
    test_format_parse()
    test_timedelta()
    test_timezone()
    test_business_days()
    test_timestamp()
    print("\n🎉 第07课全部验证通过!")
✅ 基本操作测试通过 ✅ 格式化与解析测试通过 ✅ 时间差测试通过 ✅ 时区转换测试通过 ✅ 工作日计算测试通过 ✅ 时间戳测试通过 🎉 第07课全部验证通过!

🔑 本课要点

  1. naive vs aware——无时区 vs 有时区,API 开发必须用 aware
  2. zoneinfo——Python 3.9+ 标准库,自动处理夏令时
  3. f-string 格式化——f"{now:%Y-%m-%d}" 比 strftime 更优雅
  4. weekday() 返回 0-6——0=周一,用于工作日判断
  5. isoformat()——API 时间传输首选格式