ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python时间处理实战:time与datetime模块核心用法及避坑指南

Python时间处理实战:time与datetime模块核心用法及避坑指南 如果你写过一段时间 Python大概率会有这种感觉明明每天都在用time和datetime真到封装一个时间处理函数、算一个上周一的日期、或者统计一段代码的耗时的时候反而开始手忙脚乱。尤其是两个模块功能重叠接口长得又像time.time()、time.localtime()、datetime.now()、datetime.timestamp()混在一起脑子稍微不清醒写出的是能跑的代码跑出来的结果却总差那么八小时。这篇文章就是一次完整的复习整理。我尽量不说教科书废话直接对标日常开发里真正用得到的功能点时间戳怎么来、格式化怎么写、日期怎么加减、时区怎么处理、性能计时用什么最准最后还会把我这些年踩过的坑挨个列出来。适合刚学完基础语法、准备系统写工具的初学者也适合用了一段时间但老要翻文档的人。1. 先搞清楚两个模块的分工该用谁不由心情决定很多新手纠结time和datetime哪个更好用其实这俩根本不是一个层面的东西谈不上谁替代谁。理解它们的分工比背 API 重要得多。1.1 time 模块贴近操作系统的底层时钟time模块可以理解成 Python 对操作系统时间接口的封装。它最核心的概念是“时间戳”也就是从 1970年1月1日 00:00:00 UTC 到现在经过的秒数一个浮点数。这个数字没有时区概念是全球统一的时间基准所以它非常适合做时间差的数学计算、数据存储和跨系统传输。time模块内部管理着三类时间表示时间戳float、结构化时间struct_time、格式化字符串str。它提供的函数基本都是这三者之间的转换工具。它的另一个重要职责是sleep让程序暂停若干秒这个也是datetime做不到的。所以当你的需求是“算耗时”“让程序停一停”“拿到一个纯粹的秒数”时直接用time不需要引入datetime。1.2 datetime 模块面向业务的日期管家datetime模块则是建立在时间戳之上的面向对象封装它把时间拆成year、month、day、hour、minute、second、microsecond这些人类可读的属性并提供date、time、datetime、timedelta、timezone五个核心类。datetime适合什么场景比如“算出 30 天后的日期”“判断今天是星期几”“把用户输入的2025-06-01解析成日期对象然后比较大小”。这些操作如果让你拿时间戳硬算会很痛苦因为你要自己处理每个月有多少天、闰年怎么算。datetime把这些逻辑都内化了日期加减直接用timedelta对比直接用运算符代码读起来跟自然语言差不多。一句话总结跟时间撮打交道用time跟日历和业务日期打交道用datetime。两个模块在转换处有接口互相衔接后面会详细说。2. time 模块核心点别只记住 time() 和 sleep()time模块函数不算多但每个都值得认真理解。我按使用频率从高到低拆开讲。2.1 三种时间形态的互相转换time模块的三种时间形态分别是时间戳floattime.time()返回当前时间戳结构化时间time.struct_time一个包含年月日时分秒等 9 个字段的命名元组time.localtime()返回本地时区的结构化时间time.gmtime()返回 UTC 时区的结构化时间格式化字符串time.strftime(format, struct_time)把结构化时间变成字符串time.strptime(string, format)把字符串解析成结构化时间日常最常用的两个组合是import time # 时间戳转字符串 timestamp time.time() struct_time time.localtime(timestamp) str_time time.strftime(%Y-%m-%d %H:%M:%S, struct_time) print(str_time) # 字符串转时间戳 parsed time.strptime(2025-06-01 12:00:00, %Y-%m-%d %H:%M:%S) timestamp_back time.mktime(parsed) print(timestamp_back)这里特别要注意time.localtime()不传参数时默认使用当前时间戳time.strftime()如果不传第二个参数默认也是当前时间。这不一定是坏事但新手容易误以为格式化跟本地时间无关。第二个要注意的坑是time.mktime()。它接收的是本地时区的结构化时间如果你把一个 UTC 的 struct_time 拿给它转时间戳就会多出时区偏移的误差。反过来time.gmtime()把时间戳转成 UTC 的结构化时间。2.2 沉睡的精确度问题time.sleep(seconds)是暂停控制的主要手段但它的实际休眠时间并不严格等于你传入的秒数。原因在于操作系统的时间片调度sleep的精度一般在毫秒到几十毫秒级别而且一旦被信号中断还可能提前返回。如果需要高精度的延迟比如控制硬件或者做动画帧率限制可以配合time.perf_counter()做补偿循环import time def precise_delay(seconds): start time.perf_counter() while time.perf_counter() - start seconds: pass这个写法在普通业务里不需要但在某些对时序敏感的采集程序里能救命。记得不要在生产环境的普通逻辑里用空转循环会占满 CPU可以配合time.sleep(0.001)降低空转频率。2.3 性能计时perf_counter 才是正解统计一段代码跑了多久最直观的写法是start time.time() # do something end time.time() print(end - start)这个写法能跑但不够严谨。time.time()返回的是墙上时钟时间wall clock time它会受到系统时间调整的影响。比如在计时过程中系统自动校准了时间或者你手动修改了系统时间开始和结束的差值就会异常。另外在部分操作系统上time.time()的精度有限对于毫秒以下级别的耗时就无能为力。更可靠的是time.perf_counter()它专门用于测量短时间间隔精度极高并且不受系统时间调整影响返回的是一个单调递增的计数器。正确写法import time start time.perf_counter() # do something, e.g. 计算列表求和 sum(range(1000000)) end time.perf_counter() print(f耗时: {end - start:.6f} 秒)我见过不少性能优化场景本来要对比两种算法的耗时差异结果因为用了time.time()数据噪声很大最后换到perf_counter才得到稳定结论。建议所有秒级以下的性能测试统一使用perf_counter。3. datetime 模块实战核心类与日期运算datetime模块的类设计非常贴近生活学起来不难但有几个细节特别值得展开讲。3.1 五个类的关系一张表讲明白类名用途示例datetime.date日期年月日date(2025, 6, 1)datetime.time时间时分秒微秒无日期time(14, 30, 0)datetime.datetime日期 时间datetime(2025, 6, 1, 14, 30, 0)datetime.timedelta时间差天、秒、微秒timedelta(days7)datetime.timezone时区对象timezone(timedelta(hours8))五个类里datetime.datetime是用得最多的因为它同时包含日期和时间可以直接从时间戳转换也可以拆出date和time对象。date对象可以与datetime对象直接比较大小因为datetime是date的子类。创建时间对象时注意datetime.time和datetime.timezone同名但完全是两码事前者表示“几点几分”后者表示“时区偏移”。导入时建议写成from datetime import datetime, timedelta, timezone避免名称冲突。3.2 timedelta 运算日期加减的核心玩法timedelta是datetime模块里最强大的工具之一。你可以直接在datetime或date对象上加减它from datetime import datetime, timedelta now datetime.now() print(当前时间:, now) # 加 3 天 later now timedelta(days3) print(3天后:, later) # 减 1 周 last_week now - timedelta(weeks1) print(一周前:, last_week) # 加 2小时30分 delta timedelta(hours2, minutes30) print(2小时30分后:, now delta)两个datetime对象相减得到timedelta反之timedelta不能单独存在必须挂在某个日期上进行运算。timedelta的构造函数接受days、seconds、microseconds、milliseconds、minutes、hours、weeks七个参数。它内部会统一换算成days seconds microseconds存储所以打印时看到的可能是1 day, 2:03:00这种格式。有个容易踩的细节timedelta(days1)在datetime上加减会同时处理跨月和跨年的情况。也就是说1月31日加1天会正确变成2月1日不需要你手动判断当月天数。这个能力在处理月末排期时特别有用。3.3 格式化输出与字符串解析datetime对象的格式化同样使用strftime和strptime只不过调用方式变成了对象方法from datetime import datetime now datetime.now() print(now.strftime(%Y-%m-%d %H:%M:%S)) # 2025-06-01 14:30:00 print(now.strftime(%A, %B %d, %Y)) # Sunday, June 01, 2025 print(now.strftime(%j)) # 年第几天如 152 parsed datetime.strptime(2025/06/01 14:30, %Y/%m/%d %H:%M) print(parsed)常用格式符对应关系整理成表放在手边备查格式符含义示例%Y四位年份2025%y两位年份25%m两位月份06%d两位日期01%H24小时制小时14%I12小时制小时02%M分钟30%S秒00%f微秒6位123456%A完整星期名Sunday%a缩写星期名Sun%B完整月份名June%b缩写月份名Jun%j年内的第几天152%w星期几0为周日0strptime解析字符串时格式必须和字符串严格匹配包括分隔符。多一个空格、少一个前导零都会报错。比如2025-6-1用%Y-%m-%d解析会失败因为%m和%d默认期望两位数。解决办法是用%-m和%-d仅 Linux/macOS 支持或者干脆用dateutil库的parser.parse()做宽松解析。4. 高频场景实操把复习变成能用的代码光看文档记不住我把日常开发里出现频率最高的几个场景拉出来直接给可复制的代码。4.1 时间戳与 datetime 互转的完整映射datetime与time模块之间的核心桥梁是datetime.timestamp()方法和datetime.fromtimestamp()类方法from datetime import datetime, timezone import time # datetime - 时间戳 dt datetime(2025, 6, 1, 12, 0, 0) ts dt.timestamp() print(ts) # 时间戳 - datetime本地时区 dt_local datetime.fromtimestamp(ts) print(dt_local) # 时间戳 - datetimeUTC 时区 dt_utc datetime.fromtimestamp(ts, tztimezone.utc) print(dt_utc) # 当前时间 now_dt datetime.now() now_ts time.time()datetime.now()得到的是本地时区的 naive datetime不带时区信息datetime.utcnow()已不推荐使用官方建议直接用datetime.now(timezone.utc)获取带时区的 UTC 时间。这样做的好处是后续如果要把数据存进数据库或者做跨时区计算不会因为缺失时区信息而出错。4.2 日志文件名与任务调度格式化实例两则日志文件的命名是strftime的经典应用场景。按天切分或按小时切分时通常直接这样写from datetime import datetime import os log_filename datetime.now().strftime(app_%Y%m%d_%H.log) log_path os.path.join(logs, log_filename) print(log_path) # logs/app_20250601_14.log任务调度则经常用到“某个时间点之后”的逻辑比如定时任务的抓取窗口from datetime import datetime, timedelta def is_in_window(nowNone): now now or datetime.now() start now.replace(hour0, minute0, second0, microsecond0) end start timedelta(days1) return start now end print(is_in_window())这里用到了datetime.replace()方法它可以把对象的某个字段替换成指定值返回新对象而不修改原对象。这是个非常实用的小技巧比手动构造datetime(year, month, day, 0, 0, 0)要简洁得多也避免了时区或闰日的计算疏漏。4.3 月末日期与上个月日期平移的正确姿势业务中经常要算“上个月最后一天”或“本月第几个周五”。如果你用天数硬减很容易被大月小月坑到。正确姿势是用calendar模块配合datetimefrom datetime import datetime, date import calendar def last_day_of_prev_month(nowNone): now now or datetime.now() first_of_this_month now.replace(day1) last_of_prev_month first_of_this_month - timedelta(days1) return last_of_prev_month.date() print(last_day_of_prev_month()) # 如果是2025-06-15返回2025-05-31另一个高频需求是“算上一个工作日”。这个逻辑比较简单粗暴如果是周一就回退3天其余情况回退1天然后判断是否法定节假日。法定节假日需要接节假日库这里只给到周末判断from datetime import datetime, timedelta def previous_workday(nowNone): now now or datetime.now() days_back 1 if now.weekday() 0: # 周一 days_back 3 return (now - timedelta(daysdays_back)).date() print(previous_workday())这里用到了weekday()它返回 0 代表周一6 代表周日和%w格式符0代表周日不一样容易混写判断时注意区分。4.4 时间差计算能否赶上 3 秒超时在做接口超时判断或任务延迟检测时标准做法是记录开始时间戳然后在关键节点比对差值from datetime import datetime, timedelta start datetime.now() # 模拟某操作 import time time.sleep(2.5) elapsed datetime.now() - start if elapsed timedelta(seconds3): print(超时, elapsed.total_seconds()) else: print(按时完成, elapsed.total_seconds())timedelta.total_seconds()返回浮点数是计算时间差的统一出口。注意elapsed.seconds属性只包含“秒”的部分不包含“天”如果时间差超过一天直接用.seconds会少算一天必须用.total_seconds()才准确。5. 常见坑点与排查经验这些年我踩过的雷时间处理代码看起来简单但出问题的时候往往很难排查因为错误可能只在特定时区、特定日期或者特定字符串格式下暴露。下面这些坑我一个一个都踩过。5.1 本地时间与 UTC 混淆八小时偏差的元凶最经典的坑就是本地时间和 UTC 时间相差八小时。常见出问题场景数据库里存的是字符串2025-06-01 12:00:00你用datetime.strptime解析后直接比较大小但这份数据其实是 UTC 时间。如果不先把字符串转成带时区的 UTC 时间再转成你的本地时间比较结果就会偏移八小时。排查建议所有系统中流转的时间要么统一用时间戳存储要么统一用 UTC 时间字符串并且字段名标注_utc后缀。展示层再转本地时间。5.2 strftime 与 strptime 的格式错配有一次我解析一个日志文件格式是2025-06-01 14:30:05,123也就是毫秒部分用逗号分隔。我拿着%Y-%m-%d %H:%M:%S,%f去解析结果总是报错。查了半天才发现%f期望的是 6 位微秒而我提供的是 3 位毫秒。解决方法是把毫秒先补成 6 位log_time 2025-06-01 14:30:05,123 log_time_normalized log_time.replace(,, .) 000 parsed datetime.strptime(log_time_normalized, %Y-%m-%d %H:%M:%S.%f) print(parsed)还有一次我用%Y%m%d去解析20250601完全没问题但如果字符串里面混进了全角字符或者 BOM 头就会莫名失败。遇到解析报错先打印字符串的repr()看是不是混入了看不见的字符。5.3 时间格式化踩坑%m 与 %M 的视觉陷阱%m是月份%M是分钟大小写差距只有一丁点但含义天差地别。我曾经在生成报表文件名时把%M写成了%m结果所有文件名都带上了当前月份而不是当前分钟导致一天的文件全部重名覆盖数据丢了一批。后来我给自己定了一条规矩脚本里所有时间格式化字符串写完之后先跑一行datetime.now().strftime(...)打印输出确认一下再投入使用。5.4 月末日期计算day0 技巧平时用timedelta做加减很方便但想快速拿到某个月的最后一天有个冷门又好用的写法让day参数传 0。Python 的datetime会把day0解释为“上个月的最后一天”。from datetime import datetime def last_day_of_month(year, month): if month 12: return datetime(year, 12, 31).date() else: return datetime(year, month 1, 1).date() - timedelta(days1)实际上更简洁的做法是利用calendar.monthrange(year, month)返回(首日星期几, 当月天数)直接取第二个值。两种方法都可以个人推荐calendar.monthrange可读性更好。5.5 性能计时别用 f-string 拼接浮点写完代码之后我发现用 f-string 打印perf_counter的差值时如果直接写f{end - start}会输出一长串小数比如0.002345678901234567。这不是 bug但观感很差。正确做法是加上格式控制print(f耗时 {end - start:.4f} 秒)另外不要在计时循环里混入所有无关的 IO 操作。perf_counter测的是时间间隔如果里面夹了网络请求、文件写入测出来的数据就不是纯算法耗时了分析性能时容易误判。5.6 日期比较aware 与 naive 不能混用如果两个datetime对象一个带时区aware一个不带时区naive直接比较大小会抛出TypeError: cant compare offset-naive and offset-aware datetimes。我自己在做全球时区订单统计时被这个报错卡住过。解决方法是统一转换成带时区的时间再比较from datetime import datetime, timezone dt_naive datetime(2025, 6, 1, 12, 0, 0) dt_aware datetime(2025, 6, 1, 12, 0, 0, tzinfotimezone.utc) # 统一为 aware dt_naive_utc dt_naive.replace(tzinfotimezone.utc) print(dt_naive_utc dt_aware) # 或统一为 naive直接用 timestamp 比较 print(dt_naive_utc.timestamp() dt_aware.timestamp())养成习惯业务数据入库前统一转成 UTC 的 aware datetime展示和运算全用同一个标准就能少踩一半的时区坑。6. 一张速查表日常编写时的提效锦囊我最后整理一张速查表覆盖time和datetime日常最高频的编码需求直接存下来当备忘录用比翻文档快。需求推荐写法当前时间戳time.time()当前时间 datetime本地datetime.now()当前时间 datetimeUTCdatetime.now(timezone.utc)时间戳转 datetime本地datetime.fromtimestamp(ts)时间戳转 datetimeUTCdatetime.fromtimestamp(ts, tztimezone.utc)datetime 转时间戳dt.timestamp()当前格式化字符串datetime.now().strftime(%Y-%m-%d %H:%M:%S)字符串解析datetime.strptime(s, %Y-%m-%d %H:%M:%S)日期加 N 天dt timedelta(daysN)上个月最后一天dt.replace(day1) - timedelta(days1)当月天数calendar.monthrange(year, month)[1]两个日期相差天数(d1 - d2).days高精度性能计时time.perf_counter()毫秒级延迟time.sleep(0.001)我个人在实际使用中形成的一个习惯是所有跨模块、跨函数的时间参数一律用时间戳传递只有展示和给用户看的时候才转成字符串或 datetime。这样做能最大程度避免时区污染和格式化出入也让整个程序的时间脉络更清晰。建议你从这个角度重新过一遍自己的代码也许能发现不少以前没注意到的隐性问题。最后再分享一个小技巧如果你经常要写时间相关的推导式或条件判断可以在项目里封装一个time_utils.py把上面这些高频操作截成函数统一维护格式符和时区设置。时间处理这种代码每个项目都写一遍也不难但集中管理之后遇到坑只用修一处绝对划算。
返回列表