作者:小猪快跑

基础数学&计算数学,从事优化领域6年+,主要研究方向:MIP求解器、整数规划、随机规划、智能优化算法

python3 的时区是一个很容易出错的地方。本篇将从原理层面剖析时区概念,让读者真正学懂时区,不踩坑。

如有错误,欢迎指正。如有更好的算法,也欢迎交流!!!——@小猪快跑

相关文献

当前时间

from datetime import datetime

print(datetime.now())
# 2024-07-12 14:39:59.531525

前一天日期、后一天日期

from datetime import datetime, timedelta

dt = datetime(2024, 1, 1)

dt + timedelta(1)
# datetime.datetime(2024, 1, 2, 0, 0)

dt - timedelta(1)
# datetime.datetime(2023, 12, 31, 0, 0)

东八区(北京)时间

import pytz
from datetime import datetime

print(datetime(2024, 1, 1, tzinfo=pytz.timezone('Etc/GMT-8')))
# 2024-01-01 00:00:00+08:00

时间戳转换

datetime -> str

import pytz
from datetime import datetime

dt = datetime(2024, 1, 1, tzinfo=pytz.timezone('Etc/GMT-8'))

fmt = '%Y-%m-%d %H:%M:%S%z'
dt.strftime(fmt)
# 2024-01-01 00:00:00+0800

fmt = '%a %d %b %Y, %I:%M%p'
dt.strftime(fmt)
# Mon 01 Jan 2024, 12:00AM

fmt = '%d/%m/%y %H:%M:%S.%f'
dt.strftime(fmt)
# 01/01/24 00:00:00.000000

'The {1} is {0:%d}, the {2} is {0:%B}, the {3} is {0:%I:%M%p}.'.format(dt, "day", "month", "time")
# 'The day is 01, the month is January, the time is 12:00AM.'

str -> datetime

from datetime import datetime

datetime.strptime("21/11/06 16:30", "%d/%m/%y %H:%M")
# datetime.datetime(2006, 11, 21, 16, 30)

datetime -> timestamp(时间戳)

from datetime import datetime

dt = datetime(2024, 1, 1)
dt.timestamp()
# 1704038400.0

timestamp -> datetime

from datetime import datetime

datetime.fromtimestamp(1704038400)
# datetime.datetime(2024, 1, 1, 0, 0)

下面列出了 1989 年 C 标准所要求的所有格式代码,这些代码可在所有使用标准 C 实现的平台上运行。

指令含义样例
%a工作日作为地方缩写名称。Sun, Mon, …, Sat (en_US);So, Mo, …, Sa (de_DE)
%A作为地区全称的工作日。Sunday, Monday, …, Saturday (en_US);Sonntag, Montag, …, Samstag (de_DE)
%w以十进制数字表示的工作日,其中 0 代表周日,6 代表周六。0, 1, …, 6
%d以小数点后 0 位数字表示的月日。01, 02, …, 31
%b月(Month)作为本地语言的缩写名称。Jan, Feb, …, Dec (en_US);Jan, Feb, …, Dez (de_DE)
%B以本地全称表示的月份。January, February, …, December (en_US);Januar, Februar, …, Dezember (de_DE)
%m以十进制零位表示的月份。01, 02, …, 12
%y以小数点后零位的数字表示不带世纪的年份。00, 01, …, 99
%Y带世纪的年为十进制数。0001, 0002, …, 2013, 2014, …, 9998, 9999
%H小时(24 小时制时钟)为零位小数。00, 01, …, 23
%I小时(12 小时钟)为零位小数。01, 02, …, 12
%p当地的上午或下午。AM, PM (en_US);am, pm (de_DE)
%M分钟,小数点后零位。00, 01, …, 59
%S秒(小数点后零位)。00, 01, …, 59
%f微秒为十进制数,置零后为 6 位数。000000, 000001, …, 999999
%zUTC偏移量,格式为±HHMM[SS[.fffffff]](如果对象为空字符串)。(empty), +0000, -0400, +1030, +063415, -030712.345216
%Z时区名称(如果对象为非正则表达式,则为空字符串)。(empty), UTC, GMT
%j以十进制零位形式表示的年日。001, 002, …, 366
%U年份的星期数(星期日为一周的第一天),以十进制零位表示。在新的一年中,第一个星期日之前的所有天数都被视为第 0 周。00, 01, …, 53
%W年的周号(周一为一周的第一天),小数点后加 0。在新的一年中,第一个星期一之前的所有日子都被视为第 0 周。00, 01, …, 53
%c当地语言的日期和时间表示法。Tue Aug 16 21:30:00 1988 (en_US);Di 16 Aug 21:30:00 1988 (de_DE)
%x本地语言的日期表示法。08/16/88 (None);08/16/1988 (en_US);16.08.1988 (de_DE)
%X当地语言中合适的时间表示法。21:30:00 (en_US);21:30:00 (de_DE)
%%字面"%"字符。%

