如何仅使用标准库将UTC日期时间转换为本地日期时间?

332
我有一个使用datetime.utcnow()创建并持久化到数据库的python datetime实例。 为了显示,我希望将从数据库中检索到的datetime实例转换为本地datetime,并使用默认本地时区(就像使用datetime.now()创建datetime一样)。 如何仅使用Python标准库(例如无需pytz依赖项)将UTCdatetime转换为本地datetime? 似乎可以使用datetime.astimezone(tz)来解决问题,但是如何获取默认的本地时区?

时间以哪种格式持久化到数据库中的?如果它是标准格式,那么您可能不需要进行任何转换。 - Apalala
2
这个答案展示了使用pytz的简单方法。 - juan
1
没有使用 pytz 的情况下找不到答案。感觉很傻。 - Kuo
14个回答

444

在 Python 3.3+ 中:

from datetime import datetime, timezone

def utc_to_local(utc_dt):
    return utc_dt.replace(tzinfo=timezone.utc).astimezone(tz=None)

在Python 2/3中:

import calendar
from datetime import datetime, timedelta

def utc_to_local(utc_dt):
    # get integer timestamp to avoid precision lost
    timestamp = calendar.timegm(utc_dt.timetuple())
    local_dt = datetime.fromtimestamp(timestamp)
    assert utc_dt.resolution >= timedelta(microseconds=1)
    return local_dt.replace(microsecond=utc_dt.microsecond)

使用pytz(适用于Python 2/3):

import pytz

local_tz = pytz.timezone('Europe/Moscow') # use your local timezone name here
# NOTE: pytz.reference.LocalTimezone() would produce wrong result here

## You could use `tzlocal` module to get local timezone on Unix and Win32
# from tzlocal import get_localzone # $ pip install tzlocal

# # get local timezone    
# local_tz = get_localzone()

def utc_to_local(utc_dt):
    local_dt = utc_dt.replace(tzinfo=pytz.utc).astimezone(local_tz)
    return local_tz.normalize(local_dt) # .normalize might be unnecessary

示例

def aslocaltimestr(utc_dt):
    return utc_to_local(utc_dt).strftime('%Y-%m-%d %H:%M:%S.%f %Z%z')

print(aslocaltimestr(datetime(2010,  6, 6, 17, 29, 7, 730000)))
print(aslocaltimestr(datetime(2010, 12, 6, 17, 29, 7, 730000)))
print(aslocaltimestr(datetime.utcnow()))

输出

Python 3.3
2010-06-06 21:29:07.730000 MSD+0400
2010-12-06 20:29:07.730000 MSK+0300
2012-11-08 14:19:50.093745 MSK+0400
Python 2
2010-06-06 21:29:07.730000 
2010-12-06 20:29:07.730000 
2012-11-08 14:19:50.093911 
pytz
2010-06-06 21:29:07.730000 MSD+0400
2010-12-06 20:29:07.730000 MSK+0300
2012-11-08 14:19:50.146917 MSK+0400

注意:它考虑了DST和MSK时区最近的UTC偏移更改。

我不知道非pytz解决方案在Windows上是否可行。


3
“normalize”是什么意思? - avi
4
一般来说:pytz: 在转换时区时为什么需要使用normalize?。注意:由于.astimezone()的源时区是UTC,因此在目前的实现中不需要使用.normalize() - jfs
2
当我尝试使用pytz解决方案时,出现了“TypeError: replace() takes no keyword arguments”的错误。 - Craig
2
Python 3.3+的快速提示。要进行相反的转换(本地时间到UTC时间),可以使用以下方法:local_dt.replace(tzinfo=None).astimezone(tz=timezone.utc) - jasonrhaas
2
@Tes3awy 是的,答案中的 DST 代表“夏令时”。 - jfs
显示剩余6条评论

91
自 Python 3.9 起,您可以使用 zoneinfo 模块。 首先让我们使用 utcnow() 获取该时间:
>>> from datetime import datetime
>>> database_time = datetime.utcnow()
>>> database_time
datetime.datetime(2021, 9, 24, 4, 18, 27, 706532)

然后创建时区:

