news 2026/10/3 10:05:26

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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与strptime:

from 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 加减法:让日期自动跑起来

日期计算最核心的工具是timedelta:

from datetime import datetime, timedelta now = datetime(2025, 7, 20, 18, 0, 0) tomorrow = now + timedelta(days=1) last_week = now - timedelta(weeks=1) in_3_hours = now + timedelta(hours=3)

timedelta支持天、秒、微秒、毫秒、分钟、小时、周,可以组合传入,使用起来非常直观。但有几个点需要提醒:

  • datetime与timedelta相加得到datetime;
  • datetime与datetime相减得到timedelta;
  • timedelta不接受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", inplace=True)

parse_dates=True背后用的就是 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(days=30) recent_limit_up = df[df["limit_up_time"] >= cutoff]

做策略回测时,尤其要注意 naive 与 aware 的混用。pandas的Timestamp有自己的时区机制,如果用原生datetime去和DatetimeIndex里的时间做比较,有时会触发 “can't compare offset-naive and offset-aware datetimes” 的报错。所以我一般会在数据入口处统一成一个时区,再往下做计算。

3.3 自动报表与数据库查询中的日期槽

自动化办公最常见的需求是:每天凌晨跑一个脚本,把昨天的数据拉出来生成 Excel 报表。datetime在这里要负责两件事:计算窗口边界、格式化成 SQL 参数或文件名。

from datetime import datetime, timedelta today = datetime.now().date() yesterday = today - timedelta(days=1) 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 时,文件名和展示字段也得靠strftime:

filename = 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-6,0 是周一;isoweekday()返回 1-7,1 是周一。我个人更爱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/Shanghai:

from 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 的“北京时间字符串”,需要先给它“贴上东八区的标签”,再转成 UTC:

naive_time = datetime(2025, 7, 20, 9, 0, 0) aware_local = naive_time.replace(tzinfo=ZoneInfo("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, tzinfo=ny) print(dt) except Exception as e: print("这个时间不存在:", e)

有些操作系统时区库会默默把这个 2:30 映射到 3:30,并不会报错。如果你在写日终报表或轮询任务,建议把凌晨业务统一放到 UTC 环境下计算,展示时再转成当地时区,这样可以绕开大多数夏令时难题。

5. 高频报错与排查速查表

5.1 ValueError:格式对不上怎么办

写三个月代码,你会遇到最多的是ValueError: time data '...' does not match format。这个报错核心就是“你的字符串和格式串对不上”。

排查顺序很固定:

  1. 用print或者repr把原始字符串原样打出来,看有没有隐藏的前后空格、制表符、换行。
  2. 对照格式表逐项检查,年份、月份、日期占位符有没有写反。
  3. 能解析 ISO 字符串时,直接用datetime.fromisoformat(),别自己拼strptime格式。
  4. 遇到微秒字段,%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 TypeError:naive 和 aware 乱加如

另一个高频报错是:

TypeError: can't subtract offset-naive and offset-aware datetimes

原因很直接:一边是 naive,一边是 aware,Python 拒绝直接计算。解决办法不是删掉时区信息,而是把它们统一到同一频率。

如果确实要得到 naive 对象,可以这样:

aware = datetime.now(timezone.utc) naive = aware.replace(tzinfo=None)

但我不建议把这个当常规武器。更好的办法是给 naive 对象补上时区标签:

from zoneinfo import ZoneInfo local = ZoneInfo("Asia/Shanghai") naive_local = datetime(2025, 7, 20, 9, 0, 0) aware_local = naive_local.replace(tzinfo=local) 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(days=1),而要先now().date()退到当天零点,再算窗口;判断是否同一天,先调.date();算月末别靠记天数,用calendar.monthrange。

最后分享一个小技巧:如果你要把datetime作为字典键或放进集合里,尽量用 aware 时间,而且最好统一成 UTC。naive 时间经常让两个“看起来不同”的时刻在换算后变成同一个值,触发奇怪的幂等冲突。我写缓存时都是先统一成 UTC 再存,后来踩坑率明显变低。

datetime不是一个一眼能看透的小模块,但只要肯花半小时把格式、时区、边界这三关打通,后面写脚本会稳得多。这篇里的代码我基本都亲手跑过,踩过的坑也如实写了,希望能帮你省下一些深夜排查的时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 10:05:24

基于SSM+Java的青少年心理健康科普平台毕设实战解析

每年一到开题季&#xff0c;就有不少人抱着“SSM Java”的组合来找我&#xff0c;问同一个问题&#xff1a;这个技术栈是不是太老了&#xff1f;2026年的毕设还能不能选&#xff1f;我的回答一向很直接&#xff1a;SSM从来不是问题&#xff0c;问题是你拿它做了什么。如果项目…

作者头像 李华
网站建设 2026/10/3 10:05:03

绿色药都医药博物馆展陈设计:从产业叙事到空间体验的实践

第一次拿到椒江绿色药都医药博物馆的展陈设计委托时&#xff0c;主题文案写得很大气&#xff1a;擎创新设计之笔&#xff0c;擘画椒江绿色药都医药博物辉煌。作为森克思科技的项目负责人&#xff0c;我看到这句话的第一反应不是兴奋&#xff0c;而是清醒——医药产业馆我们做过…

作者头像 李华
网站建设 2026/10/3 10:03:43

Claude Managed Agents重构销售线索入口

1. 项目概述&#xff1a;当销售团队开始用“AI同事”接管线索入口 你有没有遇到过这样的场景&#xff1a;市场部刚投完一轮LinkedIn广告&#xff0c;CRM里瞬间涌进200条新线索&#xff0c;销售主管盯着看板&#xff0c;手指在键盘上悬停三秒&#xff0c;最后只敲出一句&#xf…

作者头像 李华
网站建设 2026/10/3 10:03:43

BCT脑网络分析入门:MATLAB图论工具安装与实战指南

1. 为什么BCT对神经影像研究者是“绕不开的硬门槛”——从一张fMRI图谱说起 你刚拿到一组静息态fMRI数据&#xff0c;预处理做完&#xff0c;时间序列提取完毕&#xff0c;准备构建功能连接矩阵。这时候&#xff0c;同事随口问一句&#xff1a;“打算用什么算全局效率&#xff…

作者头像 李华
网站建设 2026/10/3 10:02:52

同步相量算法对比:FFT、窗函数、HHT与小波变换的Matlab实践

做电力系统同步相量计算研究&#xff0c;最容易踩的坑就是——把FFT、窗函数法、希尔伯特-黄变换、小波变换四条路线各自跑一遍&#xff0c;得到几张漂亮的对比图&#xff0c;然后发现不知道该信谁。FFT快、窗函数法稳、HHT自适应、小波能抓暂态&#xff0c;这个“常识”谁都会…

作者头像 李华
网站建设 2026/10/3 10:00:02

新版MyBatis-Plus代码生成器FastAutoGenerator实战指南

写Java后端的人&#xff0c;大概都有被CRUD支配过的经历。实体类加一个字段&#xff0c;Controller、Service、Mapper、XML全要跟着动&#xff1b;新表建好&#xff0c;光把那套标准文件补齐就要小半天。我第一次用MyBatis-Plus代码生成器时&#xff0c;只当它是个快速生成类的…

作者头像 李华