ARTICLE DETAIL

资讯详情

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

Python datetime模块详解:核心类、时区与实用技巧

Python datetime模块详解:核心类、时区与实用技巧 日期时间在处理爬虫数据、交易日历、定时报表的时候几乎每天都离不开datetime模块。它看着简单但真正写起来光“取当前时间”就有好几种写法时区更是能让你半夜加班。这篇博客会从datetime的核心类讲起把常用操作和实际业务场景揉在一起穿插我踩过的坑和处理办法帮你一次补齐这块拼图。无论你是刚通过 Python 安装教程入门的初学者还是已经在用 pandas 做数据的开发者应该都能找到对自己有用的细节。1. 先认识 datetime 模块的核心类1.1 五个核心类各自扮演什么角色初学 Python 时很多人会把datetime理解成一个单一类型其实它是一个模块里面装着date、time、datetime、timedelta、tzinfo五个核心类。名字看起来像复制粘贴但分工非常明确date只有年、月、日比如2025-07-20不关心这一天里的哪一秒。time只有时、分、秒、微秒不关心是哪一天。datetime日期和时间合体后的“完整瞬间”日常最常打交道的就是它。timedelta表示两个时间点之间的间隔比如 3 天 5 小时 30 分钟。tzinfo时区信息它更像一个抽象接口我们实际使用时通常通过zoneinfo.ZoneInfo或第三方库来实现。用生活化类比来理解date是日历上的那一天time是钟表上的某一刻datetime是“在 2025 年 7 月 20 日下午 14:30 那一瞬间”的完整表述timedelta是“距离那个瞬间还有多久”tzinfo则是“那一瞬间是在哪个时区被人看见的”。这里有一个容易忽略的细节datetime是date的子类。也就是说凡是能接受date对象的地方通常也能接受datetime对象但反过来不行。在做函数接口设计时如果你只关心日期就不要把datetime当参数类型过度收窄否则调用者容易在你这里收到类型错误。实际项目中最常用的还是datetime.datetime和datetime.timedelta。数据入库、导出报表、定时任务的逻辑基本都是围着这两个类转。单独的date和time更多用在明确“只关心生日”“只关心闹钟时间”这种特殊业务里字段含义会更清晰。1.2 naive 与 aware理解时区的第一步datetime对象还有一个容易被初学者忽视的分裂属性naive朴素时间和 aware感知时间。naive 对象只记录年、月、日、时、分、秒不记录时区aware 对象则携带tzinfo知道自己是哪个时区。为什么要这么分两个 naive 对象相减严格按照墙上时钟的数字来算也就是“本地钟表时间差”。比如23:00减07:00得到 16 小时但它没有回答“这两个 7 点和 23 点分别是在哪国看到的”。两个 aware 对象相减会先换算成 UTC 再计算间隔结果才代表真实的物理时间流逝。我在一个跨地区协作项目里踩过这个坑。研发同事在北京运营同事在海外大家把时间往数据库里一存都以为是“世界统一时间”。结果脚本统计某个请求的耗时算出来经常是负数。后来定位到原因海外机器上的datetime.now()是服务器本地时区北京机器上的也是本地时区两边 naive 相减完全没对齐。新手可以先记住一条原则在写入数据库、做时间计算前先统一时区口径要么所有对象都带tzinfo要么明确“这是某时区的本地时间”并尽早完成转换。第四章我会专门展开讲。2. 上手必会日期时间的获取、解析与格式化2.1 获取“现在”并注意 utcnow 的坑获取当前时间是入门第一课常见写法有这几种from datetime import datetime now datetime.now() # 本机当前时间naive today datetime.today() # 和 now() 基本一致 utc_now datetime.utcnow() # 老代码里常见但不推荐如果你用的 Python 版本在 3.11 及以上调用datetime.utcnow()时解释器会标记 deprecation warning。原因是utcnow()返回的是 naive 的 UTC 时间它把“UTC 时区的墙上时间”当成了“没有时区的时间”。一旦有人把这个值拿去和本地时间做比较结果就会错得莫名其妙。更安全的替代写法from datetime import datetime, timezone utc_now datetime.now(timezone.utc) # aware 的 UTC 时间 local_now datetime.now().astimezone() # 带本机时区的 aware 时间astimezone()在不传参数时会把当前时间转换到系统所在时区并自动挂上时区信息。这个操作比我以前手动 “取 UTC 再 8 小时” 要安全得多因为系统时区会自动根据服务器配置调整遇到夏令时地区也能少踩几个坑。在纯 Python 代码里我一般不会用now()去反复比较“本地时间”而是统一取 UTC 再转换。如果是在无人值守的服务器上系统时区可能压根不是业务时区取now()容易让日志时间看起来对不上直接用datetime.now(ZoneInfo(Asia/Shanghai))反而更直观。2.2 字符串和 datetime 互转常用格式代码开发过程中时间对象最终要变成字符串给人看或者反过来从字符串里解析成对象。这两个操作对应strftime与strptimefrom datetime import datetime dt datetime(2025, 7, 20, 14, 30, 0) # 对象转字符串 s dt.strftime(%Y-%m-%d %H:%M:%S) # 2025-07-20 14:30:00 # 字符串转对象 parsed datetime.strptime(s, %Y-%m-%d %H:%M:%S)常用格式占位符贴一张表建议收藏占位符含义示例%Y四位数年份2025%m两位月份07%d两位日期20%H24 小时制小时14%M分钟30%S秒00%f微秒000000%zUTC 偏移量0800%Z时区名称CST%A星期全称Sunday%w星期数字0 为周日0%j一年中的第几天201最容易翻车的地方是解析时“看起来像实际不匹配”。比如2025-7-20用%Y-%m-%d解析会直接 ValueError因为月份没有补零2025-07-20 14:30用%Y-%m-%d %H:%M:%S解析也会报错因为缺少秒字段。我的处理习惯是对外接口和数据库存储统一用 ISO 格式%Y-%m-%dT%H:%M:%S展示给用户时才转成中文或本地样式。这样双方解析都省心。Python 3.11 以后datetime.fromisoformat()可以直接解析带时区偏移的 ISO 字符串速度还更快。如果你遇到一堆乱七八糟的字符串比如July 20, 2025、2025/07/20、2025年7月20日直接用dateutil.parser.parse可以少写十几套格式。它牺牲了一点性能换来的是极佳的容错性。我爬虫处理第三方数据时基本用它做第一层解析能过 90% 以上的情况。2.3 timedelta 加减法让日期自动跑起来日期计算最核心的工具是timedeltafrom datetime import datetime, timedelta now datetime(2025, 7, 20, 18, 0, 0) tomorrow now timedelta(days1) last_week now - timedelta(weeks1) in_3_hours now timedelta(hours3)timedelta支持天、秒、微秒、毫秒、分钟、小时、周可以组合传入使用起来非常直观。但有几个点需要提醒datetime与timedelta相加得到datetimedatetime与datetime相减得到timedeltatimedelta不接受months参数因为每月的天数不固定Python 故意不做这个设计。要计算“三个月后”或“月末”需要自己包一层函数。比如计算当月最后一天from datetime import datetime import calendar def last_day_of_month(dt): year, month dt.year, dt.month return datetime(year, month, calendar.monthrange(year, month)[1])calendar.monthrange(year, month)返回一个元组第一个值是当月第一天是星期几第二个值是当月天数。用第二个值就能组装出月末日期。再比如判断两个时间是否属于同一天a datetime(2025, 7, 20, 23, 59, 59) b datetime(2025, 7, 21, 0, 0, 0) print(a.date() b.date()) # False直接用datetime对象进行比较时间不一致就会误判先取.date()再比才是“同一天”的判断。类似这种边界细节写报表和自动任务时会频繁遇到。3. 在真实业务里使用 datetime 的几种典型场景3.1 爬虫数据的时间清洗与窗口去重写爬虫时抓下来的数据多半带时间戳可格式五花八门。有2025-07-20 14:30:00也有2025/07/20还有英文的Jul 20, 2025。第一步不是直接往数据库塞而是把时间统一成可比较的格式。我通常这样清洗from datetime import datetime from dateutil import parser raw July 20, 2025 02:30PM dt parser.parse(raw) normalized dt.strftime(%Y-%m-%d %H:%M:%S)dateutil.parser.parse是第三方库我做了大量爬虫后越发觉得它好使。它不需要你手写复杂格式就能识别大多数常见写法。但它也有猜错的风险遇到歧义格式还是建议显式指定格式。另一个经典问题是窗口去重。比如每天抓一次目标页面只保留当天发布的新闻。我可以先把发布日期解析成datetime再与“上次成功抓取时间”比较from datetime import datetime last_success datetime(2025, 7, 19, 12, 0, 0) new_item_time parser.parse(2025-07-20 09:30) if new_item_time last_success: # 是新增内容入库并更新 last_success pass注意这种比较必须保证两侧时区一致。如果来源是海外站点务必先转成自己定义的统一时区。比如抓到的时间是纽约时间我一般会先用ZoneInfo(America/New_York)给它贴标签再astimezone(timezone.utc)转成 UTC最后才和数据库里的时间比较。3.2 量化交易中的时间序列基础量化交易几乎离不开时间。K 线周期、回测区间、指标计算、信号触发全部要处理时间轴。pandas的DatetimeIndex就是建立在 Pythondatetime语义之上的时间序列索引。拿到行情数据后第一件事通常是让 pandas 把字符串解析成时间戳import pandas as pd df pd.read_csv(ticks.csv, parse_dates[timestamp]) df.set_index(timestamp, inplaceTrue)parse_datesTrue背后用的就是 datetime 相关解析能力。之后很多判断都依赖DatetimeIndex的字段属性。比如只保留 9:30 到 10:30 的分钟数据mask ( (df.index.hour 9) (df.index.minute 30) | (df.index.hour 10) (df.index.minute 30) ) morning df[mask]按周期聚合时resample也依赖时间索引weekly df[close].resample(W).last()写出前 30 天内涨停过的股票名单也可以用timedelta做时间过滤cutoff datetime.now() - timedelta(days30) recent_limit_up df[df[limit_up_time] cutoff]做策略回测时尤其要注意 naive 与 aware 的混用。pandas的Timestamp有自己的时区机制如果用原生datetime去和DatetimeIndex里的时间做比较有时会触发 “cant compare offset-naive and offset-aware datetimes” 的报错。所以我一般会在数据入口处统一成一个时区再往下做计算。3.3 自动报表与数据库查询中的日期槽自动化办公最常见的需求是每天凌晨跑一个脚本把昨天的数据拉出来生成 Excel 报表。datetime在这里要负责两件事计算窗口边界、格式化成 SQL 参数或文件名。from datetime import datetime, timedelta today datetime.now().date() yesterday today - timedelta(days1) start_time datetime.combine(yesterday, datetime.min.time()) end_time datetime.combine(today, datetime.min.time())datetime.combine(date, time)可以把日期和时间拼成一个datetime。用datetime.min.time()表示 00:00:00能很清爽地构造出“某天零点”。查询 Oracle 或 MySQL 时我把 SQL 写成时间区间sql SELECT order_id, create_time FROM orders WHERE create_time :start AND create_time :end params {start: start_time, end: end_time}我偏爱 start 且 end写法因为“某天 23:59:59”其实很难取准用“小于次日零点”则天然包含当天最后一刻也不会和明天数据重叠。导出 Excel 时文件名和展示字段也得靠strftimefilename f订单报表_{yesterday.strftime(%Y%m%d)}.xlsx display_text end_time.strftime(%Y年%m月%d日)文件名里的日期我尽量用%Y%m%d而不是%Y-%m-%d因为短横线在某些上传控件的文件名拼接里可能被误解析生成日志文件时也更清爽。3.4 定时脚本里的“每周几”判断不引入 APScheduler 或 Celery 时我也常直接在脚本开头写时间判断from datetime import datetime import calendar now datetime.now() if now.weekday() 4: # 0周一4周五 do_weekly_cleanup() if now.day calendar.monthrange(now.year, now.month)[1]: do_monthly_summary()weekday()返回 0-60 是周一isoweekday()返回 1-71 是周一。我个人更爱isoweekday()因为“周五 5”比“周五 4”更直观代码读起来不容易混淆。有节假日需求时本地放一个集合存放假期日期holidays {date(2025, 10, 1), date(2025, 10, 2)} if now.date() not in holidays and now.isoweekday() 5: run_market_open_tasks()这种做法的好处是纯函数、可测试、不依赖外部调度服务。缺点是脚本必须依赖操作系统的计划任务来唤醒但一旦涉及多机分布式调度那已经是另一个领域了。4. 时区问题最容易翻车的地方4.1 为什么时区无知会害了你看一个让人后背发凉的例子from datetime import datetime appointment datetime(2025, 7, 20, 9, 0, 0) return_time datetime(2025, 7, 20, 17, 0, 0) print(return_time - appointment) # 8:00:00这看似没问题。但如果第一个时间是北京早上 9 点第二个时间是纽约傍晚 5 点真实间隔就完全不同了。naive 对象把所有时间都当成同一个墙钟时间一旦系统分布在多个时区偏差立刻会出现。我自己线上出过一次小事故脚本在东京的服务器跑数据库在北京我用datetime.now()去和 UTC 基准时间比较结果差了一个小时导致凌晨报表少了一批数据。查了很久才发现now()返回的是服务器本地时间完全没有时区信息。所以凡是涉及多地区时间或者需要跨系统比较都建议一律使用 aware 对象。如果团队规约统一“数据库存 UTC”那就在入库前转成 UTC展示时再转成本地时间。4.2 用 zoneinfo 做东八区时间的正确姿势Python 3.9 之后标准库里加入了zoneinfo不需要再为时区单独安装pytz。东八区的时间名是Asia/Shanghaifrom datetime import datetime, timezone from zoneinfo import ZoneInfo cn ZoneInfo(Asia/Shanghai) local_now datetime.now(cn) utc_now local_now.astimezone(timezone.utc)如果从数据库读出来的是一个 naive 的“北京时间字符串”需要先给它“贴上东八区的标签”再转成 UTCnaive_time datetime(2025, 7, 20, 9, 0, 0) aware_local naive_time.replace(tzinfoZoneInfo(Asia/Shanghai)) utc_value aware_local.astimezone(timezone.utc)注意replace(tzinfo...)只是给一个墙上时间挂上时区信息不改变数值astimezone()才是真正的时间换算会把墙上时间调整为对应时区的数字。这两步非常容易混我见过不少同事在replace和astimezone之间选错方向。实际团队开发时我会把允许使用的时区名统一写进配置例如只允许Asia/Shanghai和UTC。因为时区字符串一旦放开业务代码里就会出现America/New_York、Europe/London等一堆变体排查问题时非常痛苦。4.3 夏令时看似存在又突然消失的 1 小时如果你只做国内业务夏令时可以基本不管因为中国已经多年不实行夏令时。但只要对接欧美市场、做跨境业务或者处理云服务账单你就一定会遇到夏令时。夏令时最典型的特征是时间跳变。每年春季北美部分地区凌晨 2 点会直接跳到 3 点这一小时的datetime根本不存在秋季又会多出一个 2 点同一个墙钟时间对应两个 UTC 时刻。用zoneinfo构造这种时刻时要非常小心from zoneinfo import ZoneInfo from datetime import datetime ny ZoneInfo(America/New_York) try: dt datetime(2025, 3, 9, 2, 30, 0, tzinfony) print(dt) except Exception as e: print(这个时间不存在, e)有些操作系统时区库会默默把这个 2:30 映射到 3:30并不会报错。如果你在写日终报表或轮询任务建议把凌晨业务统一放到 UTC 环境下计算展示时再转成当地时区这样可以绕开大多数夏令时难题。5. 高频报错与排查速查表5.1 ValueError格式对不上怎么办写三个月代码你会遇到最多的是ValueError: time data ... does not match format。这个报错核心就是“你的字符串和格式串对不上”。排查顺序很固定用print或者repr把原始字符串原样打出来看有没有隐藏的前后空格、制表符、换行。对照格式表逐项检查年份、月份、日期占位符有没有写反。能解析 ISO 字符串时直接用datetime.fromisoformat()别自己拼strptime格式。遇到微秒字段%f必须放在最后并且和%S之间不要加空格。顺手给一个排查小工具raw 2025-07-20 14:30 fmt %Y-%m-%d %H:%M:%S try: datetime.strptime(raw, fmt) except ValueError as e: print(解析失败实际字符串, repr(raw)) print(我用格式, fmt)repr(raw)会把字符串里的空格和制表符显示出来很多肉眼看不见的问题一下就暴露了。5.2 TypeErrornaive 和 aware 乱加如另一个高频报错是TypeError: cant subtract offset-naive and offset-aware datetimes原因很直接一边是 naive一边是 awarePython 拒绝直接计算。解决办法不是删掉时区信息而是把它们统一到同一频率。如果确实要得到 naive 对象可以这样aware datetime.now(timezone.utc) naive aware.replace(tzinfoNone)但我不建议把这个当常规武器。更好的办法是给 naive 对象补上时区标签from zoneinfo import ZoneInfo local ZoneInfo(Asia/Shanghai) naive_local datetime(2025, 7, 20, 9, 0, 0) aware_local naive_local.replace(tzinfolocal) result aware_local - another_aware_time工程上我会在代码入口处定下规矩所有从数据库读出的时间都按 UTC 处理所有用户输入的时间先转成 Python aware 对象再进业务层。时间一旦带上时区标签这种 TypeError 几乎绝迹。5.3 大数据量下的解析性能优化datetime.strptime很方便但遇到百万行级数据时性能会很吃力。我在处理几百万行交易明细时测试过逐行strptime比 pandas 向量化慢二十倍不止。首选方案是让 pandas 批量解析import pandas as pd df pd.read_csv(records.csv, parse_dates[created_at]) # 如果列已是字符串 df[created_at] pd.to_datetime(df[created_at])pd.to_datetime底层也是在调用时间解析但它用 C 级循环批量处理性能高得多。小数据量无所谓大数据量别在 Python 层写 for 循环。如果坚持只用标准库也有两个技巧一是用datetime.fromisoformat而不是strptime因为少了动态解析速度快一些二是对重复值做缓存cache {} def fast_parse(s): if s not in cache: cache[s] datetime.fromisoformat(s) return cache[s]这种缓存适合日志数据里日期重复率高的情况比如一堆记录都是同一个天。如果每天字符串天差地别缓存收益有限。6. 我的个人经验处理日期时间的“内功心法”工作做得越久越觉得datetime的麻烦不是“不会用”而是“没想清楚”。这一路踩坑我沉淀出来几条自己的规则。第一存储用 UTC展示用本地。所有数据库字段、日志时间戳、消息队列里的时间优先使用 UTC 意识时间只在用户界面、报表输出时才转换成本地时区。这条规则能一次摁灭绝大多数跨时区问题。第二尽量不手工拼格式。面向接口和数据库统一用 ISO 8601也就是YYYY-MM-DDTHH:MM:SS。Python 3.11 以后fromisoformat可以直接读你在 pandas、JavaScript、Java 里也都能自动解析。展示需要中文时再转一次不要在链路里混用多种格式。第三边界就是魔鬼。统计“昨天”不要写成now() - timedelta(days1)而要先now().date()退到当天零点再算窗口判断是否同一天先调.date()算月末别靠记天数用calendar.monthrange。最后分享一个小技巧如果你要把datetime作为字典键或放进集合里尽量用 aware 时间而且最好统一成 UTC。naive 时间经常让两个“看起来不同”的时刻在换算后变成同一个值触发奇怪的幂等冲突。我写缓存时都是先统一成 UTC 再存后来踩坑率明显变低。datetime不是一个一眼能看透的小模块但只要肯花半小时把格式、时区、边界这三关打通后面写脚本会稳得多。这篇里的代码我基本都亲手跑过踩过的坑也如实写了希望能帮你省下一些深夜排查的时间。
返回列表