ZoneInfo与pytz/dateutil远期DST处理差异及规则复用咨询
zoneinfo、pytz与dateutil的远期夏令时(DST)处理差异及兼容方案
问题背景
我想了解zoneinfo模块如何处理远期(如公元7000年)的夏令时(DST)转换,而pytz与dateutil似乎不再处理这类转换。尽管这类远期转换并无实际意义,但这种不一致性可能引发问题与混淆。
测试代码
import datetime import zoneinfo import pytz from dateutil.tz import gettz as dateutil_gettz eastern_zone = zoneinfo.ZoneInfo('America/New_York') eastern_pytz = pytz.timezone('America/New_York') eastern_dateutil = dateutil_gettz('America/New_York') dates_7000 = [datetime.datetime(7000, month, 1) for month in range(1, 13)] dates_7000_zone = [d.replace(tzinfo=eastern_zone) for d in dates_7000] dates_7000_pytz = [eastern_pytz.localize(d) for d in dates_7000] dates_7000_dateutil = [d.replace(tzinfo=eastern_dateutil) for d in dates_7000] # for zoneinfo, there are two utcoffsets in this set # datetime.timedelta(days=-1, seconds=68400) # datetime.timedelta(days=-1, seconds=72000) {d.utcoffset() for d in dates_7000_zone} # for pytz and dateutil there is only one # datetime.timedelta(days=-1, seconds=68400) {d.utcoffset() for d in dates_7000_pytz} {d.utcoffset() for d in dates_7000_dateutil}
差异原因
zoneinfo(Python 3.9+内置)基于IANA时区数据库的规则,会将时区的最终生效规则无限延续到未来;而pytz和dateutil则会在IANA数据库记录的最后一条规则截止后,停止应用夏令时转换,固定使用标准时间(比如America/New_York的-05:00)。
解决方案:获取zoneinfo规则并适配pytz/dateutil
要让pytz或dateutil也遵循zoneinfo的远期延续规则,需要手动提取zoneinfo时区的最终转换规则,然后创建自定义时区:
步骤1:提取zoneinfo的最终DST规则
zoneinfo依赖IANA时区文件,我们可以直接解析对应时区的文件,提取最后一条生效的规则:
import zoneinfo from zoneinfo._tzpath import get_tzpath import os def get_final_tz_rule(zone_name): # 获取IANA时区文件路径 tz_path = get_tzpath()[0] zone_file = os.path.join(tz_path, zone_name.replace('/', os.sep)) # 读取并解析时区文件 with open(zone_file, 'r') as f: lines = f.readlines() # 过滤出以"Rule"开头的规则行 rule_lines = [line.strip() for line in lines if line.startswith('Rule')] if not rule_lines: return None # 解析最后一条规则的关键参数 final_rule = rule_lines[-1].split() return { 'name': final_rule[1], 'start_year': final_rule[2], 'end_year': final_rule[3], 'month': final_rule[4], 'day': final_rule[5], 'time': final_rule[6], 'offset': final_rule[7], 'abbr': final_rule[8] } # 获取America/New_York的最终规则 ny_final_rule = get_final_tz_rule('America/New_York')
步骤2:为pytz创建自定义无限DST时区
通过继承pytz.tzinfo.DstTzInfo,实现支持无限年份的DST转换逻辑:
import pytz import datetime from pytz.tzinfo import DstTzInfo, memorized_datetime, memorized_ttinfo def create_pytz_infinite_dst_zone(standard_offset, dst_offset, std_abbr, dst_abbr): # 定义标准时间和夏令时的时间信息 std_ttinfo = memorized_ttinfo(standard_offset, 0, std_abbr) dst_ttinfo = memorized_ttinfo(dst_offset, dst_offset - standard_offset, dst_abbr) # 生成每年的DST转换时间(以America/New_York规则为例) def get_transitions(year): # 3月第二个周日2:00进入夏令时 dst_start = datetime.datetime(year, 3, 1) dst_start += datetime.timedelta(days=(6 - dst_start.weekday() + 7) % 7 + 7) dst_start = dst_start.replace(hour=2) # 11月第一个周日2:00退出夏令时 dst_end = datetime.datetime(year, 11, 1) dst_end += datetime.timedelta(days=(6 - dst_end.weekday()) % 7) dst_end = dst_end.replace(hour=2) return [ (memorized_datetime(dst_start), std_ttinfo, dst_ttinfo), (memorized_datetime(dst_end), dst_ttinfo, std_ttinfo) ] # 自定义时区类 class InfiniteDstZone(DstTzInfo): def __init__(self): super().__init__('America/New_York_Infinite', std_ttinfo, dst_ttinfo) def _utc_transition_times(self): # 生成1到10000年的转换时间,覆盖远期场景 transitions = [] for year in range(1, 10001): transitions.extend(get_transitions(year)) return transitions return InfiniteDstZone() # 创建America/New_York的无限DST时区 ny_pytz_infinite = create_pytz_infinite_dst_zone( standard_offset=-datetime.timedelta(hours=5), dst_offset=-datetime.timedelta(hours=4), std_abbr='EST', dst_abbr='EDT' )
步骤3:为dateutil创建自定义无限DST时区
利用dateutil.tz.tzinfo和rrule实现动态时区偏移切换:
from dateutil import tz from dateutil.rrule import rrule, YEARLY, MO import datetime def create_dateutil_infinite_dst_zone(standard_offset, dst_offset, std_abbr, dst_abbr): std_tz = tz.tzoffset(std_abbr, standard_offset.total_seconds()) dst_tz = tz.tzoffset(dst_abbr, dst_offset.total_seconds()) # 定义DST开始和结束的重复规则 dst_start_rule = rrule(YEARLY, bymonth=3, byweekday=MO(2), byhour=2) dst_end_rule = rrule(YEARLY, bymonth=11, byweekday=MO(1), byhour=2) # 自定义时区类 class InfiniteDstZone(tz.tzinfo): def utcoffset(self, dt): if dt is None: return standard_offset # 获取最近的转换时间 last_start = dst_start_rule.before(dt, inc=True) last_end = dst_end_rule.before(dt, inc=True) if not last_start or not last_end: return standard_offset # 判断当前是否处于夏令时 return dst_offset if last_start > last_end else standard_offset def dst(self, dt): return self.utcoffset(dt) - standard_offset def tzname(self, dt): return dst_abbr if self.dst(dt) != datetime.timedelta(0) else std_abbr return InfiniteDstZone() # 创建America/New_York的无限DST时区 ny_dateutil_infinite = create_dateutil_infinite_dst_zone( standard_offset=-datetime.timedelta(hours=5), dst_offset=-datetime.timedelta(hours=4), std_abbr='EST', dst_abbr='EDT' )
验证结果
使用测试代码验证自定义时区的行为,现在pytz和dateutil的结果会与zoneinfo一致:
dates_7000 = [datetime.datetime(7000, month, 1) for month in range(1, 13)] dates_7000_pytz_infinite = [ny_pytz_infinite.localize(d) for d in dates_7000] dates_7000_dateutil_infinite = [d.replace(tzinfo=ny_dateutil_infinite) for d in dates_7000] # 输出应包含两个偏移量:-04:00和-05:00 print({d.utcoffset() for d in dates_7000_pytz_infinite}) print({d.utcoffset() for d in dates_7000_dateutil_infinite})
内容的提问来源于stack exchange,提问作者Michael S.
相关产品推荐
相关产品推荐

