- 用户在不同时区提交的时间数据,在数据库中存储后竟然对不上?
- 计算两个日期之间的天数差,结果总是少一天或多一天?
- 格式化日期字符串时,%Y和%y傻傻分不清,总是报ValueError?
- 处理夏令时转换时,发现同一时间点在系统中存在两个不同的表示?
- 选择日期时间库时,在datetime、dateutil、arrow、pandas之间犹豫不决?
如果你正在为这些问题烦恼,那么恭喜你,今天就是你告别时间处理噩梦的转折点!datetime模块作为Python标准库的核心组件,提供了完整、专业、经过千锤百炼的时间处理能力,是每一位Python开发者必须掌握的基本功。本文将带你从零开始,系统性地掌握datetime模块的每一个核心概念,通过大量实战代码示例和最佳实践,让你彻底理解Python时间处理的精髓,成为时间管理的大师!datetime模块是Python标准库中专门用于处理日期和时间的核心模块。它提供了一套完整、类型安全、高性能的API,能够满足从简单日期计算到复杂时区处理的各种需求。- 完整性:涵盖日期(date)、时间(time)、日期时间(datetime)、时间间隔(timedelta)、时区(timezone)等所有时间相关概念
- 准确性:支持微秒级精度,正确处理闰年、闰秒、夏令时等复杂时间规则
- 类型安全:每个时间概念都有专门的类表示,编译时和运行时都能发现错误
- 国际化:支持时区转换、ISO 8601标准格式、本地化显示
- 生态兼容:与pandas、numpy、SQLAlchemy等主流库无缝集成
datetime模块主要包含以下5个核心类和2个关键函数: | | |
| | |
| | |
| | datetime(2024, 12, 25, 14, 30, 45) |
| | timedelta(days=7, hours=3) |
| | timezone(timedelta(hours=8)) |
| | dt.strftime("%Y-%m-%d %H:%M:%S") |
| | datetime.strptime("2024-12-25", "%Y-%m-%d") |
在Python 3.9+中,还可以使用zoneinfo模块(标准库新增)来处理更复杂的时区需求,我们会在时区处理部分详细介绍。date类用于处理不带时间信息的日期,即年、月、日的组合。它适用于生日、纪念日、合同生效日、节假日等只需要日期的场景。from datetime import date# 创建日期对象d1 = date(2024, 12, 25) # 2024年圣诞节print(f"圣诞节: {d1}") # 输出: 2024-12-25# 获取当前日期today = date.today()print(f"今天是: {today}") # 输出: 2026-03-17# 获取日期组件print(f"年: {d1.year}, 月: {d1.month}, 日: {d1.day}") # 输出: 年: 2024, 月: 12, 日: 25# 获取星期几 (0=周一, 6=周日)print(f"星期几: {d1.weekday()}") # 输出: 2 (周三)print(f"ISO星期几: {d1.isoweekday()}") # 输出: 3 (1=周一, 7=周日)
from datetime import date, timedelta# 日期比较d1 = date(2024, 12, 25)d2 = date(2025, 1, 1)print(f"d1 < d2: {d1 < d2}") # 输出: Trueprint(f"d1 == d2: {d1 == d2}") # 输出: False# 日期加减today = date.today()yesterday = today - timedelta(days=1)tomorrow = today + timedelta(days=1)next_week = today + timedelta(weeks=1)print(f"昨天: {yesterday}")print(f"明天: {tomorrow}")print(f"一周后: {next_week}")# 日期差计算christmas = date(2024, 12, 25)new_year = date(2025, 1, 1)days_between = new_year - christmas # 返回timedelta对象print(f"圣诞节到元旦还有 {days_between.days} 天") # 输出: 7天
time类用于处理不带日期信息的时间,即时、分、秒、微秒的组合。它适用于会议时间、营业时间、航班时刻等只需要时间的场景。from datetime import time# 创建时间对象t1 = time(14, 30, 45) # 14:30:45t2 = time(14, 30, 45, 123456) # 14:30:45.123456 (微秒级精度)t3 = time(9, 0, 0, tzinfo=None) # 无时区信息print(f"会议时间: {t1}") # 输出: 14:30:45print(f"精确时间: {t2}") # 输出: 14:30:45.123456print(f"带微秒: {t2.microsecond}") # 输出: 123456# 获取时间组件print(f"时: {t1.hour}, 分: {t1.minute}, 秒: {t1.second}")
from datetime import time# 创建时间对象meeting_time = time(14, 30, 45)# 格式化为字符串formatted = meeting_time.strftime("%H:%M:%S")print(f"24小时制: {formatted}") # 输出: 14:30:45formatted_12h = meeting_time.strftime("%I:%M:%S %p")print(f"12小时制: {formatted_12h}") # 输出: 02:30:45 PM# ISO格式iso_time = meeting_time.isoformat()print(f"ISO格式: {iso_time}") # 输出: 14:30:45
datetime类是datetime模块中最常用、功能最全面的类,它结合了date和time的功能,可以处理带日期和时间信息的完整时间点。几乎所有实际业务场景都应该使用datetime类。from datetime import datetime# 获取当前日期时间now = datetime.now()print(f"当前时间: {now}") # 输出: 2026-03-17 14:30:45.123456# 创建指定日期时间dt1 = datetime(2024, 12, 25, 14, 30, 45)print(f"圣诞节下午: {dt1}") # 输出: 2024-12-25 14:30:45# 获取日期和时间组件print(f"年: {dt1.year}, 月: {dt1.month}, 日: {dt1.day}")print(f"时: {dt1.hour}, 分: {dt1.minute}, 秒: {dt1.second}, 微秒: {dt1.microsecond}")# 提取date和time部分print(f"日期部分: {dt1.date()}") # 输出: 2024-12-25print(f"时间部分: {dt1.time()}") # 输出: 14:30:45
时间戳是计算机领域表示时间的通用方式,指从1970年1月1日00:00:00 UTC开始到当前时间的秒数。from datetime import datetime# datetime转时间戳dt = datetime(2024, 12, 25, 14, 30, 45)timestamp = dt.timestamp()print(f"时间戳: {timestamp}") # 输出: 1735137045.0# 时间戳转datetimedt_from_ts = datetime.fromtimestamp(timestamp)print(f"从时间戳恢复: {dt_from_ts}") # 输出: 2024-12-25 14:30:45# UTC时间戳转datetimedt_utc = datetime.utcfromtimestamp(timestamp)print(f"UTC时间: {dt_utc}") # 输出: 2024-12-25 06:30:45 (假设本地时区+8)
from datetime import datetime, timedelta# 当前时间now = datetime.now()# 未来的时间one_hour_later = now + timedelta(hours=1)three_days_later = now + timedelta(days=3)two_weeks_ago = now - timedelta(weeks=2)print(f"一小时后: {one_hour_later}")print(f"三天后: {three_days_later}")print(f"两周前: {two_weeks_ago}")# 复合时间间隔complex_delta = timedelta(days=5, hours=12, minutes=30, seconds=45)future = now + complex_deltaprint(f"5天12小时30分45秒后: {future}")# 时间差计算start = datetime(2024, 1, 1, 9, 0, 0)end = datetime(2024, 1, 2, 10, 30, 15)time_diff = end - start # 返回timedelta对象print(f"总秒数: {time_diff.total_seconds()}") # 输出: 91815.0print(f"天数: {time_diff.days}, 秒数: {time_diff.seconds}") # 输出: 1天, 18015秒
timedelta类用于表示两个时间点之间的间隔。它支持天、秒、微秒三个基本单位,所有其他时间单位(小时、分钟、周等)都会转换为这三个单位的组合。from datetime import timedelta# 创建时间间隔td1 = timedelta(days=7) # 一周td2 = timedelta(hours=12, minutes=30) # 12小时30分钟td3 = timedelta(weeks=2, days=3, hours=8) # 2周3天8小时print(f"一周: {td1}") # 输出: 7 days, 0:00:00print(f"12小时30分: {td2}") # 输出: 12:30:00print(f"2周3天8小时: {td3}") # 输出: 17 days, 8:00:00# 获取时间间隔组件print(f"天数: {td3.days}, 秒数: {td3.seconds}") # 输出: 17天, 28800秒# 总秒数print(f"总秒数: {td3.total_seconds()}") # 输出: 1504800.0
from datetime import timedelta# timedelta加减运算td1 = timedelta(days=5, hours=12)td2 = timedelta(days=3, hours=6)td_sum = td1 + td2td_diff = td1 - td2print(f"td1 + td2 = {td_sum}") # 输出: 8 days, 18:00:00print(f"td1 - td2 = {td_diff}") # 输出: 2 days, 6:00:00# timedelta乘除运算td3 = timedelta(days=2, hours=6)td_multiplied = td3 * 3# 乘以整数td_divided = td3 / 2# 除以整数print(f"td3 * 3 = {td_multiplied}") # 输出: 6 days, 18:00:00print(f"td3 / 2 = {td_divided}") # 输出: 1 day, 3:00:00# timedelta比较运算print(f"td1 > td2: {td1 > td2}") # 输出: Trueprint(f"td1 == td2: {td1 == td2}") # 输出: False
- naive对象:不带时区信息的日期时间对象,无法确定其表示的绝对时间点
- aware对象:带时区信息的日期时间对象,可以唯一确定一个绝对时间点
from datetime import datetime# naive对象(不推荐在实际业务中使用)naive_dt = datetime(2024, 12, 25, 14, 30, 45)print(f"naive对象: {naive_dt}, tzinfo: {naive_dt.tzinfo}") # tzinfo=None# aware对象(推荐)from datetime import timezone, timedelta# 使用timezone类(固定时差)utc_dt = datetime(2024, 12, 25, 14, 30, 45, tzinfo=timezone.utc)beijing_dt = datetime(2024, 12, 25, 14, 30, 45, tzinfo=timezone(timedelta(hours=8)))print(f"UTC时间: {utc_dt}") # 输出: 2024-12-25 14:30:45+00:00print(f"北京时间: {beijing_dt}") # 输出: 2024-12-25 14:30:45+08:00
6.2 zoneinfo模块(Python 3.9+)Python 3.9引入了zoneinfo模块,提供了对IANA时区数据库的完整支持,是处理时区的首选方案。from datetime import datetimefrom zoneinfo import ZoneInfo# 创建带时区的当前时间beijing_now = datetime.now(ZoneInfo("Asia/Shanghai"))newyork_now = datetime.now(ZoneInfo("America/New_York"))print(f"北京时间: {beijing_now}") # 例如: 2026-03-17 14:30:45.123456+08:00print(f"纽约时间: {newyork_now}") # 例如: 2026-03-17 02:30:45.123456-04:00# 时区转换utc_time = beijing_now.astimezone(ZoneInfo("UTC"))print(f"对应的UTC时间: {utc_time}") # 例如: 2026-03-17 06:30:45.123456+00:00# 处理夏令时paris_time = datetime(2024, 3, 31, 2, 30, 0, tzinfo=ZoneInfo("Europe/Paris"))print(f"巴黎时间 (夏令时切换): {paris_time}") # 自动处理夏令时
from datetime import datetimefrom zoneinfo import ZoneInfodefcreate_aware_datetime(year, month, day, hour=0, minute=0, second=0, tz_name="UTC"):"""创建带时区的日期时间对象"""return datetime(year, month, day, hour, minute, second, tzinfo=ZoneInfo(tz_name))defconvert_timezone(dt, target_tz_name):"""安全转换时区"""if dt.tzinfo isNone:raise ValueError("必须使用时区感知对象")return dt.astimezone(ZoneInfo(target_tz_name))defstore_in_utc(dt):"""将时间转换为UTC存储(最佳实践)"""if dt.tzinfo isNone:raise ValueError("必须使用时区感知对象")return dt.astimezone(ZoneInfo("UTC"))defdisplay_local_time(dt, user_tz_name):"""根据用户时区显示时间""" utc_time = dt.astimezone(ZoneInfo("UTC"))return utc_time.astimezone(ZoneInfo(user_tz_name))# 使用示例meeting_time = create_aware_datetime(2024, 12, 25, 14, 30, tz_name="Asia/Shanghai")utc_storage = store_in_utc(meeting_time)display_time = display_local_time(utc_storage, "America/New_York")print(f"原始会议时间: {meeting_time}")print(f"UTC存储时间: {utc_storage}")print(f"纽约显示时间: {display_time}")
七、格式化与解析:strftime与strptimestrftime()方法将日期时间对象格式化为指定格式的字符串。from datetime import datetimenow = datetime.now()# 常用格式formats = {"标准格式": now.strftime("%Y-%m-%d %H:%M:%S"), # 2026-03-17 14:30:45"中文格式": now.strftime("%Y年%m月%d日 %H时%M分%S秒"), # 2026年03月17日 14时30分45秒"美国格式": now.strftime("%m/%d/%Y %I:%M %p"), # 03/17/2026 02:30 PM"ISO格式": now.strftime("%Y-%m-%dT%H:%M:%S"), # 2026-03-17T14:30:45"周信息": now.strftime("%A, %B %d, %Y"), # Tuesday, March 17, 2026"文件名安全": now.strftime("%Y%m%d_%H%M%S"), # 20260317_143045}for name, fmt in formats.items(): print(f"{name}: {fmt}")
strptime()方法将字符串解析为日期时间对象,格式必须严格匹配。from datetime import datetime# 解析不同格式的字符串date_strings = [ ("2024-12-25 14:30:45", "%Y-%m-%d %H:%M:%S"), ("25/12/2024 14:30", "%d/%m/%Y %H:%M"), ("2024年12月25日", "%Y年%m月%d日"), ("Dec 25, 2024 02:30 PM", "%b %d, %Y %I:%M %p"), ("2024-12-25T14:30:45+08:00", "%Y-%m-%dT%H:%M:%S%z"), # 带时区]for date_str, format_str in date_strings: dt = datetime.strptime(date_str, format_str) print(f"字符串: {date_str} -> 解析结果: {dt}")
from datetime import datetime, timedeltafrom zoneinfo import ZoneInfodefschedule_meeting(organizer_tz, participants, duration_hours=1):"""安排跨时区会议"""# 获取组织者当前时间 organizer_now = datetime.now(ZoneInfo(organizer_tz))# 找到最近的整点时间if organizer_now.minute > 0or organizer_now.second > 0or organizer_now.microsecond > 0: start_time = organizer_now.replace( minute=0, second=0, microsecond=0 ) + timedelta(hours=1)else: start_time = organizer_now# 计算结束时间 end_time = start_time + timedelta(hours=duration_hours)# 为每个参与者转换时间 meeting_schedule = {"organizer": {"timezone": organizer_tz,"start": start_time,"end": end_time },"participants": {} }for participant, tz in participants.items(): participant_start = start_time.astimezone(ZoneInfo(tz)) participant_end = end_time.astimezone(ZoneInfo(tz)) meeting_schedule["participants"][participant] = {"timezone": tz,"start": participant_start,"end": participant_end }return meeting_schedule# 使用示例participants = {"张三": "Asia/Shanghai","李四": "America/New_York","王五": "Europe/London","赵六": "Australia/Sydney"}schedule = schedule_meeting("Asia/Shanghai", participants, duration_hours=1.5)print("会议安排:")print(f"组织者 ({schedule['organizer']['timezone']}):")print(f" 开始: {schedule['organizer']['start'].strftime('%Y-%m-%d %H:%M')}")print(f" 结束: {schedule['organizer']['end'].strftime('%Y-%m-%d %H:%M')}")print("\n参与者:")for name, info in schedule["participants"].items(): print(f"{name} ({info['timezone']}):") print(f" 开始: {info['start'].strftime('%Y-%m-%d %H:%M')}") print(f" 结束: {info['end'].strftime('%Y-%m-%d %H:%M')}")
from datetime import datetime, date, timedeltafrom zoneinfo import ZoneInfodefcalculate_workdays(start_date, num_days, holidays=None, timezone="Asia/Shanghai"):"""计算工作日,排除周末和节假日"""if holidays isNone: holidays = set() # 可以预先加载法定节假日 current_date = start_date workdays_count = 0 workdays_list = []while workdays_count < num_days:# 检查是否是周末 (周一=0, 周日=6)if current_date.weekday() >= 5: # 5=周六, 6=周日 current_date += timedelta(days=1)continue# 检查是否是节假日if current_date in holidays: current_date += timedelta(days=1)continue workdays_list.append(current_date) workdays_count += 1 current_date += timedelta(days=1)return workdays_listdefcreate_project_timeline(start_date, tasks, holidays=None):"""创建项目时间线""" timeline = [] current_date = start_datefor task in tasks: name = task["name"] workdays = task["workdays"]# 计算结束日期 workdays_list = calculate_workdays( current_date, workdays, holidays ) end_date = workdays_list[-1] if workdays_list else current_date timeline.append({"task": name,"start_date": current_date,"end_date": end_date,"workdays": workdays,"actual_days": (end_date - current_date).days + 1 }) current_date = end_date + timedelta(days=1)return timeline# 使用示例tasks = [ {"name": "需求分析", "workdays": 5}, {"name": "系统设计", "workdays": 10}, {"name": "开发实现", "workdays": 20}, {"name": "测试验收", "workdays": 10}]# 假设今天是项目开始日project_start = date.today()timeline = create_project_timeline(project_start, tasks)print("项目时间线:")for item in timeline: print(f"{item['task']}:") print(f" 开始: {item['start_date'].strftime('%Y-%m-%d')}") print(f" 结束: {item['end_date'].strftime('%Y-%m-%d')}") print(f" 工作日: {item['workdays']}天") print(f" 实际跨度: {item['actual_days']}天") print()
from datetime import datetimefrom zoneinfo import ZoneInfoimport jsonclassLogTimestampProcessor:"""日志时间戳处理器"""def__init__(self, default_timezone="UTC"): self.default_timezone = default_timezonedefparse_log_timestamp(self, timestamp_str, source_timezone=None):"""解析日志时间戳"""# 尝试常见格式 formats = [ ("%Y-%m-%dT%H:%M:%S.%f%z", True), # ISO带时区 ("%Y-%m-%dT%H:%M:%S%z", True), # ISO带时区(无微秒) ("%Y-%m-%d %H:%M:%S.%f", False), # 带微秒无时区 ("%Y-%m-%d %H:%M:%S", False), # 标准格式无时区 ("%d/%b/%Y:%H:%M:%S %z", True), # Nginx格式 ("%a %b %d %H:%M:%S %Y", False), # C标准格式 ]for fmt, has_tz in formats:try: dt = datetime.strptime(timestamp_str, fmt)# 如果没有时区信息,根据参数添加ifnot has_tz and source_timezone: dt = dt.replace(tzinfo=ZoneInfo(source_timezone))elifnot has_tz: dt = dt.replace(tzinfo=ZoneInfo(self.default_timezone))return dtexcept ValueError:continueraise ValueError(f"无法解析的时间戳格式: {timestamp_str}")defnormalize_to_utc(self, dt):"""统一转换为UTC时间"""if dt.tzinfo isNone: dt = dt.replace(tzinfo=ZoneInfo(self.default_timezone))return dt.astimezone(ZoneInfo("UTC"))defformat_for_storage(self, dt):"""格式化为存储格式""" utc_dt = self.normalize_to_utc(dt)return {"iso_utc": utc_dt.isoformat(),"timestamp": utc_dt.timestamp(),"local_display": dt.strftime("%Y-%m-%d %H:%M:%S %Z") }# 使用示例processor = LogTimestampProcessor(default_timezone="Asia/Shanghai")# 模拟不同来源的日志logs = ["2024-12-25T14:30:45.123456+08:00", # ISO带时区"2024-12-25 14:30:45", # 无时区"25/Dec/2024:14:30:45 +0800", # Nginx格式]print("日志时间戳处理结果:")for log in logs:try: parsed = processor.parse_log_timestamp(log) normalized = processor.normalize_to_utc(parsed) formatted = processor.format_for_storage(parsed) print(f"\n原始: {log}") print(f"解析: {parsed}") print(f"UTC: {normalized}") print(f"存储格式: {json.dumps(formatted, indent=2, ensure_ascii=False)}")except ValueError as e: print(f"\n错误: {e}")
9.1 问题一:naive与aware对象混合运算from datetime import datetime, timezonenaive_dt = datetime(2024, 12, 25, 14, 30, 45)aware_dt = datetime(2024, 12, 25, 14, 30, 45, tzinfo=timezone.utc)# 会引发TypeErrorresult = aware_dt - naive_dt
# 方案1:统一转换为aware对象from datetime import datetime, timezonenaive_dt = datetime(2024, 12, 25, 14, 30, 45)aware_dt = datetime(2024, 12, 25, 14, 30, 45, tzinfo=timezone.utc)# 将naive对象转换为aware对象(假设为UTC)naive_as_aware = naive_dt.replace(tzinfo=timezone.utc)result = aware_dt - naive_as_aware# 方案2:统一转换为naive对象(不推荐,会丢失时区信息)aware_as_naive = aware_dt.replace(tzinfo=None)result = aware_as_naive - naive_dt
from datetime import datetimeimport pytz # 旧版方法,容易出错# 错误:直接使用replace添加时区dt = datetime(2024, 3, 31, 2, 30, 0)paris_time = dt.replace(tzinfo=pytz.timezone("Europe/Paris"))# 结果可能不正确
from datetime import datetimefrom zoneinfo import ZoneInfo# 方法1:创建时直接指定时区dt = datetime(2024, 3, 31, 2, 30, 0, tzinfo=ZoneInfo("Europe/Paris"))# 方法2:使用zoneinfo(Python 3.9+推荐)dt = datetime(2024, 3, 31, 2, 30, 0)paris_time = dt.replace(tzinfo=ZoneInfo("Europe/Paris"))
# Y大(大写)表示年份完整,y小(小写)表示年份简写"%Y-%m-%d"# 2024-12-25 (正确)"%y-%m-%d"# 24-12-25 (容易混淆)# m是month(月份),M是Minute(分钟)"%Y-%m-%d %H:%M:%S"# 2024-12-25 14:30:45# H是Hour(24小时制),I是I(看起来像1,12小时制需要搭配%p)"%H:%M:%S"# 14:30:45"%I:%M:%S %p"# 02:30:45 PM
import timefrom datetime import datetime# 混淆秒和毫秒timestamp_ms = 1735137045123# 毫秒时间戳dt_wrong = datetime.fromtimestamp(timestamp_ms) # 错误!会解析为秒# 正确做法dt_correct = datetime.fromtimestamp(timestamp_ms / 1000.0)
defsafe_timestamp_conversion(timestamp, unit='seconds'):"""安全时间戳转换"""if unit == 'milliseconds': timestamp = timestamp / 1000.0elif unit == 'microseconds': timestamp = timestamp / 1000000.0return datetime.fromtimestamp(timestamp)# 使用时明确指定单位dt = safe_timestamp_conversion(1735137045123, unit='milliseconds')
通过今天的深度解析,我们系统性地掌握了datetime模块的完整知识体系:- 基础类:date、time、datetime、timedelta的核心用法
- 时区处理:naive与aware对象的区别,zoneinfo模块的正确使用
- 格式化解析:strftime与strptime的灵活运用
- 实战场景:跨时区会议、工作日计算、日志处理等真实业务需求
想要在时间处理领域达到专家水平?建议继续深入学习:- pandas.Timestamp:数据分析场景的最佳选择
- MySQL的DATETIME与TIMESTAMP区别
真正掌握datetime模块需要实践,建议完成以下项目:
时间处理是软件开发中的基础能力,但往往也是最容易被忽视的部分。一个健壮的系统,必然有严谨的时间处理逻辑。- 始终使用时区感知对象:避免naive对象在实际业务中使用
- 统一存储为UTC时间:在存储和传输层使用UTC,仅在展示层转换时区
- 明确时间单位:在使用时间戳时,始终明确是秒、毫秒还是微秒
掌握这些原则,你就能写出更加健壮、可靠、可维护的代码。时间处理不再是你的痛点,而是你代码质量的亮点!
下一篇预告:明天我们将深入探索json模块,学习如何高效、安全地处理JSON数据,掌握序列化复杂对象、性能优化和安全防护的完整技能体系。敬请期待!
本文为"Python与AI智能研习社"公众号原创文章,转载请注明出处。关注公众号,获取更多Python技术干货!