如何在CEST和UTC时区之间转换datetime对象

10
我不想使用pytz库,因为我正在进行的项目需要介绍依赖项。如果我可以在没有第三方库的情况下实现这一点,我会更加高兴。
当日期处于夏令时时,我无法将日期在中欧时间和协调世界时之间转换。它比我预期的早一个小时:
>>> print from_cet_to_utc(year=2017, month=7, day=24, hour=10, minute=30)
2017-07-24T09:30Z

2017-07-24T08:30Z  # expected

CET比协调世界时提前一个小时,在夏令时期间比协调世界时提前两个小时。因此,我预计在中欧地区的午夏时节,10:30实际上相当于8:30 UTC。

该函数是:

from datetime import datetime, tzinfo, timedelta

def from_cet_to_utc(year, month, day, hour, minute):
    cet = datetime(year, month, day, hour, minute, tzinfo=CET())
    utc = cet.astimezone(tz=UTC())
    return '{:%Y-%m-%d:T%H:%MZ}'.format(utc)

我使用了两个时区信息对象:

class CET(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=1)

    def dst(self, dt):
        return timedelta(hours=2)

class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)

    def dst(self, dt):
        return timedelta(0)

1
注意:Python 3.9+在标准库中具有zoneinfo模块,该模块包含所有您需要的时区处理功能。 - FObersteiner
@MrFuppes 谢谢您的帮助。我现在才开始从2.7迁移到3.8。总有一天... - Peter Wood
1
没问题,放心;-) 以防万一,zoneinfo也可以通过backports.zoneinfo在Python <3.9中使用,并且有一个用于pytz弃用shim - FObersteiner
@MrFuppes,非常有帮助和实用,非常感谢! - Peter Wood
2个回答

8

仅补充@Sergey's answer

要知道何时应该使用DST,您需要获取DST开始和结束的日期/时间的历史数据,并检查是否必须将其应用于指定的datetime。您可以从IANA数据库中获取这些信息,或者在许多在线来源中查找,例如timeanddate网站

另一个细节是CET是一个模糊的名称,因为它被多个时区使用。真正的时区名称使用IANA数据库定义的名称(始终以Region/City格式,如Europe/ParisEurope/Berlin)。


间隙和重叠

我将以Europe/Berlin时区为例。在今年(2017)的柏林夏令时于3月26日开始: 在凌晨2点,时钟向前调整了1小时到3点,偏移值从+01:00变为+02:00

你可以想象时钟直接从凌晨1:59跳到3点,这意味着当地时间在早上2点到2点59分之间在那一天不存在于该时区。这就是所谓的间隙,您应该检查这种情况(一种常见方法是将凌晨2点调整为下一个有效小时 - 在这种情况下,是夏令时中的3点)。

在同一时区,2017年夏令时将于10月29日结束:在凌晨3点,时钟将向后调整1小时到2点,偏移值将从+02:00变为+01:00

这意味着在凌晨2点至2点59分之间的所有本地时间都会存在两次:一次是夏令时(+02:00),一次是非夏令时(+01:00)。这就是所谓的重叠,并且当创建一个落在这种情况下的 datetime 时,你必须决定哪一个你将选择去创建(应该选择夏令时还是非夏令时?你决定!)。
PS:现在使用中欧洲国家采用CET的时间不同,所以如果您处理旧日期,也必须考虑这一点。
另一个细节是夏令时由政府定义,并不能保证规则永远都是这样,您必须相应地更新规则 - 使用pytz的另一个优势是,它的更新是从IANA发布和 PyPI上发布(感谢@Matt Johnson指出这个信息)生成的。
您确定文书工作比手动编写这些规则更糟吗?只需查看如何阅读IANA tz文件,也许您会改变主意。

2
只是为了回答你关于pytz更新的问题,它们是从IANA发布中生成的。 IANA 2017b => pytz 2017.2。 Stuart Bishop进行维护,并在此处发布到pypi:https://pypi.python.org/pypi/pytz#downloads - Matt Johnson-Pint
@MattJohnson 我已经更新了答案并加入了这些信息,非常感谢! - user7605325
1
你和Sergey的回答都非常有帮助。我认为你已经做得最好,让我真正相信应该把这个问题留给专家来解决。我们的解决方案是要求输入UTC时间,这样我们就不需要进行任何转换了(c:)。 - Peter Wood

4
您应仔细阅读时区信息手册,因为其中有详细说明。
首先,dst()通常应返回timedelta(0)timedelta(hours=1),而不是2个或更多小时,很少情况下返回介于两者之间的值(例如30分钟)。这仅是相对于正常TZ偏移量的夏令时偏移量,并非夏令时模式下的完整TZ偏移量。
其次,utcoffset()必须已包括夏令时校正。手册中还描述了一些情况下仅使用dst()方法(例如用于检测是否正在使用夏令时),但除非您这样做,否则它不会自动应用于TZ偏移量。
您的代码应如下所示:
from datetime import datetime, tzinfo, timedelta

class CET(tzinfo):
    def utcoffset(self, dt):
        return timedelta(hours=1) + self.dst(dt)

    def dst(self, dt):
        dston = datetime(year=dt.year, month=3, day=20)
        dstoff = datetime(year=dt.year, month=10, day=20)
        if dston <= dt.replace(tzinfo=None) < dstoff:
            return timedelta(hours=1)
        else:
            return timedelta(0)

class UTC(tzinfo):
    def utcoffset(self, dt):
        return timedelta(0)

    def dst(self, dt):
        return timedelta(0)

def from_cet_to_utc(year, month, day, hour, minute):
    cet = datetime(year, month, day, hour, minute, tzinfo=CET())
    utc = cet.astimezone(tz=UTC())
    return '{:%Y-%m-%d:T%H:%MZ}'.format(utc)


print from_cet_to_utc(year=2017, month=7, day=24, hour=10, minute=30)
# 2017-07-24:T08:30Z

在这里,我大胆假设DST适用于20.03.YYYY到19.10.YYYY之间,其中YYYY是日期/日期时间对象的年份。

通常,这种开启和关闭日期的逻辑要复杂得多:转换只发生在星期日晚上,有时是月底最后一个星期日,而且在不同的年份之间会有所变化,有时在州的主权之间也会有所变化(边界线也随着时间的推移而变化),夏令时法律和时区每隔几年就会改变(你好,俄罗斯)。


3
实际上,你也应该检查一下当天的时间——在中欧,夏令时通常在凌晨2点到3点之间更改,因此仅检查日期是不够精确的。无论如何,我会在日期更改后立即点赞(我今天已经达到了投票限制)。 - user7605325
目前,春季是凌晨2:00,秋季是凌晨3:00。https://www.timeanddate.com/time/change/france/paris - Matt Johnson-Pint
1
@Hugo,你说得对。感谢你对我的回答的补充。然而,即使是你所描述的,也只是冰山一角。我故意只回答了关于Python和datetime.tzinfo部分的问题,因为这似乎是问题作者遇到的问题。虽然提到实际的时区计算是一个非常困难和庞大的话题。因为它太大了,以至于人们可以纯粹地写一本关于日期和时间正确处理的书(不是开玩笑)。 - Sergey Vasilyev
2
@SergeyVasilyev,实际上我们在时区方面只是刚刚开始涉及。我希望我们能说服OP使用适当的API,而不是重复造轮子。 - user7605325

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接