此下篇主要讲解跟时区相关的概念和程序中经常使用的场景,希望通过此文大家可以搞定所有时区相关的编程问题(如果还有不明白的地方,请联系我,我将进一步补充)。
时区基本概念
时区
由于地球自转导致不同地区的人看到太阳升起和落下的时间不同,于是人们就定义了时区的概念,将全球分为24个时区,其中位于英国的本初子午线作为零时区中线,然后向东划分出十二个时区(分别为+1, +2....+12),向西也划分成十二个时区(分别为-1, -2 .....-12)。其中最早进入新的一天的是+12时区,当+12时区为中午12点时,正好零时区进入第二天(它们相差12小时,所以+12)
GMT和UTC
GMT(Greenwich Mean Time),即格林尼治标准时间,也就是本初子午线所在的时区。UTC(Universal Time Coordinated),即标准世界时间。GMT和UTC虽然表示的时间相同,但是两个是不同的概念,大家注意区分,实践过程中,我们通常使用UTC时间作为标准时间。
时区偏移Offset
时区偏移(Offset)是指所处时区时间相对于UTC时间的偏移量,比如中国的CST时间其偏移量就是+8,即相对于UTC时间需要+8小时。有些程序会使用秒或者分钟来替代小时,所以使用的偏移量计算时间的时候需要注意具体使用的时间单位。具体可以参考wiki时区偏移
夏令时(DST)
关于夏令时我觉得这篇文章已经讲解比较详细了, 大家可以直接参考,在此不再赘述。但是夏令时进一步增加了复杂度,这意味着即使同一个时区,一年中也会随着夏令时和非夏令时而导致offset的变化。
模糊时间(Ambiguous Time)
指的是在夏令时转换过程中的一段时间,在夏令时转换时,会有两个正确的时间,那么到底应该如何显示呢,所以要让程序知道到底选择哪个时间,就必须要有一个参数来确定这件事情。关于模糊时间的操作,Python2和Python3是不同的,具体可以参考Paul Ganssle的这篇文章pytz: The Fastest Footgun in the West
设置时区
tzinfo
在《上篇》中我们已经说过Python用于表示时间的对象会分为原始的(naive)和有知的(aware)两种,而要表示有知的时间,就必须给相应的对象传递tzinfo参数。tzinfo参数主要用在datetime.datetime对象和datetime.time对象,其类初始化函数定义如下:
1
2
3
4
5# datetime.datetime
class datetime.datetime(year, month, day[, hour[, minute[, second[, microsecond[, tzinfo]]]]])
# datetime.time
class datetime.time([hour[, minute[, second[, microsecond[, tzinfo]]]]])
可以看到初始化datetime和time对象时,都有一个tzinfo参数,当我们传递一个tzinfo对象给这个参数的时候我们就可以初始化一个有知的时间对象。
那么这个tzinfo对象到底是怎么来的呢? 先来看下Python官网的定义:
This is an abstract base class, meaning that this class should not be instantiated directly. You need to derive a concrete subclass, and (at least) supply implementations of the standard
tzinfo
methods needed by thedatetime
methods you use. Thedatetime
module does not supply any concrete subclasses oftzinfo
.
从这段定义我们可以看出,tzinfo只是一个抽象类,而且官网已经明确说了不提供相应的实现,那我们怎么做呢?有两种做法:一是自己实现,Python官网还给出了示例代码,参考这里;另一种就是使用我们下边要讲到的dateutil模块。
dateutil
-
安装dateutil
1
pip install python-dateutil
-
简介 官网对dateutil的介绍就一句话
The dateutil module provides powerful extensions to the standard datetime module, available in Python.
看到了吧,专门为拓展datetime而开发的,其中我们感兴趣的主要是如何构造时区tzinfo。
- 常见的使用场景:
1.转换为相应时区的时间
1
2
3
4
5
6import dateutil.tz as tz
from datetime import datetime
my_tz = tz.gettz('Asia/Shanghai')
d = datetime(2018, 8, 20, tzinfo=my_tz)
# datetime.datetime(2018, 8, 20, 0, 0, tzinfo=tzfile('/usr/share/zoneinfo/Asia/Shanghai'))
也就是说通过tz.gettz()
我们可以得到一个tzinfo对象从而可以将其作为参数传递给datetime初始化函数。
通过这种方式,我们就可以表示一个本地时间了,比如我们获取了当前时间后想要表示成NewYork时间(换句话说就是要表示不同地方的当前时间),该怎么处理呢?
先看正确的做法:
1
2
3
4
5
6
7NYC = tz.gettz('America/New_York')
now_utc = datetime.now(tz.tzutc())
# datetime.datetime(2018, 8, 25, 8, 15, 53, 143709, tzinfo=tzutc())
now_utc.astimezone(NYC)
# datetime.datetime(2018, 8, 25, 4, 15, 53, 143709, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
错误的做法:
1
2
3
4now = datetime.utcnow()
# datetime.datetime(2018, 8, 25, 8, 17, 21, 161843)
now.astimezone(NYC)
# datetime.datetime(2018, 8, 24, 20, 17, 21, 161843, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York'))
为什么会出现这种情况呢,因为单纯的使用的utcnow()得到是一个原始naive time对象,而根据时区转换时,该时间会先从本地时区(+8 Shanghai)转换成UTC时区时间,然后再转化为NewYork时间,因此导致最终多减了8小时。
另外,有的时候我们获取到的是offset信息而不是时区信息,那么我们也可以将UTC时间转换成对应的当地时间,如下:
1
2
3# 通过tzoffset也可以构筑tzinfo对象
now_utc.astimezone(tz.tzoffset('NewYork', -14400))
# datetime.datetime(2018, 8, 25, 4, 15, 53, 143709, tzinfo=tzoffset('NewYork', -14400))
2.获取当前的时区信息,并可以做相应转换
1
2
3
4
5from dateutil.tz import tzlocal
tz_local = tzlocal()
type(tz_local)
# dateutil.tz.tz.tzlocal
1
2d = datetime(2018, 8, 20, 9, 10, 5, tzinfo=tz_local)
# datetime.datetime(2018, 8, 20, 9, 10, 5, tzinfo=tzlocal())
1
2d.astimezone(tz.UTC)
# datetime.datetime(2018, 8, 20, 1, 10, 5, tzinfo=tzutc())
pytz
在时间处理的时候,我们还经常能看到pytz这个库,这个库比较有意思的是,它与datetime的tzinfo并不完全兼容,很多时候它是独立的一套处理时间的库。
我们来看下如下代码(代码来自Paul Ganssle的文章,Paul是dateutil的核心开发者):
1
2
3
4
5
6
7
8import pytz
from datetime import datetime, timedelta
NYC = pytz.timezone('America/New_York')
# 将timezone直接传入datetime初始化函数
dt = datetime(2018, 2, 14, 12, tzinfo=NYC)
print(dt)
# 2018-02-14 12:00:00-04:56
可以看到实际的offset成了-04:56,这就不对了,正确使用pytz的姿势如下:
1
2
3
4要使用localize方法来转化datetime对象
dt = NYC.localize(datetime(2018, 2, 14, 12))
print(dt)
# 2018-02-14 12:00:00-05:00
再进行一些操作,如下
1
2
3
4
5from datetime import timedelta
dt_spring = dt + timedelta(days=60)
print(dt_spring)
# 2018-04-15 12:00:00-05:00
注意,这里的offset是-5:00,而考虑到夏令时,应当是-4:00,这是由于pytz在之前localize的时候就已经将offset设定好了,其在做其它运算之后也无法改变其offset,所以导致无法针对夏令时调整offset,所以针对pytz,每一次做类似timedelta的运算之后,都需要使用normalize函数进行调整,如下:
1
2print(NYC.normalize(dt_spring))
# 2018-04-15 13:00:00-04:00
在项目实践除非对性能有极端要求,并不推荐使用pytz,毕竟不是每个人都熟悉这个库,项目协作过程中很难避免误用。关于pytz和dateutil的性能比较,可以参考Paul Ganssle的这篇文章pytz: The Fastest Footgun in the West
时区处理的最佳实践
所有中间步骤均使用UTC时间或者时间戳
所有中间的存储或者计算均应当使用UTC时间或者Timestamp,只有在最终显示的时候如果需要转换成本地时间,那么再将时间转换为特定时区的时间进行显示。
存储timezone信息,而不是offset
如果需要针对用户本地时区做时间转换,需要存储timezone的信息,如timezone名称,而不是offset。这是由于有些地区可能有夏令时,offset会改变,所以最好是存时区名称之类的信息,这样通过tzinfo会自动进行调整。