获取日期中的年、季度、月、周、日、小时、分、秒等

from datetime import datetime

dt = datetime.strptime("21/11/06 16:30", "%d/%m/%y %H:%M")

# Using datetime.timetuple() to get tuple of all attributes
tt = dt.timetuple()
for it in tt:   
    print(it)

2006    # year
11      # month
21      # day
16      # hour
30      # minute
0       # second
1       # weekday (0 = Monday)
325     # number of days since 1st January
-1      # dst - method tzinfo.dst() returned None

时区原理

时区问题复杂性的来源

  • DST (Daylight Saving Time) 夏令时

    • 是一种在夏季期间将时钟向前调整一小时的做法,目的是为了在白天较长的季节里更有效地利用自然光照,从而节省能源。在夏令时期间,人们可以享受到更多的日光时间,理论上可以减少照明需求。

    • 夏令时的实施通常遵循以下模式:

      • 在春季,时钟在特定的周末(通常是三月或四月的某个周日)凌晨2点时向前调整一小时,变为3点。

      • 在秋季,时钟则在特定的周末(通常是十月或十一月的某个周日)凌晨2点时向后调整一小时,回到1点。

    • 不过,每个国家和地区的具体规则可能有所不同,有些地区可能不实行夏令时。

    • 在中国,曾经在1986年至1991年间实行过夏令时,具体的调整时间为:

      • 每年从四月中旬第一个星期日的凌晨2时整(北京时间),将时钟拨快一小时,即从2时跳至3时,夏令时开始;
      • 到九月中旬第一个星期日的凌晨2时整(北京夏令时),再将时钟拨回一小时,即从2时跳回至1时,夏令时结束。
    • 自1992年起,中国暂停实行夏令时,目前中国全境全年使用的是北京时间,即东八区的标准时间,没有进行夏令时调整。

  • ST(Standard Time) 冬令时

    • 指的是在不实行夏令时(Daylight Saving Time,DST)调整的时期内所采用的标准时间。在那些实行夏令时的地区,冬令时实际上就是全年时间中的“正常”时间,而在夏令时期间,时钟会向前调快一个小时。
    • 当夏令时结束,通常在每年的秋季,时钟会被拨回一小时,恢复到冬令时。这一调整意味着日落时间会提前,白天的时间会相应缩短,而晚上则会提早变暗。冬令时的目的是在冬季减少能源消耗,尤其是在北半球,因为冬季的日光时间较短,不需要额外延长日光时间来节约能源。
    • 中国在1986年至1991年间曾经实行过夏令时,相应的,在这期间的非夏令时阶段即为所谓的“冬令时”。然而,自1992年起,中国停止实行夏令时,因此也不再有冬令时的概念。目前,中国全境全年使用的是北京时间,即东八区的标准时间(UTC+8),不再进行任何季节性的时间调整。
    • 在世界其他实行夏令时的国家和地区,冬令时通常是从每年的秋季持续到次年的春季,具体日期可能因国家而异。例如,美国和加拿大在每年11月的第一个周日开始冬令时,而欧洲大多数国家则在10月的最后一个周日开始。
  • GMT (Greenwich Mean Time) 格林尼治标准时间

    • 这是以英国格林尼治天文台观测结果得出的时间,这是英国格林尼治当地时间,这个地方的当地时间过去被当成世界标准的时间。
  • UT (Universal Time) 世界时

    • 以本初子午线的平子夜起算的平太阳时。又称格林尼治平时或格林尼治时间。各地的地方平时与世界时之差等于该地的地理经度。1960年以前曾作为基本时间计量系统被广泛应用。由于地球自转速度变化的影响,它不是一种均匀的时间系统。后来世界时先后被历书时原子时所取代,但在日常生活、天文导航、大地测量和宇宙飞行等方面仍属必需;同时,世界时反映地球自转速率的变化,是地球自转参数之一,仍为天文学和地球物理学的基本资料。
  • TAI(International Atomic Time)国际原子时

    • 原子时计量的基本单位:原子时秒。由原子钟导出。
    • 原子时秒的定义:铯 -133 原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间。
    • 1967年第十三届国际计量大会(CGPM)决定,把在海平面实现的上述原子时秒,规定为国际单位制中的时间单位。根据原子时秒的定义,任何原子钟在确定起始历元后,都可以提供原子时。由各实验室用足够精确的铯原子钟导出的原子时称为地方原子时。
    • 全世界大约有20多个国家的不同实验室分别建立了各自独立的地方原子时。国际时间根据比较、综合世界各地原子钟数据,最后确定的原子时,称为国际原子时,简称TAI。
    • TAI的起点是这样规定的:取1958年1月1日0时0分0秒世界时(UT)的瞬间作为同年同月同日0时0分0秒TAI。(事后发现,在该瞬间原子时与世界时的时刻之差为0.0039秒。这一差值就作为历史事实而保留下来。)
    • 在确定原子时起点之后,由于地球自转速度不均匀,世界时与原子时之间的时差便逐年积累。由于世界时存在不均匀性和历书时的测定精度低,自1967年起,原子时已取代历书时作为基本的时间计量系统。
  • UTC (Universal Time Coordinated) 协调世界时:

    • 国际原子时的准确度为每日数纳秒,而世界时的准确度为每日数毫秒。许多应用部门要求时间系统接近世界时UT,对于这种情况,一种称为协调世界时的折中时标于1972年面世。为确保协调世界时与世界时相差不会超过0.9秒,在有需要的情况下会在协调世界时内加上正或负闰秒。因此协调世界时与国际原子时之间会出现若干整数秒的差别,两者之差逐年积累,便采用跳秒(闰秒)的方法使协调时与世界时的时刻相接近,其差不超过1s。它既保持时间尺度的均匀性,又能近似地反映地球自转的变化。 [2]按国际无线电咨询委员会(CCIR)通过的关于UTC的修正案,从1972年1月1日起UTC与UT1(在UT中加入极移改正得到)之间的差值最大可以达到±0.9s。位于巴黎的国际地球自转事务中央局负责决定何时加入闰秒。一般会在每年的6月30日、12月31日的最后一秒进行调整。
    • 原子时 + 自转因素(闰秒) = UTC
  • LMT (Local Mean Time) 地方平时

    • 地方平时是太阳时改变形式修正后,在指定的经度范围内使用一致时间的地方太阳时,他的一致性仅取决于测量用的钟表准确性。
    • 地方平时从19世纪初期开始逐渐被采用,这些地区都不再使用地方太阳时或日晷的时间,直到每个国家都以各种不同的形式将之定为标准时间。标准时间意味着相同的时间使用在一些区域中— 通常,不是从格林威治标准时间中抵销就是选择区域内主要区域的地方时间作为标准时间。地方平时和视太阳时之间的差别就是均时差(equation of time)。
    • 地球划分出不同的时区,每个时区都有一个自己的当地时间。比如上海 LMT 就是 UTC +08:06。
    • 原子时 + 自转因素(闰秒) + 地理因素 = LMT
  • CST时间 China Standard Time

    • 某个国家统一采用某个时区的时间,比如上海的采用标准时间,就是UTC+8小时。
    • 夏令时/冬令时。比如美国会在夏季将时间拨快一个小时。这个时间称之为标准时间。
    • 历史因素:比如中国曾在 1986 年到 1991 年的内实行过夏令时,以上海为例,他在1988年8月份的标准时间就是 UTC + 9:00, 而在 1988年 12 月份标准时间是 UTC + 8:00。
    • 原子时 + 自转因素(闰秒) + 法律因素(法律选区的特定时区、 冬令时、夏令时)+ 历史因素 = 当地标准时间

