DEBUG.WIKI
丽萨主机;为跨境业务选好 IP;原生 / 住宅 IP · 按套餐选择;查看 IP →
阿里云;从建站到云上业务;云服务器 · 数据库 · 对象存储;查看产品 →
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

Python 比较时间报 can't compare offset-naive and offset-aware datetimes

带偏移的时间与 datetime.now() 比较时报 TypeError,使用 aware UTC 当前时间,区分 astimezone 转换和 replace 标记,避免无声改变时间含义。

从邮件或接口解析出带 UTC 偏移的时间,再与 datetime.now() 比较先后,可能出现:

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

这通常表示一侧带有有效时区信息,另一侧没有。即使两者打印出来的年月日和时分秒很接近,Python 也不会替没有时区的对象猜测它代表哪个时区。

下面使用 Python 3 标准库,适用于 %z 解析结果为 aware datetime 的 Python 3.2 及以后版本。示例只处理表示具体时刻的时间比较;不把生日、每日营业时间等本来就不对应单一时刻的业务字段强行转换为 UTC。

找出没有时区信息的那一侧

这是一个简化的错误示例,使用数字日期避免英文月份、星期与系统语言设置产生额外问题:

from datetime import datetime

received = datetime.strptime(
    "2020-01-31 09:59:34 +0000",
    "%Y-%m-%d %H:%M:%S %z",
)
now = datetime.now()

print(now.tzinfo, now.utcoffset())
print(received.tzinfo, received.utcoffset())
print(now > received)

datetime.now() 没有传入时区时,返回本地日期时间,但对象的 tzinfo 是 None。strptime 使用 %z 读取 +0000 后,生成带零偏移的 aware 对象。最后一行进行大小比较,因此触发错误。

判断 aware 不能只看有没有 tzinfo 属性:它必须不是 None,而且对象的 utcoffset() 也不能返回 None。排查时同时查看这两项,可以发现自定义时区对象没有提供有效偏移的情况。

当前时间直接生成 aware UTC 对象

这里不需要去掉收到时间里的偏移,也不需要安装额外时区包。让当前时间从创建时就带有 UTC 时区:

from datetime import datetime, timezone

received = datetime.strptime(
    "2020-01-31 09:59:34 +0000",
    "%Y-%m-%d %H:%M:%S %z",
)
now = datetime.now(timezone.utc)

print(now > received)

比较结果取决于实际运行时刻,但不应再因 naive 与 aware 混用而报错。收到的时间如果是 +0800,也不必先把字符串手工减去八小时;这里由 %z 创建的固定偏移对象可以按对应时刻比较;不要把这条说明直接推广到地区时区的夏令时重复区间。为了让日志或存储统一为 UTC,可对已带时区的对象使用 received.astimezone(timezone.utc)。

把 now() 换成 utcnow() 不能解决类型差异。utcnow() 虽然给出 UTC 的年月日时分秒,却仍返回 naive 对象;它从 Python 3.12 起已被弃用。使用 datetime.now(timezone.utc) 也能兼容较早的 Python 3,无需依赖 Python 3.11 才新增的 datetime.UTC 别名。

replace 只改时区标记,不转换时刻

下面这两种“让代码不报错”的做法需要特别小心:

local_now = datetime.now()
wrongly_tagged = local_now.replace(tzinfo=timezone.utc)
without_zone = received.replace(tzinfo=None)

第一种把本地墙上时间直接标成 UTC,小时、分钟完全不变。如果系统本地时区不是 UTC,就改变了这个对象代表的时刻。第二种丢掉收到时间的时区,后续比较只剩日期时间字段,可能掩盖两侧原本不同的偏移。

只有数据契约明确保证某个 naive 值本来就是 UTC,才可以用 replace(tzinfo=timezone.utc) 补上缺失标记。若它代表其他地区的当地时间,应先确认原始时区及夏令时重复、缺失时间的处理规则;来源不明时不能直接假定 UTC。已有 aware 值要换时区,则使用 astimezone,保留同一个时刻。

timestamp() 方法从 Python 3.3 起提供,也不要把两侧都转成时间戳当作通用补救。对 naive datetime,它会按系统本地时间解释;机器时区一变,同一份缺失时区的数据就可能变成不同时间戳。先明确时间含义,再选择转换方式。

用固定偏移样本检查比较语义

只确认“不再抛异常”还不够。下面的两个输入日期时间不同,但应当表示同一时刻:

from datetime import datetime, timezone

fmt = "%Y-%m-%d %H:%M:%S %z"
a = datetime.strptime("2020-01-31 09:59:34 +0000", fmt)
b = datetime.strptime("2020-01-31 17:59:34 +0800", fmt)

print(a == b)
print(b.astimezone(timezone.utc).isoformat())

预期分别为 True 和 2020-01-31T09:59:34+00:00。再覆盖业务中的早于、晚于和恰好相同三种输入,检查 <、>、<= 等边界是否符合规则。

修改解析或比较逻辑时,先在独立分支验证,不要顺手批量重写历史时间字段。若已有数据曾被错误地补成 UTC,恢复代码并不会自动恢复那些记录:需要保留原始输入和旧数据,根据真实来源时区另行修正。缺少来源时区的记录应保留待核状态,不能通过删掉偏移让错误悄悄消失。