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

How to find next day's Unix timestamp for same hour, including DST, in Python?

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

1
2
3
4
5
6
7
8
9
10
11
>>> 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时间戳是好的。这个时区也是我当地的时区,这个时间戳似乎是正确的:

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

现在,时钟在11月3日凌晨2点在蒙特利尔调整了一个小时,所以我们当天增加了一小时。这意味着2013/11/03至2013/11/04之间有25小时。这表明:

1
2
3
4
5
6
>>> 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,但我相信它不知道时区或夏令时:

1
2
3
4
5
>>> 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之间发生的所有日志条目。

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


不使用新包装:

1
2
3
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))

完整脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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。)


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

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

要查找给定时区明天午夜(或其他固定小时)的POSIX时间戳,您可以使用以下代码来获取给定时区的"午夜"UTC时间?

1
2
3
4
5
6
7
8
9
10
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()方法之外的.localize()方法不可避免的DST转换期间解析输入日期时出现歧义,您可以使用与日期一起存储的Unix时间戳:

1
2
3
4
5
6
7
8
9
10
11
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()

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

1
2
3
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()):

1
2
3
4
5
6
7
8
9
10
11
12
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()方法更改其输入(在这种情况下检测到不存在的时间)以与其他代码示例保持一致,则可以在此处引发异常。


这是基于dateutil的替代方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
>>> # 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


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

使用Arrow,此操作变得简单:

1
2
3
4
5
6
7
8
9
10
11
12
>>> 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日。