以上几个因素是时区问题复杂度的来源,为了解决这个问题,人们成立了时区信息数据库,Linux 系统也是采用了该数据库来维护系统时间。

深入理解 datetime 的坑

不同函数时区标准不同

创建 datetime 对象使用的是 LMT

import pytz
from datetime import datetime

# LMT
datetime(2021, 1, 1, tzinfo=pytz.timezone('Asia/Shanghai'))  # 2021-01-01 00:00:00+08:06
datetime(2021, 1, 1, tzinfo=pytz.timezone('Asia/Tokyo'))  # 2021-01-01 00:00:00+09:19

转换时区函数 astimezone 输出是 ST/DST,除非输入时区 == 输出时区

import pytz
from datetime import datetime

tz_sh = datetime(2021, 1, 1, tzinfo=pytz.timezone('Asia/Shanghai'))  # 2021-01-01 00:00:00+08:06
# UTC
tz_sh.astimezone(pytz.timezone('Asia/Tokyo'))  # 2021-01-01 00:54:00+09:00
# LMT
tz_sh.astimezone(pytz.timezone('Asia/Shanghai'))  # 2021-01-01 00:00:00+08:06

替换时区函数 replaceLMT

import pytz
from datetime import datetime

tz_sh = datetime(2021, 1, 1, tzinfo=pytz.timezone('Asia/Shanghai'))  # 2021-01-01 00:00:00+08:06
tz_sh.replace(tzinfo=pytz.timezone('Asia/Tokyo'))  # 2021-01-01 00:00:00+09:19

