【问题标题】:How to create a Timezone aware Time object that works with all dates?如何创建适用于所有日期的时区感知时间对象?
【发布时间】:2019-04-22 10:47:40
【问题描述】:

我已经为这个问题苦苦挣扎了一段时间。我觉得我很了解 Timezones,而且我认为我了解如何正确使用 pytz,所以我通常使用它,而且我通常没有任何问题。也许我正在尝试使用错误的工具来满足我的需求。

使用我当前的应用程序,我需要处理抽象的 time 对象。也就是说,我关心 16:00 发生的事情。我不在乎它发生在 12 月 23 日的 16:00。似乎 pytz 更喜欢使用 datetime 对象:这是有道理的。由于夏令时和历史原因等原因,它无法计算出偏移量。

我试图让用户协调世界各地的日常活动。如果日本的用户说活动是每天 21:00 到 23:00,我希望美国/中部的用户每天看到 6:00 到 8:00。我以为我有这个工作......直到两周前。你看,美国大部分地区的夏令时刚刚结束,所以现在 6:00-8:00 实际上是之前的 7:00-9:00。

这打破了我通常需要以 UTC 格式存储时间,然后将它们转换为仅用于查看的想法。它创建的时区实际上非常重要。如果我们反转这一点,并且在美国时区观察夏令时的用户设置了一个事件时间,那么即使他们没有观察到日本的时间也需要更改!如果我将该时间存储为 UTC,日本不会有任何变化,但这不是我想要的功能。

所以我想用tzinfo 存储为time 对象。但是你不能在没有日期的情况下创建具有准确 pytz tzinfo 的时间对象,但日期并不重要。如果我使用当前日期来计算tzinfo,那么一旦该时区发生变化,这实际上将不再准确。

我想我的问题是:以一种可以在未来任何时间在世界任何地方检索的方式存储“东部时间下午 4 点”的最佳方式?包括东方!我希望它在 DST 期间和 DST 之外是下午 4 点。我不能将它存储为 UTC,因为 12:00 UTC 与 12:00 UTC 在整个一年中的时间相同,但我不希望这样。我想我想要的是一个“抽象”或“临时”pytz.timezone,直到给出日期(查看日期)才具有实际偏移量。那是一回事吗?我在这个网站上阅读了无数的问题,包括 python 和 pytz 文档,但找不到类似的东西,或者任何有类似问题的人。一切似乎都在谈论特定的 datetimes 或仅在 datetimes 内工作,但这似乎与我的问题无关。

我的应用程序非常庞大,因此很难抽出具体部分,但我可以尝试展示我尝试过的内容以及为什么无法正常工作。

event_time = datetime.time(hour=12, tzinfo=pytz.timezone("US/Eastern")) 将是我理想的解决方案。但是使用 pytz 来创建 tzinfo 并不是一个好主意(由于历史原因,这会给我一个像 -5:04 这样的偏移量) - 有没有办法指定要使用的 US/Eastern 版本?

datetime.now(pytz.timezone("US/Eastern")).replace(hour=12, minute=0, second=0, microsecond=0).timetz() 给了我一些我想要的东西,但它只有在美国/东部不变的情况下才能正常运行。如果我将此应用于我在 DST 更改之前移回的日期,它会给我 13:00,这不是我想要的。

