Python标准库里提供了time、datetime和calendar这3个模块来进行时间和日期的处理,其中应用最广的是datetime,而转换时区也是靠它来做的。
Python的datetime可以处理2种类型的时间,分别为offset-naive和offset-aware。
datetime类型有一个时区属性tzinfo,但是默认为None,所以无法区分这个datetime到底是哪个时区
native是指没有包含时区信息的时间,aware是指包含时区信息的时间,
只有同类型的时间才能进行减法运算和比较。
datetime模块的函数在默认情况下都只生成offset-naive类型的datetime对象,例如now()、utcnow()、fromtimestamp()、utcfromtimestamp()和strftime()。
但是now()和fromtimestamp()可以接受一个tzinfo对象来生成offset-aware类型的datetime对象
例如:
在两台设备上同时执行datetime.now()
# 在本地时间为utc时间的电脑上
In [8]: datetime.now()
Out[8]: datetime.datetime(2018, 11, 2, 12, 6, 13, 753943)
# 在本地时间为北京时间的电脑上
In [9]: datetime.now()
Out[9]: datetime.datetime(2018, 11, 2, 20, 4, 53, 604180)
In [11]: import pytz
In [12]: datetime.now(pytz.utc)
Out[12]: datetime.datetime(2018, 11, 2, 12, 6, 13, 498316, tzinfo=)
前两个两个时间都没有tzinfo属性
如何将一个特定时区的时间转化成时间戳
正常的理解自然是将特定时区的时间转化成utc时间,然后在计算时间戳了,
下面是一个含有问题的小demo
# 获取一个含有时区的datetime对象
import pytz
sh = pytz.timezone('Asia/Shanghai')
In [12]: datetime(2018, 11, 1, hour=8, tzinfo=sh) # 获取一个上海时区的datetime对象
Out[12]: datetime.datetime(2018, 11, 1, 8, 0, tzinfo=)
In [14]: dt = datetime(2018, 11, 1, hour=8, tzinfo=sh)
In [16]: utc_naive = dt.replace(tzinfo=None) - dt.utcoffset() # 将上海时区的datetime对象,转化成utc 时间(tzinfo=None)
In [17]: timestamp = (utc_naive - datetime(1970, 1, 1)).total_seconds() # 获取到时间戳
In [18]: utc_naive
Out[18]: datetime.datetime(2018, 10, 31, 23, 54)
In [19]: timestamp
Out[19]: 1541030040.0
我们检查一下时间戳发现差了360s
这就是说明我们根据上海时间对象,获取时间戳会有问题
眼睛好的人,估计看到了06这个数字,接下来我会一步步跳坑
当我们用datetime.now(sh)获取上海时间时
from datetime import datetime
In [21]: datetime.now(sh)
Out[21]: datetime.datetime(2018, 11, 2, 20, 24, 2, 539329, tzinfo=)
自己对照打印出来的tzinfo就发现两个居然不一样,一个是8:06,一个是8:00
平时使用时可能没什么问题,但是构造datetime对象,或调用replace方法时就会莫名其妙地差6分钟了
>>> from datetime import datetime
>>> import pytz
>>> tz = pytz.timezone('Asia/Shanghai')
>>> tz
>>> dt = datetime.now(tz)
>>> dt
datetime.datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=)
>>> dt2 = datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=tz)
>>> dt2
datetime.datetime(2010, 12, 14, 19, 32, 23, 281000, tzinfo=)
>>> dt == dt2
False
>>> dt - dt2
datetime.timedelta(0, 360)
>>> dt.tzinfo
>>> dt2.tzinfo
这些坑就在那里,我们碰见就暂时躲一下吧
避坑
上面例子中上海时间和北京时间有6min的时间差,我们平时一般都会用北京时间,所以我们需要获取一个北京时区
我们可以自定义一个北京时区
In [28]: from datetime import tzinfo
class BeiJingTimezone(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=8)
def dst(self,dt):
return timedelta(0)
def tzname(self,dt):
return '+08:00'
bj = BeiJingTimezone()
dt = datetime(2018, 11, 1, hour=8, tzinfo=bj)
utc_naive = dt.replace(tzinfo=None) - dt.utcoffset()
# utc_naive datetime.datetime(2018, 11, 1, 0, 0)
timestamp = (utc_naive - datetime(1970, 1, 1)).total_seconds()
In [35]: timestamp
Out[35]: 1541030400.0
明显可以看到没有了360s的差错
datetime对象astimezone 方法的坑
In [37]: dt = datetime(2018, 11, 1, hour=8, tzinfo=sh)
In [38]: dt
Out[38]: datetime.datetime(2018, 11, 1, 8, 0, tzinfo=)
In [39]: dt.astimezone(pytz.utc)
Out[39]: datetime.datetime(2018, 10, 31, 23, 54, tzinfo=)
明显可以看到 tzinfo 是上海时区的情况下astimezone 为utc时间会有6min的误差
In [40]: utc_dt = datetime(2018, 11, 1, hour=0, tzinfo=pytz.utc)
In [41]: utc_dt.astimezone(sh)
Out[41]: datetime.datetime(2018, 11, 1, 8, 0, tzinfo=)
而utc时间astimezone 为上海时区就没有问题
这是一个坑请注意
还有一个aware datetime对象转化成时间戳的方法,就是利用time.mktime,但是mktime不会根据你传入的时间对象时区不同,而进行调整的
import time
from datetime import datetime
class BeiJingTimezone(tzinfo):
def utcoffset(self, dt):
return timedelta(hours=8)
def dst(self,dt):
return timedelta(0)
def tzname(self,dt):
return '+08:00'
bj = BeiJingTimezone()
In [42]: dt = datetime(2018, 11, 1, hour=8, tzinfo=bj) # 因为pytz 没法获取到北京时区,所有用自己定义的吧
In [43]: dt
Out[43]: datetime.datetime(2018, 11, 1, 8, 0, tzinfo=<__main__.BeiJingTimezone object at 0x7f06be8ef490>)
In [44]: dt.astimezone(pytz.utc)
Out[44]: datetime.datetime(2018, 11, 1, 0, 0, tzinfo=)
# 在本地时间为utc时间的设备上运行
In [46]: time.mktime(dt.astimezone(pytz.utc).timetuple())
Out[46]: 1541030400.0 # 北京时间 2018/11/1 8:0:0
# 在本地时间是北京时间的设备上运行
In [29]: time.mktime(dt.astimezone(pytz.utc).timetuple())
Out[29]: 1541001600.0 # 北京时间2018/11/1 0:0:0
关于mktime 再举一个例子
import datetime
In [47]: dt
Out[47]: datetime.datetime(2018, 11, 1, 8, 0, tzinfo=<__main__.BeiJingTimezone object at 0x7f06be8ef490>)
In [48]: time.mktime(dt.timetuple())
Out[48]: 1541059200.0
In [49]: time.mktime(dt.astimezone(pytz.utc).timetuple())
Out[49]: 1541030400.0
In [51]: dt.astimezone(pytz.utc)
Out[51]: datetime.datetime(2018, 11, 1, 0, 0, tzinfo=)
In [52]: dt.utctimetuple()
Out[52]: time.struct_time(tm_year=2018, tm_mon=11, tm_mday=1, tm_hour=0, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=305, tm_isdst=0)
In [53]: dt.timetuple()
Out[53]: time.struct_time(tm_year=2018, tm_mon=11, tm_mday=1, tm_hour=8, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=305, tm_isdst=0)
# 正常来说北京时间11月1号8点,和utc时间凌晨是同一个时间戳,但是转化的时间戳是不一样的
利用第一种方法.无论本地时间是多少,执行结果都一样
举例
# 本地时间为北京时间
In [31]: bj
Out[31]: <__main__.BeiJingTimezone at 0x1106eda50>
In [32]: dt = datetime(2018, 11, 1, hour=8, tzinfo=bj) # 获取北京时间为8点的时间对象
In [33]: utc_naive = dt.replace(tzinfo=None) - dt.utcoffset()
In [34]: utc_naive
Out[34]: datetime.datetime(2018, 11, 1, 0, 0)
In [35]: timestamp = (utc_naive - datetime(1970, 1, 1)).total_seconds()
# 本地时间为utc时间
In [61]: bj
Out[61]: <__main__.BeiJingTimezone at 0x7f06be8ef490>
In [62]: dt = datetime(2018, 11, 1, hour=8, tzinfo=bj)
In [63]: utc_naive = dt.replace(tzinfo=None) - dt.utcoffset()
In [64]: utc_naive
Out[64]: datetime.datetime(2018, 11, 1, 0, 0)
In [65]: timestamp = (utc_naive - datetime(1970, 1, 1)).total_seconds()
In [66]: timestamp
Out[66]: 1541030400.0
明显就可以看到时间是一样的,想一想为什么下面这段代码无论在什么地方执行获取的时间戳是一样的呢?
In [69]: (datetime(2018, 11, 1, 0, 0) - datetime(1970, 1, 1)).total_seconds()
Out[69]: 1541030400.0
在计算机中,时间实际上是用数字表示的。我们把1970年1月1日 00:00:00 UTC+00:00时区的时刻称为epoch time,记为0(1970年以前的时间timestamp为负数),当前时间就是相对于epoch time的秒数,称为timestamp。
你可以认为:
timestamp = 0 = 1970-1-1 00:00:00 UTC+0:00
对应的北京时间是
timestamp = 0 = 1970-1-1 08:00:00 UTC+8:00
总结
一旦生成了一个offset-aware类型的datetime对象,我们就能调用它的astimezone()方法,生成其他时区的时间(会根据时差来计算)。
而如果拿到的是offset-naive类型的datetime对象,也是可以调用它的replace()方法来替换tzinfo的,只不过这种替换不会根据时差来调整其他时间属性。
因此,如果拿到一个格林威治时间的offset-naive类型的datetime对象,直接调用replace(tzinfo=UTC())即可转换成offset-aware类型,然后再调用astimezone()生成其他时区的datetime对象。
而如果是+6:00时区的offset-naive类型的datetime对象,则可以创建一个+6:00时区的tzinfo类,然后用上述方式转换。
而反过来要将offset-aware类型转换成offset-naive类型时,为了不至于弄混,建议先用astimezone(UTC())生成格林威治时间,然后再replace(tzinfo=None)。
datetime.now() 获取的是本地时间的datetime native 对象,不含有任何时区信息.
再将datetime转化成时间戳时需要将datetime 转化成本地时间,然后在利用time.mktime 进行处理
或者直接将datetime 转化成utc 的native 时间,然后再减去 datetime(1970, 1, 1) 就可以获取到相应的时间戳了
参考一
参考二
参考三