ARTICLE DETAIL

资讯详情

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

Python datetime 模块精讲:时区、格式化与实战避坑指南

Python datetime 模块精讲:时区、格式化与实战避坑指南 写 Python 这几年如果说有什么东西看着简单、用起来却最容易翻车我第一个想到的就是日期时间。字符串和时间对象搞混、格式化指令写错、时区悄悄偏移——几乎每个项目都要在这些小事上栽一次。而处理这些事情Python 自带的datetime模块就是绕不开的核心工具它不用装任何第三方库日常开发里九成以上的日期时间需求都能用它搞定。这篇文章我不会从文档复读概念而是把datetime掰开揉碎从设计思路讲到高频操作再带几个真实场景爬虫、量化数据、日志脚本过一遍最后把最容易踩的坑统一抖出来。不管你是刚学 Python 的入门者还是写了几年代码想要查漏补缺的开发者照着走一遍日期时间这块基本就稳了。1. 先看懂模块设计datetime 的六个类到底怎么分工1.1 现实世界的时间模型日历、钟表和时间差很多初学者一上来就查datetime的用法结果发现里面有date、time、datetime、timedelta、timezone一堆东西直接懵了。其实这个模块的设计思路特别贴近现实它把“时间”这个概念拆成了几个独立的零件。date管日历上的日期年、月、日time管一天中的时刻时、分、秒、微秒datetime是前两者的组合体表示一个完整的时间点比如“2025年1月15日14点30分45秒”。timedelta表示的则是两个时间点之间的距离比如“3天5小时”。还有两个管时区的timezone和tzinfo前者是固定偏移量的实现后者是抽象基类一般用不到。用生活化的话说date是一张日历time是一个挂钟datetime是“日历 挂钟”一起看timedelta是两趟列车发车时间之间的间隔。搞懂这个对应关系后面所有操作都是在这几个零件之间做组合。1.2 核心类的职责对照表与选型建议我整理了一张表把每个类的职责和典型使用场景列出来平时写代码前对着看一眼能少走很多弯路类职责典型场景date只表示年月日不关心时分秒生日、节日、报表日期列time只表示一天中的时刻每日定时任务触发点datetime年月日 时分秒完整时间点日志时间、订单创建时间、接口返回时间timedelta时间差可以做加减运算过期时间、倒计时、时间窗口timezone固定时区偏移量如东八区把时间标上“这是北京时间”tzinfo时区抽象基类timezone是它的实现需要自定义时区规则时选型建议很简单只要“日期”字段用date只要“几点几分”这种一天内的时刻用time要记录一个完整时间点用datetime算差值、做加减用timedelta。别上来就全部datetime比如你只需要一个“今天是几号”用date比用datetime更精准也避免和时分秒搞混。1.3 新手最容易踩的坑date 和 datetime 不是一回事我刚学 Python 时犯过一个经典错误拿date.today()的结果去做strftime或者比较时分秒结果发现date对象根本没有hour和minute属性直接AttributeError。date是日历datetime才是完整时间点两者虽然有共同的year、month、day但date没有时分秒。反过来如果手里有一个date和一个time想拼成一个datetime要用datetime.combine()。我做过一个定时任务脚本日期从配置里读是date类型时间从用户输入里解析是time类型最后用datetime.combine(date_obj, time_obj)合成完整时间点再去做和当前时间的比较。这个细节很多新人会卡住先记住datetimedatetime.combine()。2. 基础操作实战获取当前时间、构造目标时间和格式化2.1 获取当前时间now、today、utcnow 的区别与正确姿势获取当前时间是最高频的操作但datetime.now()、datetime.today()、datetime.utcnow()这三个方法很多新手分不清。简单说now()返回本地时间的 naive datetime不带时区信息today()和now()基本等价utcnow()返回 UTC 时间的 naive datetime。这里有个重点Python 3.12 开始datetime.utcnow()已经标记为弃用DeprecationWarning因为它返回的是一坨“没有时区信息的 UTC 时间”很容易让代码里埋下时区 bug。我现在的习惯是明确传时区本地时间用datetime.now()就行但如果你要的是 UTC 时间请写datetime.now(timezone.utc)这样拿到的就是带时区信息的 aware datetime后面转时间戳、跨时区比较都安全。from datetime import datetime, timezone local_now datetime.now() # 本地时间tzinfoNonenaive utc_now datetime.now(timezone.utc) # UTC时间tzinfoUTCaware print(local_now) print(utc_now)我踩过的坑是早期写爬虫时把服务器返回的 UTC 时间当成本地时间去处理数据的发布时间全部往前推了 8 个小时。后来统一用datetime.now(timezone.utc)拿标准时间所有内部分析都用 UTC只在最后给用户展示时才转成本地时间问题彻底消失了。2.2 手工构造指定时间参数顺序、默认值和越界检查构造指定时间也是家常便饭比如量化策略里要指定回测起始日期、爬虫里要按指定日期拉数据。直接datetime(2025, 1, 15, 14, 30, 45)就行参数顺序是年、月、日、时、分、秒、微秒、时区。前面三个必填后面可以省略时分秒默认 0。有两个细节值得记住。第一月份和日期越界会直接抛ValueError比如datetime(2025, 2, 30)Python 不会帮你规整成 3 月 2 日它直接拒绝执行。这反而是好事能逼你在源头把非法日期挡住。第二可以用datetime.min和datetime.max作为边界值比如判断“某个事件是否在今天之前”把当天零点作为下边界比手动拼datetime(2025, 1, 15, 0, 0, 0)更稳妥。2.3 字符串和时间对象互转strftime 与 strptime 完整对照这是datetime模块里最容易出错的部分没有之一。我见过太多同事把%m月份和%M分钟写反一查就是半天。strftime是“把时间对象变成字符串”strptime是“把字符串解析成时间对象”两者用的格式化指令是一样的。from datetime import datetime now datetime.now() # 时间对象 - 字符串 s1 now.strftime(%Y-%m-%d %H:%M:%S) # 2025-01-15 14:30:45 print(s1) # 字符串 - 时间对象 s2 2025-01-15 14:30:45 dt datetime.strptime(s2, %Y-%m-%d %H:%M:%S) print(dt)常用的格式化指令我列一下你可以贴在键盘旁边指令含义示例%Y四位数年份2025%m两位数月份01%d两位数日期15%H24小时制小时14%M分钟30%S秒45%f微秒6位123456%A完整星期名Wednesday%a缩写星期名Wed%B完整月份名January%b缩写月份名Jan%j一年中的第几天015%zUTC偏移量0800%Z时区名CST%pAM/PMPM实操心得有一条很关键项目里如果时间格式不统一解析起来会痛不欲生。我现在的习惯是内部数据统一用 ISO 8601 格式也就是%Y-%m-%dT%H:%M:%S对接口、写数据库、打日志都统一这一套。Python 3.11 之后datetime.fromisoformat解析能力增强了很多很多带时区的 ISO 字符串都能直接解析比手动strptime更省心。3. timedelta 与时间戳让时间会加减、能换算、可比较3.1 timedelta 的构造与运算参数归一化和常用写法timedelta最直观的用法是给时间做加减法。比如“三小时后的提醒”“七天前的日志”。构造时你可以传weeks、days、hours、minutes、seconds、milliseconds、microseconds它会自动按进制换算合并成一个差值。注意一点timedelta内部只保存days、seconds、microseconds三个属性你传的weeks会被折算成天数多余的秒数会被归到seconds里。我写定时任务时常用的写法from datetime import datetime, timedelta now datetime.now() later now timedelta(hours3, minutes30) earlier now - timedelta(days7) print(later) print(earlier)timedelta还支持乘除和取绝对值。比如倒计时任务里把一轮时间差乘上批次次数就能算出总耗时。这些操作看着简单但组合起来能覆盖很多业务逻辑比如限流窗口、验证码过期时间、缓存有效期全部可以用timedelta表达比手写秒数再换算清晰太多。3.2 计算两个时间的差值days 和 total_seconds 千万别记混两个datetime直接相减得到的就是timedelta。这个对象有几个返回“差值”的属性但它们的含义很容易搞混days总共的整数天数。seconds减去整天之后剩余的秒数范围 0~86399。total_seconds()把整个差值全部换算成秒带小数。举个例子从 1 月 1 日 12:00 到 1 月 3 日 10:00days是 1seconds是 7920022 小时但total_seconds()是 165600。很多新手只看到days和seconds就以为能拼出总时长结果把“1天22小时”算成了“1天22秒”或者漏掉整天全错。我的建议是要做超时判断或者换算单位一律用total_seconds()别用days或seconds拼。from datetime import datetime start datetime(2025, 1, 1, 12, 0, 0) end datetime(2025, 1, 3, 10, 0, 0) diff end - start print(diff.days) # 1 print(diff.seconds) # 79200 print(diff.total_seconds()) # 165600.03.3 与 Unix 时间戳互转timestamp 和 fromtimestamp 的配套使用在爬虫、接口对接、数据库存取场景里时间戳Unix timestamp比格式化字符串更常见。datetime和 Unix 时间戳的互转方法就是一对timestamp()把datetime转换成秒级时间戳fromtimestamp()把时间戳还原成datetime。这套成对使用的核心在于时区datetime.timestamp()对 naive datetime 会假设它是本地时间转成对应的 UTC 时间戳fromtimestamp()则把时间戳按系统本地时区还原。from datetime import datetime now datetime.now() ts now.timestamp() print(ts) back datetime.fromtimestamp(ts) print(back)实测下来时间戳互转最容易翻车的点就是忘记本地时区的存在。以前接过一个第三方接口返回的数据时间戳是 UTC 的但我直接用fromtimestamp转结果所有时间都变成 UTC8 的本地时间表面上时间戳没变实际上展示值偏了 8 小时。正确做法是先用datetime.fromtimestamp(ts, timezone.utc)还原成 UTC再按需求转本地时区。3.4 大小比较与边界判断调休、营业时间和上下班打卡这类逻辑datetime对象之间可以直接用、、比较也可以放进列表用sorted()排序。这套天然支持让日期判断逻辑变得非常简洁。比如判断现在是否在工作时间内from datetime import datetime, time def is_in_business_hours(now: datetime) - bool: start datetime.combine(now.date(), time(9, 0)) end datetime.combine(now.date(), time(18, 0)) return start now end这个写法里最值得学习的一步是datetime.combine(now.date(), time(9, 0))——把“今天的日期”和“9点钟”拼成完整时间点再做区间判断。不要手动构造datetime(now.year, now.month, now.day, 9, 0)又长又容易漏。判断星期几则用weekday()周一到周日返回 0~6或isoweekday()返回 1~7周一是 1比如“只在周三跑一次”的定时任务一行就能写出来。4. 时区处理naive 与 aware最隐蔽也最烧钱的一个坑4.1 naive 和 aware 到底差在哪用“没标城市的时间”来理解naive 和 aware 是datetime时区问题的核心概念。naive datetime 就是“没有时区信息的时间”比如2025-01-15 14:30:45你只知道这一刻但不知道它是北京时间、东京时间还是 UTC。aware datetime 则是带上了tzinfo的时间比如2025-01-15 14:30:4508:00明确告诉你是东八区的下午两点半。这两者的区别用一句话概括naive 是“没标城市的时刻”aware 是“标了城市的时刻”。平时写本地小脚本naive 够用但只要涉及跨时区、对接外部接口、数据库里存全球用户的时间naive 就是定时炸弹。而且 Python 在比较 naive 和 aware datetime 时会直接抛TypeError报错信息看着挺吓人其实就是在提醒你这俩不是一个世界的东西别比。4.2 创建带时区的时间timezone、zoneinfo 和 pytz 怎么选创建带时区的时间有三种常见方式。最简单的是timezone(timedelta(hours8))固定 8 小时偏移适合确定是东八区且不考虑夏令时的情况from datetime import datetime, timezone, timedelta cn_tz timezone(timedelta(hours8)) now_cn datetime(2025, 1, 15, 14, 30, tzinfocn_tz) print(now_cn) # 2025-01-15 14:30:0008:00如果涉及具体城市名和夏令时推荐 Python 3.9 内置的zoneinfo.ZoneInfo直接写城市名系统会自动处理夏令时规则from zoneinfo import ZoneInfo from datetime import datetime tokyo_tz ZoneInfo(Asia/Tokyo) now_tokyo datetime(2025, 1, 15, 14, 30, tzinfotokyo_tz) print(now_tokyo) # 2025-01-15 14:30:0009:00第三方库pytz是老方案很多老项目里还在用。使用pytz有个经典陷阱是用replace(tzinfopytz.timezone(Asia/Shanghai))这种方式挂时区在某些历史时间点上会出错正确姿势是pytz.timezone(...).localize(dt)。所以我个人的建议是新项目直接用zoneinfo老项目里用到pytz时记住localize()而不是replace()。4.3 真实项目里的时区规范数据库存 UTC展示转本地在真实项目里我遵守的铁律是内部统一用 UTC外部展示再转本地。数据库里存时间戳或 UTC 时间日志里也统一打 UTC不把本地时间满天飞。这样做的好处是任何人拿到数据都能明确知道“这是哪一刻”至于要不要转成自己的本地时区那是展示层的事。具体落地很简单写入数据库时用datetime.now(timezone.utc)读出后需要展示给用户时再用.astimezone()转本地时区。接口对接时也尽量要求对方给 ISO 8601 带偏移格式的字符串或者直接用 UTC 时间戳。这套规范我用了很久几乎没再遇到过时区错乱的问题。5. 真实场景实战爬虫、量化数据和日志脚本里的时间处理5.1 爬虫场景解析五花八门的时间字符串并判断时效做爬虫时最头疼的其实不是反爬而是各网站的时间格式五花八门。有的返回2025-01-15 14:30:45有的返回2025-01-15T14:30:00Z还有的相对时间“3小时前”。我的处理原则是先统一成datetime再统一转成 UTC 或时间戳最后才做比较。举一个实际例子某接口的列表中每条数据带一个时间字段格式是 ISO 8601末尾带Z表示 UTC。我要筛选出 24 小时内发布的内容from datetime import datetime, timedelta, timezone def is_recent(pub_time_str: str, hours: int 24) - bool: # 把类似 2025-01-15T14:30:00Z 的字符串解析成 aware datetime dt datetime.fromisoformat(pub_time_str.replace(Z, 00:00)) now datetime.now(timezone.utc) return now - dt timedelta(hourshours) print(is_recent(2025-01-15T14:30:00Z))这段代码的核心在于先把 endswith Z 的字符串替换成00:00这样fromisoformat解析出来就是带 UTC 时区的 aware datetime再和datetime.now(timezone.utc)做减法全程不涉及本地时区也就不会出现 8 小时偏差。如果遇到“3小时前”这种相对时间可以用dateutil的parser配合手动计算或者在爬虫里直接跳过这类不统一的字段要求接口方返回标准格式。5.2 量化与数据分析场景K 线时间列的读取与切片量化交易和数据分析场景里时间列是最常用到的索引。拿日线数据举例CSV 里可能是2025-01-15这种字符串也可能是1736908200这种时间戳。用pandas读取后最好先统一转成datetime类型再操作否则切片、比较、绘图全都会出问题。实际处理 K 线数据时我一般这样做import pandas as pd from datetime import datetime df pd.read_csv(daily_kline.csv) df[date] pd.to_datetime(df[date]) # 只取 2025 年 1 月的数据 start datetime(2025, 1, 1) end datetime(2025, 1, 31) jan_df df[(df[date] start) (df[date] end)]这里有个细节pandas的Timestamp是datetime.datetime的子类所以datetime对象可以直接和它比较非常方便。回测时如果想判断“当前这根 K 线是否已经收盘”或者“当前时间是否在交易时段内”同样可以用第 3 章里的datetime.combine和区间比较方法。时间处理在量化里是个基础但又不能出错的地基我见过不止一次因为时区没统一导致回测信号对不上盘面时间的情况。5.3 日志与自动化脚本场景按天轮转、运行耗时与工作日判断日志和自动化脚本里datetime的用法更接地气。比如我写过一个定期拉数据的任务日志按天归档文件名是app_20250115.log。这个文件名直接用date.today().strftime(app_%Y%m%d.log)生成简单可靠。再看运行耗时from datetime import datetime start_time datetime.now() # 模拟任务执行 # ... end_time datetime.now() print(f任务耗时{(end_time - start_time).total_seconds():.2f} 秒)判断“是否工作日运行”一行datetime.now().weekday() 5就够了周一到周五返回 0~4。如果涉及法定节假日调休datetime本身解决不了要接节假日表但判断周几是基础能力。另外建议长时间运行的任务用time.monotonic()计耗时而不是datetime.now()差值因为系统时间可能被 NTP 校准而跳变monotonic更稳。这个细节我用datetime计时时踩过后来在长任务里就换成monotonic了。6. 常见问题与排查技巧实录我踩过的坑你直接绕开6.1 字符串解析频繁报错先检查这几个低级但致命的点strptime报ValueError大概有几种原因格式串和字符串对不上、月份写了 13、日期写了 32、%m和%M搞混、%Y写了两位年份。排查的时候我一般按三步走先打印原始字符串到底长什么样再对着指令表一个字段一个字段核对最后用repr()看一下有没有隐藏的空格或换行。特别是从网页或接口里拿到的字符串经常带 BOM 或首尾空白strip()一定要做。另一个高频问题是解析后端返回的2025-01-15 14:30:45时忘了月份和分钟的区别写出了%M-%d结果要么报错要么解析出完全错误的时间。这类问题靠肉眼很难发现我的建议是写一个小的输出对照函数把解析前后结果同时打印出来一眼就能看出问题。6.2 时间戳数值对不上被本地时区偷偷坑了一把如果你发现数据库里存的时间戳在页面上显示的时间比预期慢了 8 个小时或者快了 8 个小时八成是本地时区参与了一次不应该的转换。比如用fromtimestamp(ts)转出来的是本地时间你再手动加上timezone(timedelta(hours8))就重复加了两次。我的统一口径是时间戳本身是 UTC 的还原时先用fromtimestamp(ts, timezone.utc)得到 UTC再通过.astimezone()转目标时区。这套流程固定下来之后时区偏差问题基本绝迹。6.3 批量场景下的性能注意点循环里别反复申请当前时间在处理大量数据时datetime.now()虽然不是一个特别慢的操作但如果在循环里每次迭代都调用一次几百万行的数据也会带来不必要的开销。更关键的不是性能而是逻辑一致性循环里每次取“当前时间”如果恰好跨越了秒甚至分钟的边界你拿到的开始时间和结束时间就不是同一个时间基准可能导致误判。正确的姿势是在循环之前取一次快照now datetime.now()循环内部统一使用这个值。now datetime.now() for item in large_list: # 所有判断都用 now而不是在循环里重新调 datetime.now() if item[time] now: ...性能本身我倒不是最担心的真正容易翻车的是“时间基准漂移”。固定一个时间快照既省了重复调用的开销又保证了整个批次的比较口径一致。6.4 时间处理自测清单上线前花两分钟扫一眼这里分享几个我在代码 review 时必查的清单项第一打印或写日志的时间是 naive 还是 aware如果是 naive确认它全链路里只做“展示”用途不参与跨时区比较。第二timestamp()和fromtimestamp()配对使用时明确中间过程有没有隐式本地时区参与。第三算差值和耗时用的是total_seconds()而不是days和seconds手工拼。第四%Y、%m、%M这类指令写对了没。第五跨年的日期加减用timedelta而不是手写天数判断。这些项看着琐碎但几乎能覆盖 80% 的日期时间 bug。把自查清单放在项目里每次提交前过一遍能省下很多线上排查的时间。如果让我给刚入门的朋友一句建议我会说先把日期时间当成一个对象而不是字符串所有操作都围绕对象展开字符串只在输入和输出边界出现。这样整个代码的中间逻辑都清晰可控。最后再分享一个实用小技巧如果你不确定某个格式化指令的效果直接开一个 Python 交互环境用datetime.now().strftime(%Y-%m-%d %H:%M:%S)逐个试十秒钟验证完比自己瞎猜快得多。日期时间处理本就不该是玄学把基本概念捋顺了写起来稳得很。
返回列表