【问题讨论】:

    标签: python-3.x pytz


    【解决方案1】:

    看起来 pytz 通过 'localize' 方法提供了这一点,正如 `this answer 中所建议的那样。

    来自pytz documentation

    如果您坚持使用当地时间,这个库提供了一种明确构建它们的工具:

    eastern = timezone('US/Eastern')
    
    loc_dt = datetime(2002, 10, 27, 1, 30, 00)
    est_dt = eastern.localize(loc_dt, is_dst=True)
    edt_dt = eastern.localize(loc_dt, is_dst=False)
    print(est_dt.strftime(fmt) + ' / ' + edt_dt.strftime(fmt))
    2002-10-27 01:30:00 EDT-0400 / 2002-10-27 01:30:00 EST-0500`
    

    通过存储本地时间和本地时区,您可以在使用时进行转换,并转换为其他地区的时区。

    【讨论】:

    • 我会尝试解开您的建议,因为我不知道这是否解决了我的问题。我想我想要一个模棱两可的时间。是否建议存储创建重复事件的确切 Datetime(例如,东部时间 11 月 20 日下午 4:00)然后,在计算稍后显示的时间时,您首先要找出相关时间,构造一个新的 @ 987654325@ 使用创建它的原始时区名称(可能是不同的偏移量)以及那个小时,然后使用新的Datetime 来计算查看器的显示时间?我认为这可能会奏效......
    • 您好 manvb,我认为您的解释正确。在您的示例中,您希望下午 4 点的约会保持在下午 4 点,即使 DST 发生变化。这将如何影响非 DST 时区的约会,还是会出现这个问题?例如,美国东部时区和日本的用户是否会与同一事件进行交互?
    • 是的,完全正确!每个人都应该在同一时间一起参加同一个活动,但对于创作者来说,它总是在它被定义的时间,不管 DST。
    • 我认为马塞尔的回答应该在这里有效;我的想法是在没有日期的情况下存储“loc_dt”,检查 DST,然后使用正确的 DST 值本地化事件创建者的 local_dt。最后,该时间可以转换为活动参与者的其他时区。 Marcel 的回答处理了他的自定义类中的所有歧义。
    【解决方案2】:

    我构建了一个自动处理 DST 转换的 tzinfo 类。我从 datetime 文档中的 USTimeZone 类示例中得到了这个想法。

    这里的诀窍是 pytz 时区数据库具有夏令时生效的所有历史日期。这也是为什么当您创建没有日期的 datetime 对象时,它会错误地转换 DST;它基于数据库中的第一个条目。

    from datetime import datetime, tzinfo, timedelta
    import pytz
    
    ZERO = timedelta(0)
    
    def format_timedelta(td):
        if td < timedelta(0):
            return '-' + format_timedelta(-td)
        else:
            # Change this to format positive timedeltas the way you want
            return str(td)
    
    
    
    class WorldTimeZone(tzinfo):
        """
        A self adjusting according to DST rules in the PYTZ database tzinfo class
        See pytz.all_timezones for a list of all zone names and offsets.
        """
    
        def __init__(self, zone):
            """
            :param zone:  (str) Proper tzdatabase timze zone name. 
            """
            # initialize the pytz timezone with current time
            # this is done to avoid confusing tznames found in the start of the tz database
            # _utcoffset should always be STD rather than DST.
            self.pytzinfo = self.__getSTD(zone)
            self._utcoffset = self.pytzinfo._utcoffset
    
    
        @staticmethod
        def __getSTD(tname):
            """
            This returns a pytz timezone object normalized to standard time for the zone requested.
            If the zone does not follow DST or a future transition time cannot be found, it normalizes to NOW instead.
    
            :param tname:  Proper timezone name found in the tzdatabase. example: "US/Central"
            """
    
            # This defaults to the STD time for the zone rather than current time which could be DST
            tzone = pytz.timezone(tname)
            NOW = datetime.now(tz=pytz.UTC)
            std_date = NOW
            hasdst = False
            try:
                #transitions are in UTC.  They need to be converted to localtime once we find the correct STD transition.
                for utcdate, info in zip(tzone._utc_transition_times, tzone._transition_info):
                    utcdate = utcdate.replace(tzinfo=pytz.UTC)
                    utcoffset, dstoffset, tzname = info
                    if dstoffset == ZERO:
                        std_date = utcdate
                    if utcdate > NOW:
                        hasdst = True
                        break
            except AttributeError:
                std_date = NOW
            if not hasdst:
                std_date = NOW
            std_date = tzone.normalize(std_date)
            return std_date.tzinfo
    
        # This needs to be dynamic because pytzinfo updates everytime .dst() is called; which is a lot.
        @property
        def _dst(self):
            return self.pytzinfo._dst
    
        def __repr__(self):
            # return self.pytzinfo.__repr__()
            if self._dst:
                dst = 'DST'
            else:
                dst = 'STD'
            if self._utcoffset > timedelta(seconds=0):
                msg = '<WorldTimeZone %r %s+%s %s>'
            else:
                msg = '<WorldTimeZone %r %s%s %s>'
            return msg % (self.pytzinfo.zone, self.pytzinfo._tzname,
                          format_timedelta(self._utcoffset + self._dst), dst)
    
        def __str__(self):
            return "%s %s" % (self.pytzinfo._tzname, self.pytzinfo)
    
        def tzname(self, dt):
            # print "   TZNAME called"
            return "%s %s" % (self.pytzinfo._tzname, self.pytzinfo)
    
        def utcoffset(self, dt):
            # print "   UTCOFFSET CALLED"
            return self._utcoffset + self.dst(dt)
    
        def dst(self, dt):
            # print "   DST CALLED"
            if dt is None or dt.tzinfo is None:
                # An exception may be sensible here, in one or both cases.
                # It depends on how you want to treat them.  The default
                # fromutc() implementation (called by the default astimezone()
                # implementation) passes a datetime with dt.tzinfo is self.
                return ZERO
    
            assert dt.tzinfo is self  # WE ASSUME THE TZINFO ON THE DATE PASSED IN IS OUR TZINFO OBJECT.
            tmpdt = self.pytzinfo.normalize(dt)
            self.pytzinfo = tmpdt.tzinfo
            return tmpdt.tzinfo._dst
    

    示例代码

    EST = WorldTimeZone('US/Eastern')
    PST = WorldTimeZone('US/Pacific')
    dt_dst = datetime(2018, 11, 1, 1, 30, 00)
    dt_std = datetime(2018, 11, 6, 1, 30, 00)
    est_dst = dt_dst.replace(tzinfo=EST)
    est_std = dt_std.replace(tzinfo=EST)
    pst_dst = est_dst.astimezone(PST)
    pst_std = est_std.astimezone(PST)
    
    print(f"{dt_dst} >> {est_dst} >> {pst_dst}")
    print(f"{dt_std} >> {est_std} >> {pst_std}")
    

    输出

    2018-11-01 01:30:00 >> 2018-11-01 01:30:00-04:00 >> 2018-10-31 22:30:00-07:00
    2018-11-06 01:30:00 >> 2018-11-06 01:30:00-05:00 >> 2018-11-05 22:30:00-08:00
    

    【讨论】:

    • 这太棒了!我想你已经涵盖了一切。非常感谢。
    猜你喜欢
    • 2011-10-27
    • 2020-10-15
    • 1970-01-01
    • 2016-08-06
    • 2017-06-26
    • 2021-09-18
    • 1970-01-01
    • 2014-08-23
    相关资源
    最近更新 更多