如何在 Python 中查找同一小时的第二天 Unix 时间戳,包括 DST?

Posted

技术标签:

【中文标题】如何在 Python 中查找同一小时的第二天 Unix 时间戳,包括 DST?【英文标题】:How to find next day's Unix timestamp for same hour, including DST, in Python? 【发布时间】:2013-12-15 02:29:59 【问题描述】:

在 Python 中,我可以找到本地时间的 Unix 时间戳,知道时区,就像这样(使用 pytz):

>>> import datetime as DT
>>> import pytz
>>> mtl = pytz.timezone('America/Montreal')
>>> naive_time3 = DT.datetime.strptime('2013/11/03', '%Y/%m/%d')
>>> naive_time3
datetime.datetime(2013, 11, 3, 0, 0)
>>> localized_time3 = mtl.localize(naive_time3)
>>> localized_time3
datetime.datetime(2013, 11, 3, 0, 0, tzinfo=<DstTzInfo 'America/Montreal' EDT-1 day, 20:00:00 DST>)
>>> localized_time3.timestamp()
1383451200.0

到目前为止,一切都很好。 naive_time 不知道时区,而 localized_time 知道它在 2013/11/03 蒙特利尔的午夜,因此 (UTC) Unix 时间戳很好。这个时区也是我的本地时区,这个时间戳似乎是正确的:

$ date -d @1383451200
Sun Nov  3 00:00:00 EDT 2013

现在,蒙特利尔的时钟已于 11 月 3 日 2:00 提前一小时调整,因此我们当天多出了一个小时。这意味着在 2013/11/03 和 2013/11/04 之间有 25 小时。这表明它:

>>> naive_time4 = DT.datetime.strptime('2013/11/04', '%Y/%m/%d')
>>> localized_time4 = mtl.localize(naive_time4)
>>> localized_time4
datetime.datetime(2013, 11, 4, 0, 0, tzinfo=<DstTzInfo 'America/Montreal' EST-1 day, 19:00:00 STD>)
>>> (localized_time4.timestamp() - localized_time3.timestamp()) / 3600
25.0

现在,我正在寻找一种从localized_time3 获取localized_time4 对象的简单方法,因为我知道我想在同一时间(此处为午夜)获取下一个本地化日期.我试过timedelta,但我相信它不知道时区或夏令时:

>>> localized_time4td = localized_time3 + DT.timedelta(1)
>>> localized_time4td
datetime.datetime(2013, 11, 4, 0, 0, tzinfo=<DstTzInfo 'America/Montreal' EDT-1 day, 20:00:00 DST>)
>>> (localized_time4td.timestamp() - localized_time3.timestamp()) / 3600
24.0

我的目的是获取有关每个本地日期的日志条目的信息,这些条目与它们的 Unix 时间戳一起存储。当然,如果我使用localized_time3.timestamp() 并在此处添加24 * 3600(与localized_time4td.timestamp() 相同),我将错过localized_time4td.timestamp()localized_time4td.timestamp() + 3600 之间发生的所有日志条目。

换句话说,我正在寻找的函数或方法应该知道何时将 25 小时、24 小时或 23 小时添加到 Unix 时间戳,具体取决于 DST 转换发生的时间。

【问题讨论】:

必须是std lib模块还是可以使用pip模块? 如果轻量级 PyPI 模块提供了非常简单的解决方案,我愿意使用它们。例如,我打算使用pytz 我建议你看看优秀的 Arrow (crsmithdev.com/arrow)。 @rdodev:Arrow 似乎效果很好(刚刚尝试过)!您想通过工作示例提供答案(我会接受),还是应该使用 Arrow 回答我自己的问题?还是谢谢你。 很高兴为您提供帮助!来吧,自己回答。我的时间有点短。 【参考方案1】:

(感谢@rdodev 将我指向Arrow)。

使用箭头,这个操作变得简单:

>>> import arrow
>>> import datetime as DT
>>> lt3 = arrow.get(DT.datetime(2013, 11, 3), 'America/Montreal')
>>> lt3
<Arrow [2013-11-03T00:00:00-04:00]>
>>> lt4 = arrow.get(DT.datetime(2013, 11, 4), 'America/Montreal')
>>> lt4
<Arrow [2013-11-04T00:00:00-05:00]>
>>> lt4.timestamp - (lt3.replace(days=1).timestamp)
0
>>> (lt3.replace(days=1).timestamp - lt3.timestamp) / 3600
25.0

