社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

盘一盘 Python 特别篇 18 - 时区|夏令时

王的机器 • 3 年前 • 1012 次点击  



本文含 6625 字,12 图表截屏
建议阅读 34 分钟



本文是 Python 系列的特别篇的第十八篇



1
时区


时间差

在每个地区,中午 12 点都对应着正午,但是每个地区的 12 点是统一时刻吗?显然不是,要不然也不会有时差概念了。下图最右边的图显示着火车穿过两个时区,那么记录的时间应该是处在时区的那个时间,因此区分时区很重要。



世界上不同地区显示的时间不同,北京时间就比美国东部时间快 13 个小时,看下图:



UTC

为了方便比较不同时区的时间,我们用协调世界时间当作基准。


协调世界时间 (Coordinated Universal Time, UTC) 是最主要的世界时间标准,在在时刻上尽量接近于格林威治标准时间 (Greenwich Mean Time, GMT)。UTC 可以视为一个世界统一的时间,其他时区的时间都是在这个基础上增加或减少的,比如


  • 北京和新加坡的时间比 UTC 快 8 小時,可记做 UTC + 8

  • 美国东部时区时间比 UTC 慢 5 个小时,可记做 UTC - 5


这样看北京时间比美东时间快 13 个小时,因为 UTC + 8 - (UTC - 5) = 13。有了标准 UTC,时间就可以比较了。



当用 datetime()  对象创建时间式,如果不设定时区,那么这个时间被称为不考虑时区 (UTC-naive) 的日期时间;如果设定时区,那么这个时间被称为考虑时区 (UTC-aware) 的日期时间。


from datetime import datetime, timedelta, timezone

创建一个不考虑时区的日期时间,如果你处理的问题不需要考虑多个时区,那么这个时间可看做是你处理问题所在地区的时间;如果你处理的问题需要考虑多个时区,那么这个时间可看做是 UTC。


dt = datetime(2020, 6, 27, 21, 30)print(dt)
2020-06-27 21:30:00


已知美国东部时区时间比 UTC 慢 5 个小时,因此可用 timedelta() 对象定义一个负 5 个小时的时间差,并传入 timezone() 对象中定义美东时区 ET


ET = timezone(timedelta(hours=-5))dt = datetime(2020, 6, 27, 21, 30, tzinfo=ET)print(dt)
2020-06-27 21:30:00-05:00


打印出来的日期时间都是当地时间 (此时是美东时间),而最后有 -05:00 的字样,它叫做 UTC offset,负号代表比 UTC 慢 5 个小时。

已知北京时间比 UTC 快 8 个小时,因此可用 timedelta() 对象定义一个正 8 个小时的时间差,并传入 timezone() 对象中定义北京时区 BJ

BJ = timezone(timedelta(hours=8))dt = datetime(2020, 6, 27, 21, 30, tzinfo=BJ)print(dt)
2020-06-27 21:30:00+08:00


astimezone()

打印出来的都是当地时间 (此时是北京时间),而最后有  +08:00 的字样,它叫做 UTC offset,正号代表比 UTC 快 8 个小时。

现在定义一个美东时间 dt 为 2020-06-27 早上 9 点 30 分,用 astimezone() 对象显示出对应的北京时间是多少,结果是 2020-06-27 晚上 22 点 30 分。但两者的绝对差异是零。这个现实被称作相同时刻,不同时间 (same moment, different time)

dt = datetime(2020, 6, 27, 9, 30, tzinfo=ET)print(f'美东时间:{dt}')print(f'北京时间:{dt.astimezone(BJ)}')print(dt.astimezone(BJ)-dt)
美东时间:2020-06-27 09:30:00-05:00
北京时间:2020-06-27 22:30:00+08:00
0:00:00


同理,定义一个北京时间 dt 为 2020-06-27 晚上 22 点 30 分,用 astimezone() 对象显示出对应的美东时间是多少,结果是 2020-06-27 早上 9 点 30 分。两者的绝对差异是零。相同时刻,不同时间

dt = datetime(2020, 6, 27, 22, 30, tzinfo=BJ)print(f'北京时间:{dt}')print(f'美东时间:{dt.astimezone(ET)}')print(dt.astimezone(ET)-dt)
北京时间:2020-06-27 22:30:00+08:00
美东时间:2020-06-27 09:30:00-05:00
0:00:00


replace()

dt.replace(some_tz) 函数返回一个具有同样值的日期,但是在不同时区,即 dt 的时区和 some_tz 时区不同,这个叫做相同时间,不同时刻 (same time, different moment)

