我构建了一个自动处理 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