使用 Arrow 的 replace 方法,单数单位名称替换该属性,而复数添加它。所以lt3.replace(days=1) 是 2013 年 11 月 4 日,而 lt3.replace(day=1) 是 2013 年 11 月 1 日。

【讨论】:

Arrow 如何处理ambiguous or non-existent times? 嗯,2013 年 11 月 3 日 1:30 在蒙特利尔似乎默认为无 DST,因为它输出 &lt;Arrow [2013-11-03T01:30:00-05:00]&gt;(而 0:59 有 DST:&lt;Arrow [2013-11-03T00:59:00-04:00]&gt;)。 2014-03-09 02:45:00等不存在的时间呢?【参考方案2】:

不使用新包:

def add_day(x):
    d = x.date()+DT.timedelta(1)
    return mtl.localize(x.replace(year=d.year, month=d.month, day=d.day, tzinfo=None))

完整脚本:

import datetime as DT
import pytz
import calendar
mtl = pytz.timezone('America/Montreal')
naive_time3 = DT.datetime.strptime('2013/11/03', '%Y/%m/%d')
print repr(naive_time3)
#datetime.datetime(2013, 11, 3, 0, 0)
localized_time3 = mtl.localize(naive_time3)
print repr(localized_time3)
#datetime.datetime(2013, 11, 3, 0, 0, tzinfo=<DstTzInfo 'America/Montreal' EDT-1 day, 20:00:00 DST>)
print calendar.timegm(localized_time3.utctimetuple())
#1383451200.0
def add_day(x):
    d = x.date()+DT.timedelta(1)
    return mtl.localize(x.replace(year=d.year, month=d.month, day=d.day, tzinfo=None))
print repr(add_day(localized_time3))
#datetime.datetime(2013, 11, 4, 0, 0, tzinfo=<DstTzInfo 'America/Montreal' EST-1 day, 19:00:00 STD>)

calendar 用于 Python2。)

【讨论】:

我真的很喜欢这个解决方案!它不像使用 Arrow 那样简单,它确实内置了这个功能,但它仍然可以在没有外部部门的情况下完成工作。谢谢! 它不处理不明确或不存在的时间。你可以fix it using .dst(), .normalize()` methods 感谢您指出这一点,J.F.塞巴斯蒂安。但是,在我的具体情况下,我只需要支持午夜(即用户只指定日期,而不是日期/时间)。因此,模棱两可/不存在的时间永远不会成为问题。有趣的是,如果有人需要它,也有一个解决方案,在你对我的问题的回答中给出。 @eepp:时区 UTC 偏移量可能会在午夜发生变化(这并不常见,除非您在巴西利亚,但 it happens)。如果你想断言你的代码没有处理它,那么使用is_dst=None 来检测它。要获得午夜:datetime.combine(some_date, time(0, 0)) 可以用来代替 x.replace(year=d.year, ...【参考方案3】:

这里是基于dateutil的替代方案:

>>> # In Spain we changed DST 10/26/2013
>>> import datetime
>>> import dateutil.tz
>>> # tzlocal gets the timezone of the computer
>>> dt1 = datetime.datetime(2013, 10, 26, 14, 00).replace(tzinfo=dateutil.tz.tzlocal())
>>> print dt1
2013-10-26 14:00:00+02:00
>>> dt2 = dt1 + datetime.timedelta(1)
>>> print dt2
2013-10-27 14:00:00+01:00
# see if we hace 25 hours of difference
>>> import time
>>> (time.mktime(dt2.timetuple()) - time.mktime(dt1.timetuple())) / 3600.0
25.0
>>> (float(dt2.strftime('%s')) - float(dt1.strftime('%s'))) / 3600   # the same
25.0

【讨论】:

dateutil might fail during DST transitions。如果操作系统的历史时区数据库未使用或不存在(Windows),它可能会因过去的日期而失败。它不处理模棱两可、不存在的时间。 我用我的时区 (CEST -> CET) 测试了 DST 转换,如示例所示。我只将它用于最近和未来的日期。感谢您的提示。 时区信息一直在变化。有recent changes in 2013。我相信这些变化在未来不会停止。【参考方案4】:

在这个答案的最后,我逐渐提供了几种解决方案,其中最强大的解决方案试图处理以下问题:

由于 DST 造成的 UTC 偏移 由于与 DST 无关的原因,本地时区可能具有不同 UTC 偏移量的过去日期。 dateutil 和 stdlib 解决方案在某些系统上失败,尤其是 Windows 夏令时时间不明确(不知道Arrow是否提供接口处理) DST 期间不存在的时间(相同)

要在给定时区查找明天午夜(或其他固定时间)的 POSIX 时间戳,您可以使用来自 How do I get the UTC time of “midnight” for a given timezone? 的代码:

from datetime import datetime, time, timedelta
import pytz

DAY = timedelta(1)
tz = pytz.timezone('America/Montreal')

tomorrow = datetime(2013, 11, 3).date() + DAY

midnight = tz.localize(datetime.combine(tomorrow, time(0, 0)), is_dst=None)
timestamp = (midnight - datetime(1970, 1, 1, tzinfo=pytz.utc)).total_seconds()

dt.date() 方法为幼稚和时区感知 dt 对象返回相同的幼稚日期。

时间戳的显式公式用于支持 Python 3.3 之前的 Python 版本。否则.timestamp() 方法可以在 Python 3.3+ 中使用。

为了避免在 .localize() 方法无法避免的 DST 转换期间解析输入日期时出现歧义,除非您知道 is_dst 参数,您可以使用与日期一起存储的 Unix 时间戳:

from datetime import datetime, time, timedelta
import pytz

DAY = timedelta(1)
tz = pytz.timezone('America/Montreal')

local_dt = datetime.fromtimestamp(timestamp_from_the_log, tz)
tomorrow = local_dt.date() + DAY

midnight = tz.localize(datetime.combine(tomorrow, time(0, 0)), is_dst=None)
timestamp = (midnight - datetime(1970, 1, 1, tzinfo=pytz.utc)).total_seconds()

支持其他固定时间(不仅是午夜):

tomorrow = local_dt.replace(tzinfo=None) + DAY # tomorrow, same time
dt_plus_day = tz.localize(tomorrow, is_dst=None)
timestamp = dt_plus_day.timestamp() # use the explicit formula before Python 3.3

is_dst=None 如果结果日期不明确或不存在,则会引发异常。为避免异常,您可以选择与昨天的前一个日期最接近的时间(相同的 DST 状态,即is_dst=local_dt.dst()):

from datetime import datetime, time, timedelta
import pytz

DAY = timedelta(1)
tz = pytz.timezone('America/Montreal')

local_dt = datetime.fromtimestamp(timestamp_from_the_log, tz)
tomorrow = local_dt.replace(tzinfo=None) + DAY

dt_plus_day = tz.localize(tomorrow, is_dst=local_dt.dst())
dt_plus_day = tz.normalize(dt_plus_day) # to detect non-existent times                                            
timestamp = (dt_plus_day - datetime(1970, 1, 1, tzinfo=pytz.utc)).total_seconds()

.localize() 尊重给定的时间,即使它不存在,因此需要.normalize() 来修复时间。如果normalize() 方法更改其输入(在这种情况下检测到不存在时间)以与其他代码示例保持一致,您可以在此处引发异常。

【讨论】:

您为什么要使用is_dst=local_dt.dst() 来确保“距离昨天最接近上一个日期的时间”? localizeis_dst 参数接受 TrueFalseNone,而 datetime.datetimedst() 方法返回 datetime.timedelta。因此,mtl.localize(DT.datetime(2013, 11, 4)).dst() == TrueFalsemtl.localize(DT.datetime(2013, 11, 4)).dst() == True 也是,而第一个有夏令时,第二个没有。 @eepp:避免使用some_python == True,改用bool(some_python_obj)。阅读Truth value testing in Python 很高兴知道!我对 Python 还很陌生,但是感谢像你这样的人,我变得更好了。文档是这样说的:在布尔上下文中,timedelta 对象被认为是真当且仅当它不等于 timedelta(0).。现在这是有道理的。但是,您确实对某些事情有所了解,谢谢。 @eepp: 明确一点:你很少需要显式调用bool()if some_python_obj: ... 照常工作。

以上是关于如何在 Python 中查找同一小时的第二天 Unix 时间戳,包括 DST?的主要内容,如果未能解决你的问题,请参考以下文章

学习python的第二天之函数2.0

冲刺第二天

python学习的第二天

检索未包含在 CoreData 请求结果中的第二天

Python的第二天

《Python编程从入门到实践》——学习python的第十二天