dt = datetime(2020, 6, 27, 9, 30, tzinfo=ET)dt_to_utc = dt.replace(tzinfo=timezone.utc)dt_as_utc = dt.astimezone(timezone.utc)
print(dt)print(dt_to_utc)print(dt_as_utc)print(dt - dt_to_utc)print(dt - dt_as_utc)
2020-06-27 09:30:00-04:00
2020-06-27 09:30:00+00:00
2020-06-27 13:30:00+00:00
4:00:00
0:00:00


从上面结果可看出两点:


  • dt_to_utc 和 dt 是相同时间 (都是 2020-06-27 09:30:00),不同时刻 (从它俩的 UTC offset 或者它俩之差 4:00:00 看出来)

  • dt_as_utc 和 dt 是不同时间 (前者是 14:30:00 后者是 09:30:00),相同时刻 (从它俩之差是 0:00:00 看出来)


dateutil.tz

在实际操作做很难记住每个时区的时间和 UTC 差多少,幸运的是 dateutil 包里的 tz 对象可以帮我们解决这个难题。只需用 '区域/城市' 来设定时区就可以了,比如

  • 美东时间用  'America/New_York' 来设定

  • 北京时间用 'China/Bei_Jing' 来设定

tz 是 timezone 的缩写,可看成是时区的数据库,首先从 dateutil 引入它,然后用 gettz() 函数加上设定的字符串时区来获取时区对象。

from dateutil import tzET = tz.gettz('America/New_York')BJ = tz.gettz('China/Bei_Jing')


以下结果复制了上面结果,只不过现在用 tz.gettz() 加字符串来设置时区,而上面用 timezone(timedelta()) 加时间差来设置时区。对普通人来说,记住形象的字符串比记住枯燥的时差容易多了吧。

dt = datetime(2020, 6, 27, 9, 30, tzinfo=ET)dt_to_bj = dt.replace(tzinfo=BJ)dt_as_bj = dt.astimezone(BJ)
print(dt)print(dt_to_bj)print(dt_as_bj)print(dt - dt_to_bj)print(dt - dt_as_bj)
2020-06-27 09:30:00-04:00
2020-06-27 09:30:00+08:00
2020-06-27 21:30:00+08:00
12:00:00
0:00:00


提示:注意力放在 dt - dt_to_bj 的结果 12:00:00 上,美东时间比北京时间慢了 12 个小时。

现在突发奇想换个日期 2020-01-11 看看有什么变化?

dt = datetime(2020, 1, 11, 9, 30, tzinfo=ET)dt_to_bj = dt.replace(tzinfo=BJ)


    
dt_as_bj = dt.astimezone(BJ)
print(dt)print(dt_to_bj)print(dt_as_bj)print(dt - dt_to_bj)print(dt - dt_as_bj)
2020-01-11 09:30:00-05:00
2020-01-11 09:30:00+08:00
2020-01-11 22:30:00+08:00
13:00:00
0:00:00


这时 dt - dt_to_bj 的结果是 13:00:00 上,表示美东时间比北京时间慢了 13 个小时 (之前只慢了 12 个小时)。此外更明显的是,


  • 当日期为 2020-06-27,北京时间是 2020-06-27 21:30:00+08:00

  • 当日期为 2020-01-11,北京时间是 2020-01-11 22:30:00+08:00

为什么不同日期上同样的美东时间对应的北京时间会不同呢?时间差还能随日期变?你说对了,夏令时了解一下。






2
夏令时


夏令时 (daylight saving time, DST) 则是为了充分利用夏天日照长的特点,充分利用光照节约能源而人为调整时间的一种机制。通过在夏天将时间向前加一小时,使人们早睡早起节约能源。虽然很多西方国家都采用了DST,但是中国不采用 DST。

夏令时 (daylight saving time) 是由“创始人”本杰明·富兰克林提出,他在 1784 年发表《节约日光成本的经济工程》, 原因是注意到人们 10 点后才起床,夜生活过到深夜。所以建议大家早睡早起,每年可节省不少蜡烛。但他没能目睹这天到来。在第一次世界大战期间,美国首次实施日光节约,以此来节省燃料。1966年,约翰逊总统签署了统一时间法案,才使其成为法律。

美股开盘时间在中国的晚上,因为美国有夏令时间 , 因此夏天的交易时间与冬天相比会提前一小时:

  • 在冬天交易时间为美国东部时间 9:30 到 16:00,对应着北京时间 22:30 到次日 5:00

  • 在夏天交易时间为美国东部时间 9:30 到 16:00,对应着北京时间 21:30 到次日 4:00



在 2020 年,夏令时是从 3 月 8 日早上 2 点开始,到 11 月 1 日早上 2 点结束。


夏令时的起点 (将表前拨)

在 3 月 8 日早上 2 点,大家把表往前调 1 个小时到早上 3 点,感觉是 2 点到 3 点这一段的时间突然没有了,如下图所示:



但是对应到 UTC 上,这段时间根本没有消失,只不过美东时间的 UTC offset 变了,由原来的 UTC-5 变成了 UTC-4,即从原来正常的慢 5 个小时到现在慢 4 个小时,如下图所示:



ET = tz.gettz('US/Eastern')spring_159am = datetime(2020, 3, 8, 1, 59, 59, tzinfo=ET)spring_3am = datetime(2020, 3, 8, 3, 0, 0, tzinfo=ET)print(spring_159am)print(spring_3am)(spring_3am - spring_159am).total_seconds()
2020-03-08 01:59:59-05:00
2020-03-08 03:00:00-04:00
3601.0


spring_159am = spring_159am.astimezone(tz.UTC)spring_3am = spring_3am.astimezone(tz.UTC)print(spring_159am)print(spring_3am)(spring_3am - spring_159am).total_seconds()
2020-03-08 06:59:59+00:00
2020-03-08 07:00:00+00:00
1.0


夏令时的终点 (将表后拨)

在 11 月 1 日早上 2 点,大家把表往后调 1 个小时到早上 1 点,把之前“丢失的那一个小时找回来了”,回归正常。注意在调时间这个动作点 (夏令时终点) 的前后从“1 点到 2 点”的时间段有歧义,它们既可以指夏令时结束之前的时间段,也可以指夏令时结束之后的时间段。为了偏于说明,用两个时间轴来区分,如下图所示:



但是对应到 UTC 上,丢失的那一个小时找回来,使得对应的美东时间的 UTC offset 变了,由原来的 UTC-4 变成了 UTC-5,即从原来慢 4 个小时又回到正常情况的慢 5 个小时,如下图所示:



ET = tz.gettz('US/Eastern')

首先用 datetime_ambiugous() 函数来验证在早上 1 点到 2 点这段时间段中的时间是否有歧义:

  • 1:00:00 有歧义

  • 1:59:59 有歧义

  • 2:00:00 无歧义

first_1am = datetime(2020, 11, 1, 1, 0, 0, tzinfo=ET)tz.datetime_ambiguous(first_1am)first_159am = datetime(2020, 11, 1, 1, 59, 59, tzinfo=ET)tz.datetime_ambiguous(first_159am)first_2am = datetime(2020, 11, 1, 2, 2, 0, tzinfo=ET)tz.datetime_ambiguous(first_2am)
True
True
False


由于 1:00:00 这个时点有歧义,我们先创建两个日期时间对象 first_1am 和 second_1am,发现两者在 ET 时区和 UTC 的时间差都为零。

first_1am = datetime(2020, 11, 1, 1, 0, 0, tzinfo=ET)second_1am = datetime(2020, 11, 1, 1, 0, 0, tzinfo=ET)(second_1am - first_1am).total_seconds()
0.0


first_1am = first_1am.astimezone(tz.UTC)second_1am = second_1am.astimezone(tz.UTC)(second_1am - first_1am).total_seconds()
0.0


那这样就无法实现夏令时结束“时间回调”这个现象了,好在我们用 enfold() 函数,它将有歧义的时间“折叠”起来,使得转换成 UTC 时能考虑到“时间回调”。从 first_1am 和 second_1am 之间的时间差为 3600 秒可以看出  enfold() 函数的作用了。

first_1am = datetime(2020, 11, 1, 1, 0, 0, tzinfo=ET)second_1am = datetime(2020, 11, 1, 1, 0, 0, tzinfo=ET)second_1am = tz.enfold(second_1am)(second_1am - first_1am).total_seconds()
0.0


first_1am = first_1am.astimezone(tz.UTC)second_1am = second_1am.astimezone(tz.UTC)(second_1am - first_1am).total_seconds()
3600.0


将两者表示成美东时间,发现 first_1am 是夏令时结束前的早上 1 点钟,比 UTC 慢 4 小时,而 second_1am 是夏令时结束后的早上 1 点钟,比 UTC 慢 5 小时。

print(first_1am.astimezone(ET))print(second_1am.astimezone(ET))
2020-11-01 01:00:00-04:00
2020-11-01 01:00:00-05:00





3
总结


这么清楚的帖子还需要总结吗?需要:


  1. astimezone() 不同时间,相同时刻。

  2. replace() 相同时间,不同时刻。

  3. dateutil.tz 可以方便设定时区

  4. 很多国家有夏令时,一年调节两次时间,先调慢再调快

  5. UTC 是标准,不管你怎么变,对应在 UTC 上的时间不会变,比较不同时区的时间最好转成 UTC 再比较


下帖来讲日期计数惯例  (daycount convention)。



Stay Tuned!


Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/70740
 
1012 次点击