>>> from zoneinfo import ZoneInfo
>>> utc = ZoneInfo('UTC')
>>> localtz = ZoneInfo('localtime')

然后进行转换。为了在不同时区之间进行转换,必须知道日期时间所在的时区,然后使用astimezone()函数即可:

>>> utctime = database_time.replace(tzinfo=utc)
>>> localtime = utctime.astimezone(localtz)
>>> localtime
datetime.datetime(2021, 9, 24, 6, 18, 27, 706532, tzinfo=zoneinfo.ZoneInfo(key='localtime'))

对于Python 3.6到3.8,您需要使用backports.zoneinfo模块:

>>> try:
>>>     from zoneinfo import ZoneInfo
>>> except ImportError:
>>>     from backports.zoneinfo import ZoneInfo

其余部分相同。

对于早于该版本的版本,需要使用pytzdateutil。 dateutil 的工作方式类似于 zoneinfo:

>>> from dateutil import tz
>>> utc = tz.gettz('UTC')
>>> localtz = tz.tzlocal()

The Conversion:
>>> utctime = now.replace(tzinfo=UTC)
>>> localtime = utctime.astimezone(localtz)
>>> localtime
datetime.datetime(2010, 12, 30, 15, 51, 22, 114668, tzinfo=tzlocal())

pytz 有一个不同的接口,这是由于 Python 的时区处理不处理模糊时间所导致的:

>>> import pytz
>>> utc = pytz.timezone('UTC')
# There is no local timezone support, you need to know your timezone
>>> localtz = pytz.timezone('Europe/Paris')

>>> utctime = utc.localize(database_time)
>>> localtime = localtz.normalize(utctime.astimezone(localtz))
>>> localtime

pytz确实更好地处理了夏令时的转换。 - Lennart Regebro
2
更新:现在pytz和dateutil都支持Python 3。 - Lennart Regebro
1
@J.F.Sebastian:dateutil 无法区分在 DST 更改期间发生的两个 1:30 时间。如果您需要能够区分这两个时间,则解决方法是使用可以处理它的 pytz。 - Lennart Regebro
@J.F.Sebastian:也就是说,pytz 是一个解决办法,除非显而易见的那个可行:将所有日期时间保留在UTC中,并仅在显示时进行转换。但是即使使用pytz,你也应该始终这样做。 - Lennart Regebro
3
为什么不使用简单的datetime.now(timezone.utc),避免混淆的utcnow加上额外的替换步骤?另外,ZoneInfo('localtime')是特定于平台的。 - FObersteiner
显示剩余7条评论

27

Python 3.9添加了zoneinfo模块,因此现在可以按照以下方式完成(仅限stdlib):

from zoneinfo import ZoneInfo
from datetime import datetime

utc_unaware = datetime(2020, 10, 31, 12)  # loaded from database
utc_aware = utc_unaware.replace(tzinfo=ZoneInfo('UTC'))  # make aware
local_aware = utc_aware.astimezone(ZoneInfo('localtime'))  # convert

中欧时间比协调世界时快1到2个小时,因此local_aware是:

datetime.datetime(2020, 10, 31, 13, 0, tzinfo=backports.zoneinfo.ZoneInfo(key='localtime'))

作为{{str}}:
2020-10-31 13:00:00+01:00

Windows 没有 系统时区数据库,因此需要安装额外的软件包:

pip install tzdata  

有一个后移版本可用于在Python 3.6到3.8中使用:

sudo pip install backports.zoneinfo

然后:

from backports.zoneinfo import ZoneInfo