获取当前时间 datetime.nowST/DST

import pytz
from datetime import datetime

# UTC
datetime.now(tz=pytz.timezone('Asia/Shanghai'))  # 2024-07-18 14:37:33.706649+08:00

normalize: LMT 转换成 ST/DST(注意1988年中国有夏令时)

import pytz
from datetime import datetime

tz = pytz.timezone("Asia/ShangHai")
tz.normalize(datetime(2021, 1, 1, tzinfo=tz))  # 2020-12-31 23:54:00+08:00
tz.normalize(datetime(1988, 8, 1, tzinfo=tz))  # 1988-08-01 00:54:00+09:00

localize:增加 ST/DST(注意1988年中国有夏令时)

import pytz
from datetime import datetime

tz = pytz.timezone("Asia/ShangHai")
tz.localize(datetime(2021, 1, 1))  # 2021-01-01 00:00:00+08:00
tz.localize(datetime(1988, 8, 1))  # 1988-08-01 00:00:00+09:00

timezone 时区偏移以西为正,以东为负,和ISO标准相反

import pytz
from datetime import datetime

# 东八区
datetime(2021, 1, 1, tzinfo=pytz.timezone('Etc/GMT-8'))  # 2021-01-01 00:00:00+08:00
# 西八区
datetime(2021, 1, 1, tzinfo=pytz.timezone('Etc/GMT+8'))  # 2021-01-01 00:00:00-08:00

