尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python日期时间处理全攻略:从datetime到时区与时间序列

Python日期时间处理全攻略:从datetime到时区与时间序列 看过不少Python项目入门阶段最容易被忽略、到了写业务代码时又最容易翻车的就是日期和时间处理。你可能觉得不就是一个datetime.now()嘛真到做报表、做定时任务、处理跨时区数据的时候会发现各种稀奇古怪的问题月份加了变成了下下个月、时间字符串解析老报错、数据库存的时间取出来少了8个小时。这篇内容我想把Python日期和时间相关的核心思路、实操写法、以及我在项目里踩过的坑一次讲清楚尽量让刚入门的朋友也能照着写。不管是写爬虫做数据清洗、做Web后端写接口还是做数据分析处理Excel报表日期时间都是绕不开的基础功。我会把time和datetime的区别、格式化解析的常见写法、时区处理的正确姿势以及复杂的日期计算和pandas时间序列怎么配合都拆开讲一遍。如果你是Python零基础还没配好开发环境建议先把Python装好、随便用哪个IDE都行然后打开交互式环境边看边敲这样理解会深很多。1. 先把三个时间概念掰扯清楚时间戳、结构化时间和格式化字符串1.1 为什么Python里时间这么绕刚接触Python日期时间的时候最劝退的一点就是概念太多。今天的时间、一个时间对象、一串2025-01-01 12:00:00的字符串在Python里分别是三种完全不同的东西。我见过很多新手在这儿卡住原因就是没搞清楚这三个概念之间的关系。先说时间戳timestamp。时间戳是计算机真正用来存时间的方式在Python里用time.time()拿到的是一个浮点数表示从1970年1月1日0点0分0秒UTC到当前时刻经过的秒数。为什么要用这个因为对计算机来说用一个整数或浮点数记录时间最省空间、最方便比较大小。你只要记住时间戳是给机器看的时间。再说结构化时间。Python里用time.struct_time它是一个包含年、月、日、时、分、秒、星期、一年第几天等字段的命名元组。datetime模块里的datetime对象、date对象也属于这一类。结构化时间是给人类逻辑看的时间你可以直接拿obj.year、obj.month来操作。最后是格式化字符串比如2025-03-15 20:30:00这是给人眼睛看的时间。当你打印时间、写日志、传输数据的时候用的都是格式化字符串。这三者之间的转换关系是这样字符串通过parse或者strptime变成结构化时间结构化时间通过timestamp()变成时间戳时间戳通过fromtimestamp()变回结构化时间结构化时间通过strftime变成字符串。整个过程像一个环搞懂这个环很多问题就迎刃而解了。1.2 time模块和datetime模块到底该用谁Python标准库提供了time和datetime两个模块很多人一开始会混着用其实它们定位很清晰。time模块更偏底层它围绕时间戳和struct_time做文章提供time()、sleep()、localtime()、mktime()等函数。datetime模块则提供了更面向对象的date、time、datetime、timedelta类直接操作方法属性代码可读性好很多。我的建议是如果你要写业务代码处理日期计算、格式转化、时区优先用datetime模块它更直观也更安全。time模块通常在两个场景下更好用一是单纯需要time.sleep()做延时二是需要和C语言接口、老代码或者系统级时间打交道时直接用时间戳效率更高。日常90%的日期时间场景datetime都能搞定。选择的时候还有一个细节time模块的localtime()和gmtime()返回的是本地时区和UTC的结构化时间但它们不带时区信息你在代码里看到这个对象根本分不清它到底是哪个时区。datetime模块里的datetime对象可以通过timezone或第三方库zoneinfo挂上时区语义更明确。从工程角度讲你的数据结构里能不能看出时区信息很重要这决定了程序将来会不会埋下时间错乱的坑。2. 日期时间的创建、格式化与时区处理日常最常用的操作2.1 获取当前时间和创建指定时间用datetime模块获取当前时间最基础的两行代码from datetime import datetime, date, time, timedelta now datetime.now() today date.today() print(now) # 2025-03-15 20:30:45.123456 print(today) # 2025-03-15datetime.now()返回的是本地时间的datetime对象精确到微秒。如果你需要UTC标准时间用datetime.utcnow()或datetime.now(timezone.utc)。这里要注意一点utcnow()返回的对象是naive的没有时区信息如果你后续要做时区转换建议直接使用datetime.now(timezone.utc)返回一个带UTC时区信息的aware对象后面操作会省很多麻烦。创建指定的日期和时间可以直接传参d1 datetime(2025, 3, 15, 20, 30, 0) d2 date(2025, 3, 15) d3 time(20, 30, 0)你可能会问明明有字符串为什么还要手动创建datetime对象因为在代码里进行日期比较、加减、提取年月日等操作时datetime对象才是活的字符串只是死的展示形态。比如判断某个订单是否超时你需要拿当前时间减去下单时间得到一个timedelta字符串是做不了这种运算的。2.2 字符串解析格式化strptime和strftime的坑字符串和datetime对象互转涉及strptime字符串解析成datetime和strftimedatetime格式化成字符串两个方法。看名字容易混我的记忆口诀是p是parse解析进去f是format格式化出来。基本用法from datetime import datetime # strptime: 字符串 - datetime dt datetime.strptime(2025-03-15 20:30:00, %Y-%m-%d %H:%M:%S) print(dt) # 2025-03-15 20:30:00 # strftime: datetime - 字符串 s dt.strftime(%Y/%m/%d %H:%M:%S) print(s) # 2025/03/15 20:30:00这里面最大的坑是格式符。%Y是四位年份%y是两位年份%m是月份%M是分钟%H是24小时制%I是12小时制12小时制还需要配合%pAM/PM才能准。眼睛一花就把%m和%M搞混结果分钟变月份甚至解析时报错。我建议你把这几个常用的格式符写在一个备忘录里用的时候直接查格式符含义示例%Y四位数年份2025%y两位数年份25%m两位数月份03%d两位数日期15%H24小时制小时20%I12小时制小时08%M分钟30%S秒00%f微秒123456%A完整星期名Saturday%a缩写星期名Sat%B完整月份名March%b缩写月份名Mar%j一年中的第几天074还有一个大家经常疏忽的点strptime的格式字符串必须和输入字符串严格匹配多一个空格、少一个前导零都会报错。比如2025-3-15这个字符串你用%Y-%m-%d去解析会直接抛异常因为月份和日期不是两位数。这时要么改造输入数据补零要么用比较灵活的第三方库dateutil.parser.parse它能自动识别很多常见格式。我的习惯是业务数据格式可控的情况下用标准库strptime因为性能更好格式乱七八糟的日志或外部数据就用dateutil的parse。2.3 时区从UTC到本地时间的正确姿势时区是日期时间处理里最容易出错的一块。简单说带时区信息的datetime对象叫aware对象不带时区信息的叫naive对象。如果你把naive对象误认为本地时间再去做转换结果往往是错的。Python 3.9及以上版本标准库提供了zoneinfo模块可以直接获取系统时区数据库里的时区信息不推荐再用第三方库pytz虽然老项目里pytz很常见但它管理夏令时的方式容易踩坑localize方法也比较绕。from datetime import datetime, timezone, timedelta from zoneinfo import ZoneInfo # 创建带时区的时间 dt_utc datetime.now(timezone.utc) dt_shanghai datetime.now(ZoneInfo(Asia/Shanghai)) dt_newyork datetime.now(ZoneInfo(America/New_York)) print(dt_utc) # UTC时间 print(dt_shanghai) # 上海时间 print(dt_newyork) # 纽约时间注意夏令时 # 时区转换 converted dt_utc.astimezone(ZoneInfo(Asia/Shanghai)) print(converted)工程上比较推荐的做法是内部统一用UTC时间存储、计算只在展示的时候转换为本地时区。这样做的好处是避免夏令时、时区偏移带来的混乱。比如你的服务器部署在海外数据库存的是UTC时间用户在中国你只需要在接口返回前把UTC转换成Asia/Shanghai显示就不会差了8小时。关于时区还有一个高频坑很多数据库驱动或ORM在存储datetime时会自动把naive对象当作本地时间取出来又当成naive对象一存一取之间时间就错乱了。所以在写数据库模型时最好统一约定存UTC的aware对象字段类型设为带时区的类型比如PostgreSQL的timestamptz不要用默认的timestamp。3. 复杂日期计算和数据分析场景实战3.1 月份加减这种需求别自己硬算日期计算里加减天数最简单直接用timedeltafrom datetime import datetime, timedelta now datetime.now() yesterday now - timedelta(days1) tomorrow now timedelta(days1) next_hour now timedelta(hours1)timedelta支持天、小时、分钟、秒、微秒、星期用起来很顺手。但它有个硬伤不支持月份和年份加减。你想想也知道日期加一个月不同月份天数不一样2月28号和3月31号加一个月结果怎么定这就是timedelta做不了月份运算的原因。如果你需要当前日期加一个月这种操作标准库没有现成方案我建议用dateutil.relativedelta模块它专门处理这种情况from datetime import datetime from dateutil.relativedelta import relativedelta now datetime.now() next_month now relativedelta(months1) last_year now - relativedelta(years1) print(next_month)relativedelta在处理月份末尾的时候有自己的一套规则如果当前是1月31号加一个月它会尽量调整到2月的最后一天而不是抛异常或者自动变成3月2号。这个行为在业务上往往更符合直觉比如算账单周期、合同到期时间你期望的是月底对月底。另外提一句有些面试题会问1月31号加一个月应该得到什么很多人被问住是因为没搞清楚需求边界。我的建议是碰到这种需求一定要先跟业务方确认清楚规则再写代码。默认使用relativedelta不要自己去判断月份天数因为闰年、月末各种边界情况很容易写漏。3.2 时间差计算和业务场景时间差在业务中太常见了计算任务耗时、判断用户活跃间隔、统计订单付款时长。核心就是两个datetime对象相减得到一个timedeltafrom datetime import datetime start datetime(2025, 3, 15, 10, 0, 0) end datetime(2025, 3, 15, 14, 30, 0) delta end - start print(delta) # 4:30:00 print(delta.total_seconds()) # 16200.0 print(delta.days) # 0 print(delta.seconds) # 16200注意这是时分秒换算后的秒数timedelta对象有三个存储字段days、seconds、microseconds但实际使用中我更推荐直接拿total_seconds()去统一换算不要手动去拆days和seconds做运算。比如你想知道总共多少分钟用delta.total_seconds() / 60最省事写delta.seconds / 60很可能会导致天数部分丢失结果偏小。两个时间相比谁大谁小直接用、就行。但这里有个大坑naive对象和aware对象不能直接比较Python会直接抛出TypeError: cant compare offset-naive and offset-aware datetimes。我经常在代码里碰到这个问题原因就是一处用datetime.now()生成naive时间另一处用datetime.now(timezone.utc)生成aware时间然后做比较。解决方法是统一标准要么全部用带时区的要么全部不带不要混用。3.3 用pandas处理时间序列数据分析更省心做数据分析的朋友一定绕不开pandas。pandas里处理时间序列有自己的一套核心是to_datetime和dt访问器。import pandas as pd # 字符串列转时间类型 s pd.Series([2025-01-01, 2025-01-02, 2025-01-03]) s pd.to_datetime(s) print(s) # 提取年份、月份、星期 df pd.DataFrame({日期: pd.to_datetime([2025-03-15, 2025-03-16, 2025-03-17])}) df[年份] df[日期].dt.year df[月份] df[日期].dt.month df[星期] df[日期].dt.dayofweek # 0周一, 6周日 print(df)pandas的to_datetime比标准库的strptime宽容很多它能够自动推断日期字符串格式比如2025/03/15、2025年3月15日这类很多都能直接转。做数据清洗的时候这一点太方便了省去手工指定格式的麻烦。如果要做时间序列分析还可以把日期列设为索引然后用resample做重采样df pd.DataFrame({ 日期: pd.to_datetime([2025-01-01, 2025-01-02, 2025-01-03]), 销售额: [100, 150, 200] }) df df.set_index(日期) monthly df.resample(M).sum() print(monthly)resample(M)就是按月汇总还有D按天、W按周、Q按季度。这个功能在销售报表、流量统计场景里非常实用。我的经验是pandas里时间字段一定要先转成datetime64类型不要拿字符串硬做排序和聚合否则结果基本都会错。4. 踩坑记录与工程建议4.1 高频报错和坑位速查不管是我自己写代码还是审别人的代码日期和时间相关的报错出现频率相当高。下面这张表是我平时排查问题的速查表报错或现象根本原因解决办法ValueError: time data ... does not match format字符串和strptime格式不匹配检查格式符注意月份/分钟区别、两位数年份、空格前导零TypeError: cant compare offset-naive and offset-aware datetimesnaive和aware对象混用统一全部使用带时区对象或全部不带数据库取出来时间少了8小时存的时候是UTC展示当成了本地时间统一约定存储UTC展示时转本地时区加一个月得到下下个月直接用timedelta做月份加减用dateutil.relativedelta时间数据分组/排序乱了数据是字符串没转datetime类型用pd.to_datetime先转换12小时制和24小时制结果差12小时%I和%H用混或少了%p24小时用%H12小时用%I%pOverflowError: date value out of range比如2月30日、月份13这类非法日期用datetime构造前做业务校验其中时间少了8小时这个问题我见过的频率特别高。很多时候是因为服务器部署在海外或者数据库默认用的是UTC而前端和用户在中国。解决思路上我建议不要在后端到处写时区转换逻辑而是在统一出口做转换比如API层或者数据存取层处理好业务代码里全部使用UTC时间。这样即使以后服务器迁移、新增用户地区改动范围也最小。4.2 多进程、性能和工程化建议日期时间处理虽然看起来简单但放到工程环境里有几个容易被忽视的点。第一个是性能。如果你的代码需要在循环里反复调用strptime或strftime规模小的时候没问题数据量大了就会发现时间解析非常拖后腿。我做过一个日志解析任务百万行日志要逐行解析时间字段最开始用datetime.strptime跑了很久后来改成pd.to_datetime批量解析速度提升了一个数量级。如果数据量更大甚至可以考虑用numpy的datetime64类型它在底层的存储和运算效率更高。第二个是时间对象在并发场景下的问题。datetime.now()这类操作本身是线程安全的不会像共享变量那样出现数据竞争但你得注意一个隐蔽问题如果你拿到一个datetime对象在多个线程或协程里去修改它由于datetime是不可变类型每次修改都会生成新对象所以不会改乱。真正需要注意的是一次性获取时间这个习惯。如果你在代码里多个地方分别调用datetime.now()拿到的可能是不同的值在计算耗时、生成订单号时会出现微小偏差。我通常会在一个请求或一个任务的入口统一拿一次当前时间然后向下传递。第三个是数据库存储建议。MySQL中建议使用datetime类型还是timestamp类型这是一个经典问题。简单说timestamp存储范围较小2038年问题而且受时区影响datetime存储范围更大、不随时区变化。如果你做全球化业务建议数据库统一使用timestamptzPostgreSQL或datetime加统一UTC约定。这个选择没有绝对对错关键是整个团队要统一规范不然每次排查时间问题都会浪费大量精力。第四个是写日志时的时间格式。很多项目的日志系统默认时间是UTC本地排查问题时看起来很别扭。我建议日志格式里统一用本地时间加时区偏移比如2025-03-15 20:30:00 08:00这样既保留时区信息又方便查看。别小看这个细节关键时刻能帮你少掉几根头发。4.3 在我实际项目里反复被验证的几个习惯做久了会发现日期时间问题大多数不是不会写而是写之前没有统一约定。我这里总结几条自己坚持了很久的习惯你可以直接拿去用。第一所有内部存储和传输的接口时间字段一律用ISO 8601格式的字符串或UTC时间戳。ISO 8601像2025-03-15T20:30:0008:00可读性好解析也有标准库支持。前端和后端传递时间时用ISO字符串比用一堆自定义格式稳得多。第二对时间格式校验不要只信strptime不报错就觉得没问题。比如2025-02-31这种日期strptime会抛异常还好但有些宽松的解析库会帮你进位成3月3日这种隐性错误比显性报错更可怕。所以在关键业务里我会在解析后回读字段做二次校验确保年月日和输入一致。第三写代码时刻意区分date和datetime。如果只关心日期不关心时间就用date别用datetime这样代码语义更清晰也能减少很多明明只需要日期却因为datetime带了时间导致比较结果不对的问题。第四如果项目中很多地方用到时间处理我建议封装一个time_utils.py工具模块把常用格式化、时区转换、解析封装成统一函数。这样做的好处是万一以后要切换时区数据库、调整格式规范只需要改一个文件。我在多个项目里都这么干维护成本明显降低。写到最后分享一点个人的使用体会日期和时间这块Python标准库本身已经覆盖了大部分日常需求datetime加dateutil基本能解决90%的问题。我踩过最大的坑其实是想当然——想当然以为本地时间和UTC一样想当然以为字符串转时间不会出错想当然以为加一个月就是把month字段加1。每次都是这些想当然让线上数据出现偏差排查起来特别费劲。所以我最后给一个最朴实的建议写日期时间相关代码时先把边界条件列一遍。跨年、跨月、月末、闰年、夏令时、时区偏移这些边界条件想清楚了代码的稳定性会大幅提升。你可以在本地写个简单的测试用例把2025年每个月的月末日期都跑一遍看看你的日期计算是不是符合预期。这种小投入回报非常值。
返回列表