1
你甚至不需要 zoneinfo - 参见 jfs' answer 的第一部分(Python3.3+ 部分);-) - FObersteiner
1
@MrFuppes 我认为ZoneInfo('localtime')tz=None更清晰(毕竟,人们可能期望tz=None会删除时区或返回UTC而不是本地时间)。 - xjcl
1
@MrFuppes 另外,这个问题可能涉及到 OP 拥有一个网站,并希望以用户本地时间显示时间戳。为此,您需要将其转换为用户明确的时区,而不是本地时间。 - xjcl
2
你对Win10的注释可能是正确的,因为Python 3.9没有使用它,但自Windows 10 1703(发布于2017年)以来,Win10已经包含了IANA ICU数据,但在1903中得到了改进。根据Raymond Chen的说法,他们现在建议应用程序使用它而不是Windows注册表TZ数据。https://devblogs.microsoft.com/oldnewthing/20210527-00/?p=105255 - yzorg
好的回答,希望有朝一日能够成为最佳答案。 - wim
1
安装了 tzdata 后,我在 Windows 上遇到了一个 错误。它抱怨 ZoneInfo('localtime')。错误信息是:zoneinfo._common.ZoneInfoNotFoundError: 找不到键为 localtime 的时区。 - Wavesailor

15

标准库无法完成此操作。使用 pytz 模块,您可以将任何naive/aware datetime对象转换为任何其他时区。以下是一些使用Python 3的示例。

通过类方法 utcnow() 创建的naive对象

要将naive对象转换为其他任何时区,首先必须将其转换为aware datetime对象。您可以使用replace方法将naive datetime对象转换为aware datetime对象。然后,要将aware datetime对象转换为任何其他时区,可以使用astimezone方法。

变量pytz.all_timezones为您提供了pytz模块中所有可用时区的列表。

import datetime,pytz

dtobj1=datetime.datetime.utcnow()   #utcnow class method
print(dtobj1)

dtobj3=dtobj1.replace(tzinfo=pytz.UTC) #replace method

dtobj_hongkong=dtobj3.astimezone(pytz.timezone("Asia/Hong_Kong")) #astimezone method
print(dtobj_hongkong)

通过类方法 now() 创建的原始对象

now 方法返回当前的日期和时间,因此您需要先将 datetime 对象设置为时区感知模式。使用 localize 函数将 原始 的 datetime 对象转换为时区感知模式。然后,您可以使用 astimezone 方法将其转换为另一个时区。

dtobj2=datetime.datetime.now()

mytimezone=pytz.timezone("Europe/Vienna") #my current timezone
dtobj4=mytimezone.localize(dtobj2)        #localize function

dtobj_hongkong=dtobj4.astimezone(pytz.timezone("Asia/Hong_Kong")) #astimezone method
print(dtobj_hongkong)

8
在Alexei的评论基础上进行拓展。这也适用于夏令时。
import time
import datetime

def utc_to_local(dt):
    if time.localtime().tm_isdst:
        return dt - datetime.timedelta(seconds = time.altzone)
    else:
        return dt - datetime.timedelta(seconds = time.timezone)

6
我想我理解了:计算自纪元以来的秒数,然后使用time.localtime将其转换为本地时区,最后将时间结构体转换回datetime...
EPOCH_DATETIME = datetime.datetime(1970,1,1)
SECONDS_PER_DAY = 24*60*60

def utc_to_local_datetime( utc_datetime ):
    delta = utc_datetime - EPOCH_DATETIME
    utc_epoch = SECONDS_PER_DAY * delta.days + delta.seconds
    time_struct = time.localtime( utc_epoch )
    dt_args = time_struct[:6] + (delta.microseconds,)
    return datetime.datetime( *dt_args )

它正确地应用夏令时/冬令时:

