在Python中将时区感知的日期字符串转换为UTC及其反向转换

9 投票
2 回答
9356 浏览
提问于 2025-04-17 02:33

我正在把国家气象局的警报信息解析到一个网页应用里。我想在警报到期时把它们删除。同时,我也想把到期时间显示为与相关地区相符的本地时间格式。

这些警报覆盖整个美国,所以我觉得最好的办法是把时间存储为UTC时间戳并进行比较。到期时间在信息中以这样的字符串形式出现:2011-09-09T22:12:00-04:00

我使用Labix dateutils这个包来以考虑时区的方式解析这个字符串:

>>> from dateutil.parser import parse
>>> d = parse("2011-09-18T15:52:00-04:00")
>>> d
datetime.datetime(2011, 9, 18, 15, 52, tzinfo=tzoffset(None, -14400))

我还能够获取到UTC的时差(以小时为单位):

>>> offset_hours = (d.utcoffset().days * 86400 + d.utcoffset().seconds) / 3600
>>> offset_hours
-4

通过使用datetime.utctimetuple()time.mktime()这两个方法,我可以把解析后的日期转换为UTC时间戳:

>>> import time
>>> expiration_utc_ts = time.mktime(d.utctimetuple())
>>> expiration_utc_ts
1316393520.0

到目前为止,我觉得我能把原始字符串转换成表示到期时间的UTC时间戳,这让我很满意。我可以把当前时间作为UTC时间戳与到期时间进行比较,从而判断是否需要删除:

>>> now_utc_ts = time.mktime(time.gmtime())
>>> now_utc_ts
1316398744.0
>>> now_utc_ts >= expiration_tc_ts
True

我遇到的困难是,想把存储的UTC时间戳转换回原来的本地格式。我从最初的转换中存储了时差小时数,还有一个我解析出来的字符串用于存储时区标签:

>>> print offset_hours
-4
>>> print timezone
EDT

我想把UTC时间戳转换回本地格式的时间,但转换回datetime似乎不太成功:

>>> import datetime
>>> datetime.datetime.fromtimestamp(expiration_utc_ts) + datetime.timedelta(hours=offset_hours)
datetime.datetime(2011, 9, 18, 16, 52) # The hour is 16 but it should be 15

看起来时间差了一个小时。我不太确定错误是怎么产生的?我又做了一个测试,结果也差不多:

>>> # Running this at 21:29pm EDT
>>> utc_now = datetime.datetime.utcnow()
>>> utc_now_ts = time.mktime(right_now.utctimetuple())
>>> datetime.datetime.fromtimestamp(utc_now_ts)
datetime.datetime(2011, 9, 18, 22, 29, 47) # Off by 1 hour

有人能帮我找出我的错误吗?我不确定这是不是夏令时的问题?我看到一些信息让我觉得可能是在尝试将我的日期和时间本地化,但到现在为止我还是很困惑。我希望能以不考虑时区的方式进行所有这些计算和比较。

2 个回答

0

datetime.fromtimestamp() 是获取本地时间的正确方法,它是从 POSIX 时间戳转换过来的。你提到的问题在于,你使用 time.mktime() 将一个带时区的日期时间对象转换为 POSIX 时间戳,这样做是不对的。这里有一种正确的方法

expiration_utc_ts = (d - datetime(1970, 1, 1, tzinfo=utc)).total_seconds()
local_dt = datetime.fromtimestamp(expiration_utc_ts)
3

问题是夏令时被应用了两次。

举个简单的例子:

>>> time_tuple = datetime(2011,3,13,2,1,1).utctimetuple()
time.struct_time(tm_year=2011, tm_mon=3, tm_mday=13, tm_hour=2, tm_min=1, tm_sec=1, tm_wday=6, tm_yday=72, tm_isdst=0)
>>> datetime.fromtimestamp(time.mktime(time_tuple))
datetime.datetime(2011, 3, 13, 3, 1, 1)

我相当确定问题出在 time.mktime() 这个函数上。正如它在文档中所说:

这个函数是 localtime() 的反向函数。它的参数是一个时间结构体或者完整的9元组(因为需要夏令时标志;如果不知道,就用-1作为夏令时标志),这个时间是以本地时间表示的,而不是UTC时间。它返回一个浮点数,以便与 time() 兼容。如果输入的值无法表示为有效时间,会抛出 OverflowErrorValueError(这取决于无效值是被Python捕获还是被底层的C库捕获)。它能生成时间的最早日期依赖于平台。

当你把一个时间元组传给 time.mktime() 时,它会期待一个标志,来判断这个时间是否在 夏令时。正如上面所说,utctimetuple() 返回的元组中这个标志是 0,就像它在文档中所说明的:

如果 datetime 实例 d 是“天真”的(naive),那么这和 d.timetuple() 是一样的,只是 tm_isdst 被强制设为0,无论 d.dst() 返回什么。对于UTC时间,夏令时从不生效。

如果 d 是“有意识的”(aware),d 会通过减去 d.utcoffset() 来标准化为UTC时间,并返回标准化时间的 time.struct_timetm_isdst 被强制设为0。请注意,如果 d.year 是 MINYEARMAXYEAR,结果的 tm_year 成员可能会是 MINYEAR-1MAXYEAR+1,因为UTC调整可能会跨越年份边界。

由于你告诉 time.mktime() 你的时间不是夏令时,而它的工作是将所有时间转换为本地时间,而你所在的地区现在正处于夏令时,所以它增加了一个小时来调整为夏令时。这就是结果的原因。


虽然我手头没有相关的帖子,但几天前我看到一个方法,可以将带时区的日期时间转换为本地时间的“天真”日期时间。这可能比你目前的做法更适合你的应用(使用了优秀的pytz模块):

import pytz
def convert_to_local_time(dt_aware):
    tz = pytz.timezone('America/Los_Angeles') # Replace this with your time zone string
    dt_my_tz = dt_aware.astimezone(tz)
    dt_naive = dt_my_tz.replace(tzinfo=None)
    return dt_naive

将 'America/LosAngeles' 替换为你自己的时区字符串,你可以在 pytz.all_timezones 中找到。

撰写回答