笔者建议

  • 如果不存在夏令时类似的情况,为了避免函数时区标准不同的麻烦,可以直接使用 'Etc/GMT-8'
  • 如果存在夏令时这些,那么在转换时要注意不同标准带来的误差。
  • 尽量使用无时区信息的时间戳进行计算

datetime 源码解读

或许是由于时区非常复杂,datetime 时区仅提供了 tzinfo 的抽象类。可以使用常见的时区库:如 pytz。

tzinfo 两个接口值得注意:

class tzinfo:
    @abstractmethod
    def tzname(self, __dt: datetime | None) -> str | None: ...
    @abstractmethod
    def utcoffset(self, __dt: datetime | None) -> timedelta | None: ...
    	# Return the timezone offset as timedelta positive east of UTC (negative west of UTC).
    	# 这是描述了该时区当地平均时间与 UTC 时间的偏移
    @abstractmethod
    def dst(self, __dt: datetime | None) -> timedelta | None: ...
    def fromutc(self, __dt: datetime) -> datetime: ...
	    # datetime in UTC -> datetime in local time.
    	# 这个接口做的事情是将该UTC时间更改为当地标准时间。
        # 这是一个法律概念上的时间,需要考虑到夏令时,历史等因素。

datetime 中更换时区的基本过程,通过两个时区相对于 utc 时间的偏移,计算出两个时区的时间间隔。加上该间隔,然后直接更换时区:

(dt+ timedelta).raplace(tz)

pytz 源码解读

pytz 是时区信息数据库的 python 接口,使用了 tzif 文件来存储、描述时区,与 linux 相同。

pytz.timezone("Asia/ShangHai") 读取 tzif 文件中的信息创建对象。

一般我们认为 pytz.timezone("Asia/ShangHai") 是一个时区,其实这不完全正确,准确来说,pytz.timezone("Asia/ShangHai") 应该是一个地方时区库。

它存储了三个时区:

  • LMT +8:06
  • CST +8:00
  • CDT +9:00 (已废弃的夏令时)

可以通过一下代码进行查看

import pytz

pytz.timezone("Asia/ShangHai")._tzinfos
# {(datetime.timedelta(seconds=29160), datetime.timedelta(0), 'LMT'): <DstTzInfo 'Asia/Shanghai' LMT+8:06:00 STD>, (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST'): <DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>, (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT'): <DstTzInfo 'Asia/Shanghai' CDT+9:00:00 DST>}

pytz 库之所以被诟病的原因就是这三个时区的转换,让人有些难以把握。

除了这些信息,他还记录了上海时区变迁的历史过程。 可以使用以下代码查看

import pytz

tz = pytz.timezone("Asia/ShangHai")
for a, b in zip(tz._utc_transition_times, tz._transition_info):
    print(a, b)
"""
0001-01-01 00:00:00 (datetime.timedelta(seconds=29160), datetime.timedelta(0), 'LMT')
1901-12-13 20:45:52 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1919-04-12 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1919-09-30 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1940-05-31 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1940-10-12 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1941-03-14 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1941-11-01 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1942-01-30 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1945-09-01 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1946-05-14 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1946-09-30 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1947-04-14 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1947-10-31 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1948-04-30 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1948-09-30 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1949-04-30 16:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1949-05-27 15:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1986-05-03 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1986-09-13 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1987-04-11 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1987-09-12 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1988-04-16 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1988-09-10 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1989-04-15 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1989-09-16 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1990-04-14 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1990-09-15 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
1991-04-13 18:00:00 (datetime.timedelta(seconds=32400), datetime.timedelta(seconds=3600), 'CDT')
1991-09-14 17:00:00 (datetime.timedelta(seconds=28800), datetime.timedelta(0), 'CST')
"""
Logo

为开发者提供按需使用的算力基础设施。

更多推荐