>>> utc_to_local_datetime( datetime.datetime(2010, 6, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 6, 6, 19, 29, 7, 730000)
>>> utc_to_local_datetime( datetime.datetime(2010, 12, 6, 17, 29, 7, 730000) )
datetime.datetime(2010, 12, 6, 18, 29, 7, 730000)

1
唉,但至少在Unix上应该可以工作。对于Windows来说,如果您的夏令时规则自转换日期以来发生了变化,它可能是不正确的。为什么不想使用库呢? - Lennart Regebro
1
如果你需要Python3,那么你就没那么幸运了。但是,相比于使用库,研究和制作自己的解决方案就有些过度了。 - Lennart Regebro
1
+1(为了抵消负分:寻求仅使用标准库的解决方案可能是有效的要求),但要注意@Lennart提到的警告。鉴于dateutil可能会失败在Unix和Windows上都有可能。顺便说一下,您可以从代码中提取utctotimestamp(utc_dt) -> (seconds, microseconds)以获得更清晰的代码,请参见Python 2.6-3.x implementation - jfs
1
pytz(和dateutil)自一段时间以来就可以在Python 3上运行,所以Python3/Windows/质量问题不再是一个问题。 - Lennart Regebro
我使用一款软件,它使用PythonAXScript作为扩展语言。虽然我有时会使用另一个库,但是当我们将Python脚本转移到其他机器上与该软件一起使用时,这对我们的团队来说非常麻烦。尽可能使用标准库方法更符合我的使用逻辑,即使有小细节需要注意。感谢您的帮助!同时点个赞+1 - Dave_750
显示剩余2条评论

4
标准的Python库中没有任何tzinfo实现。我一直认为这是datetime模块一个令人惊讶的缺点。 tzinfo类的文档提供了一些有用的示例。请查看该部分末尾的大型代码块。

1
我猜核心实现的喜好是因为时区数据库需要定期更新,因为一些国家没有固定规则来确定夏令时周期。如果我没记错的话,例如JVM需要更新以获取最新的时区数据库... - Nitro Zark
@Nitro Zark,至少他们应该为UTC提供一个。os模块可以基于操作系统功能提供一个本地时间的函数。 - Mark Ransom
2
我正在查看3.2版本的新功能列表,发现他们在3.2中增加了UTC时区(http://docs.python.org/dev/whatsnew/3.2.html#datetime)。但似乎并没有本地时区... - Nitro Zark

3

使用time.timezone,它会返回一个整数,表示与UTC时间相差的秒数。

例如:

from datetime import datetime, timedelta, timezone
import time

# make datetime from timestamp, thus no timezone info is attached
now = datetime.fromtimestamp(time.time())

# make local timezone with time.timezone
local_tz = timezone(timedelta(seconds=-time.timezone))

# attach different timezones as you wish
utc_time = now.astimezone(timezone.utc)
local_time = now.astimezone(local_tz)

print(utc_time.isoformat(timespec='seconds')) 
print(local_time.isoformat(timespec='seconds'))

在我的电脑上(Python 3.7.3),它显示:
2021-05-07T12:50:46+00:00
2021-05-07T20:50:46+08:00

相当简单,只使用标准库 ~

1
我发现最简单的方法是获取您所在地的时区偏移量,然后从小时中减去该偏移量。
def format_time(ts,offset):
    if not ts.hour >= offset:
        ts = ts.replace(day=ts.day-1)
        ts = ts.replace(hour=ts.hour-offset)
    else:
        ts = ts.replace(hour=ts.hour-offset)
    return ts

这对我来说可行,在Python 3.5.2中。

0

这里有另一种在日期时间格式中更改时区的方法(我知道我浪费了精力在这上面,但我没有看到这个页面,所以我不知道该怎么做),不需要分钟和秒,因为我对我的项目没有用:

def change_time_zone(year, month, day, hour):
      hour = hour + 7 #<-- difference
      if hour >= 24:
        difference = hour - 24
        hour = difference
        day += 1
        long_months = [1, 3, 5, 7, 8, 10, 12]
        short_months = [4, 6, 9, 11]
        if month in short_months:
          if day >= 30:
            day = 1
            month += 1
            if month > 12:
              year += 1
        elif month in long_months:
          if day >= 31:
            day = 1
            month += 1
            if month > 12:
              year += 1
        elif month == 2:
          if not year%4==0:
            if day >= 29:
              day = 1
              month += 1
              if month > 12:
                year += 1
          else:
            if day >= 28:
              day = 1
              month += 1
              if month > 12:
                year += 1
      return datetime(int(year), int(month), int(day), int(hour), 00)

1
使用timedelta在不同时区之间进行切换。你只需要知道两个时区之间的小时偏移量,就不必为datetime对象的6个元素调整边界。timedelta也可以轻松处理闰年、闰世纪等问题。你需要从datetime模块中导入timedelta。如果偏移量是一个变量(以小时为单位),则可以使用timeout = timein + timedelta(hours = offset)来计算,其中timein和timeout都是datetime对象。例如,timein + timedelta(hours = -8)将GMT转换为PST。 - Karl Rudnick

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