# 关于时间
# 时间标准
1884 年,国际子午线会议决定,以经过格林尼治的经线,作为本初子午线,也就是 0 度经线。将全球 360 度经线,平均分为 24 个时区。
- 格林威治天文台,位于英国伦敦。人们将这里的时间,称为格林威治时间(Greenwich Mean Time,GMT),代表 0 时区,又被称为世界时(Universal Time,UT)。
- 格林威治天文台,会定期公布当前的 GMT 时间。而世界各地 ±N 时区的人们,会在 GMT 时间的基础上,加减 N 小时,作为当地时区的时间。
- GMT 时间,是基于天文观测进行计时。比如将太阳运行到天空最高点的时刻,记作正午。
1955 年,人们发明了铯原子钟,提供了一种比天文观测更精准的计时方法。
- 基于铯原子钟,人们发明了一种新的时间标准,称为国际原子时(TAI)。
- 它表示从
1958-01-01T00:00:00+00:00
开始,到当前为止,经过的秒数。 - 1958 年这个开始时刻,称为它的纪元(epoch)。
- 它表示从
- 基于铯原子钟,人们发明了一种新的时间标准,称为国际原子时(TAI)。
人们在世界各地存放了多个铯原子钟。经过协调同步,它们的秒数会同时递增,几乎没有误差。通过这种方式得到的时间,称为协调世界时(UTC)。
- 例如,每个 GPS 卫星,包含一个铯原子钟。
- GMT、UTC 都是指 0 时区的当前时间,可用于计算其它时区的时间。
闰秒(leap second)
- 人们将地球自转一周的时长,称为 1 天。每天分为 24 个小时,每小时分为 60 分钟,每分钟分为 60 秒,总共 86400 秒。
- 但是,地球的自转速度在逐渐变慢(由于潮汐摩擦等原因),导致一天的时长比 86400 秒多一点点。累计多日之后,这种误差会越来越大。
- 因此,当 UTC time 与 GMT time 的误差越来越大,接近 0.9 秒时,国际计量局就会进行闰秒:将 UTC time 调慢一秒,使得某天的最后一分钟包含 61 秒。
- 换句话说,原本 UTC time 与 TAI time 一致。但是人为增加闰秒之后,UTC time 偏离 TAI time ,变成与 GMT time 一致。
- 大部分软件,不会考虑到闰秒。因此闰秒的出现,可能导致某些软件发生故障。
- 截止到 2022 年,UTC time 已经增加了几十次闰秒,与 TAI time 的偏移量达到 -37 秒,或者说滞后 37 秒。
- 2022 年,国际计量大会决定,于 2035 年取消闰秒。
Unix 时间戳(Unix timestamp)
- 又称为 Unix time、POSIX time ,它是类 Unix 操作系统中常用的一种时间。
- 它表示从
1970-01-01T00:00:00+00:00
开始,到当前为止,经过的秒数。- 1970 年这个开始时刻,称为它的纪元。
- 某些软件,用 32 bits 长度的变量存储 Unix time ,导致最大时间只能记录到 2038 年,存在隐患。
- 大部分计算机都包含一个硬件时钟,即使主机关机,也会一直累计 Unix time 。
- 硬件时钟,通常是基于石英晶体的振荡进行计时,误差大于铯原子钟。换句话说,硬件时钟不能准确计量 1 秒的时长,可能偏快、偏慢。
- 除了误差之外,Unix time 是直接累计秒数,不考虑闰秒。因此计算机运行几年之后,累计的 Unix time ,必然偏离 UTC time 。
- 因此,只靠 Unix time 不能得知正确的 UTC time 。大部分计算机会通过 NTP 等网络协议,自动获取最新的 UTC time 。
- 总之,Unix time 是每个计算机独自累计的秒数,容易偏离 UTC time ,因此需要定期修改、纠正。
- 例如,2016 年底进行了一次闰秒:
- UTC time 为
2016-12-31T23:59:59+00:00
时,对应的 Unix time 为1483142399
。 - 下一秒,UTC time 为
2016-12-31T23:59:60+00:00
,属于闰秒。此时 Unix time 递增一秒,变为1483142400
。 - 下一秒,UTC time 为
2017-01-01T00:00:00+00:00
,符合常见的 24 小时制。此时 Unix time 停顿一秒,依然为1483142400
,从而与 UTC time 一致。
- UTC time 为
上面介绍的几种时间标准,是以秒数的格式查看时间。但普通人更习惯以字符串格式(包括年份、月份、日期)查看时间,此时推荐采用 ISO8601 格式。
- ISO8601 格式有多种写法,例如:
19700101 # 只记录年份、月份、日期 1970-01-01 # 加入 - 分隔符 1970-01-01T00:00:00 # 记录详细时间,用字母 T 分隔 day 与 hour 1970-01-01T00:00:00.000 # 还可记录毫秒 1970-01-01T00:00:00Z # 末尾的字母 Z 表示 0 时区 1970-01-01T00:08:00+08:00 # 末尾的 +08:00 表示东八区
- 特点:
- 采用 24 小时制。
- 各个字段从大到小排列:year、month、day、hour、minute、second
- 各个字段的长度固定,比如年份总是 4 个字符,月份总是 2 个字符。缺位则用 0 补齐。
- ISO8601 格式有多种写法,例如:
夏令时(Daylight Saving Time,DST)
- 20 世纪,一些国家开始采用夏令时的制度。
- 夏天的太阳更早升起,所以当夏天开始时,将时钟拨快一小时,从而早睡早起,提高日光的利用率,减少晚上的照明耗能。
- 当夏天结束时,将时钟调慢一小时,恢复原状。
- 20 世纪,一些国家开始采用夏令时的制度。
CST 是一种缩写,可能表示以下几个时区之一:
- 中国标准时间:China Standard Time UTC+8:00
- 美国中部时间:Central Standard Time (USA) UTC-6:00
- 澳大利亚中部时间:Central Standard Time (Australia) UTC+9:30
- 古巴标准时间:Cuba Standard Time UTC-4:00
# import time
:Python 的标准库,用于获取时间。
原理:Python 解释器本身不知道当前时间,只能调用本机操作系统的 API ,获取 Unix time ,然后转换成 UTC time 。
例:获取 UTC 时区的时间
>>> import time >>> time.time() # 返回当前的 Unix 时间戳,取值为 float 类型 1544593113.593077 >>> time.gmtime(time.time()) # 将 Unix 时间戳转换成一个 struct_time 对象,包含年、月、日等信息 time.struct_time(tm_year=2018, tm_mon=12, tm_mday=12, tm_hour=5, tm_min=38, tm_sec=33, tm_wday=2, tm_yday=346, tm_isdst=0) >>> time.gmtime() # 不输入参数时,相当于输入 time.time() time.struct_time(tm_year=2018, tm_mon=12, tm_mday=12, tm_hour=5, tm_min=38, tm_sec=33, tm_wday=2, tm_yday=346, tm_isdst=0) >>> time.gmtime().tm_year # 获取 struct_time 对象的一个字段 2018
- struct_time 对象不包含毫秒。
- tm_wday 表示当前是本周的第几天。
- tm_yday 表示当前是本年的第几天。
- tm_isdst 表示是否属于夏令时。
例:获取本地时区的时间
>>> time.localtime(time.time()) # 将 Unix 时间戳,转换成本地时区的 struct_time 对象 time.struct_time(tm_year=2018, tm_mon=12, tm_mday=12, tm_hour=13, tm_min=38, tm_sec=33, tm_wday=2, tm_yday=346, tm_isdst=0) >>> time.localtime().tm_year # 不输入参数时,相当于输入 time.time() 2018
与字符串的转换:
>>> time.strftime('%Y/%m/%d %H:%M:%S', time.localtime()) # 将 struct_time 对象,转换成指定格式的字符串 '2018/12/12 13:38:33' >>> time.strptime('2018/12/12 13:38:33', '%Y/%m/%d %H:%M:%S') # 将指定格式的字符串,转换成 struct_time 对象 time.struct_time(tm_year=2018, tm_mon=12, tm_mday=12, tm_hour=13, tm_min=38, tm_sec=33, tm_wday=2, tm_yday=346, tm_isdst=-1) >>> time.asctime() # 返回 Www Mmm dd hh:mm:ss yyyy 格式的时间字符串 'Wed Dec 12 13:38:33 2018'
关于 Python 解释器:
>>> time.sleep(1) # 让当前线程睡眠 1 秒,或者说阻塞 1 秒
>>> time.perf_counter() # 返回当前 Python 解释器启动之后,经过的时长 16.795266047 >>> time.process_time() # 返回当前进程,累计占用的 CPU 时长,不包括 sleep 的时长 0.078125 >>> time.thread_time() # 返回当前线程,累计占用的 CPU 时长,不包括 sleep 的时长 0.078125
# import datetime
- :Python 的标准库,基于 time 模块,实现一些更复杂的操作。
- 官方文档 (opens new window)
# date
date 对象,用于记录年份、月份、日期。定义如下:
class date(year, month, day)
例:
>>> from datetime import date >>> date.today() # 获取今天的日期,返回一个 date 对象,采用本地时区 datetime.date(2019, 5, 1) >>> date(2019, 5, 1) # 手动创建一个 date 对象 datetime.date(2019, 5, 1) >>> _.replace(day=2) # date 对象本身是不可变的。但可以修改某个属性,返回一个新的对象 datetime.date(2019, 5, 2)
# datetime
datetime 对象,记录的信息比 date 对象更多。定义如下:
class datetime(year, month, day, hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
- 创建 datetime 对象时,至少要传入 year、month、day 三个参数。
- 传入的所有参数必须符合以下取值范围,否则抛出 ValueError 异常。
MINYEAR <= year <= MAXYEAR 1 <= month <= 12 1 <= day <= 当前月份的总天数 0 <= hour < 24 0 <= minute < 60 0 <= second < 60 0 <= microsecond < 1000000
获取当前时间:
>>> from datetime import datetime >>> datetime.now() # 获取本地时区的时间,返回一个 datetime 对象 datetime.datetime(2019, 5, 30, 18, 0, 0, 505080)
手动创建 datetime 对象:
>>> d = datetime(2019, 5, 30, 18, 0, 0, 505080) >>> print(d.year, d.month, d.day) 2019 5 30 >>> print(d.hour, d.minute, d.second, d.microsecond) 18 0 0 505080 >>> print(d.tzinfo) # 返回时区。默认值为 None ,此时可能是指 UTC 时区,也可能是指本地时区,建议主动指定时区 None >>> d.date() # 将 datatime 对象,简化成 date 对象 datetime.date(2019, 5, 30)
与 struct_time 的转换:
>>> d.timetuple() # 直接转换成 struct_time time.struct_time(tm_year=2019, tm_mon=5, tm_mday=30, tm_hour=18, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=150, tm_isdst=-1) >>> d.utctimetuple() # 先转换成 UTC 时区,再转换成 struct_time ,不过 d.tzinfo 为 None ,不一定能正确转换时区 time.struct_time(tm_year=2019, tm_mon=5, tm_mday=30, tm_hour=18, tm_min=0, tm_sec=0, tm_wday=3, tm_yday=150, tm_isdst=0) >>> d.replace(tzinfo=pytz.timezone('Asia/Shanghai')).utctimetuple() # 建议采用这种方式,正确转换时区 time.struct_time(tm_year=2019, tm_mon=5, tm_mday=30, tm_hour=9, tm_min=54, tm_sec=0, tm_wday=3, tm_yday=150, tm_isdst=0)
与 Unix 时间戳的转换:
>>> d.timestamp() # 将 datetime 对象,转换成 Unix 时间戳(采用 d.tzinfo 时区,不会自动转换成 UTC 时区) 1559210400.50508 >>> datetime.fromtimestamp(1559210400.50508) # 将 Unix 时间戳,转换成 datetime 对象 datetime.datetime(2019, 5, 30, 18, 0, 0, 505080) >>> datetime.fromtimestamp(1559210400.50508, tz=pytz.timezone('UTC')) # 建议声明时区 datetime.datetime(2019, 5, 30, 10, 0, 0, 505080, tzinfo=<UTC>)
与字符串的转换:
>>> d.strftime('%Y/%m/%d %H:%M:%S.%f') # 将 datetime 对象,转换成指定格式的字符串 '2019/05/30 18:00:00.505080' >>> datetime.strptime('2019/05/30 18:00:00.505080', '%Y/%m/%d %H:%M:%S.%f') datetime.datetime(2019, 5, 30, 18, 0, 0, 505080) # 将指定格式的字符串,转换成 datetime 对象
- 与
time.strftime()
相比,datetime.strftime()
的优点:能用%f
输出微妙。
- 与
与 ISO8601 格式字符串的转换:
>>> d.isoformat() '2019-05-30T18:00:00.505080' >>> datetime.fromisoformat(_) datetime.datetime(2019, 5, 30, 18, 0, 0, 505080)
>>> d.replace(tzinfo=pytz.timezone('Asia/Shanghai')).isoformat() # 建议声明时区 '2019-05-30T18:00:00.505080+08:06' >>> datetime.fromisoformat(_) datetime.datetime(2019, 5, 30, 18, 0, 0, 505080, tzinfo=datetime.timezone(datetime.timedelta(seconds=29160)))
# timedelta
timedelta 对象,用于表示时间差,或者说一段时间长度。定义如下:
timedelta(days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0)
例:
>>> from datetime import timedelta >>> timedelta(milliseconds=100) datetime.timedelta(microseconds=100000) >>> delta = _ >>> delta.total_seconds() # 转换成以 seconds 为单位的一个数值 0.1 >>> str(delta) # 转换成 str 类型 '0:00:00.100000'
data、datetime、timedelta 对象,都支持算术运算,方便计算时间差:
>>> timedelta(seconds=1) + timedelta(milliseconds=100) # 加法运算 datetime.timedelta(seconds=1, microseconds=100000) >>> timedelta(milliseconds=100) * 10 # 乘法运算 datetime.timedelta(seconds=1) >>> timedelta(seconds=1) / timedelta(milliseconds=100) # 除法运算 10.0 >>> timedelta(seconds=1) > timedelta(milliseconds=100) # 比较运算 True
>>> d =datetime.now() >>> d + timedelta(hours=10) # datetime 对象与 timedelta 对象的算术运算,会返回 datetime 对象 datetime.datetime(2019, 5, 31, 4, 0, 0, 505080) >>> _ - d # 两个 datetime 对象的算术运算,会返回 timedelta 对象 datetime.timedelta(seconds=36000)
# 时区
导入 pytz 模块,用于选择时区:
>>> import pytz >>> pytz.all_timezones_set # 返回所有可用时区的名称 LazySet({'Europe/Tirane', 'Asia/Shanghai', 'America/New_York', ...}) >>> pytz.timezone('UTC') # 输入一个时区的名称,返回该时区对象 <UTC>
转换时区:
>>> datetime.now(tz=pytz.timezone('UTC')) # 获取指定时区的当前时间 datetime.datetime(2019, 5, 30, 10, 0, 0, 505080, tzinfo=<UTC>) >>> datetime.now().astimezone(pytz.timezone('Asia/Shanghai')) # 将 datetime 对象从当前时区,转换到指定时区 datetime.datetime(2019, 5, 30, 18, 0, 0, 505080, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>)
将时间字符串,从 UTC 时区,转换到指定时区:
>>> utc_time_str = '2019/05/30 10:00:00' >>> datetime.strptime(utc_time_str, '%Y/%m/%d %H:%M:%S') datetime.datetime(2019, 5, 30, 10, 0) >>> _.replace(tzinfo=pytz.timezone('UTC')) datetime.datetime(2019, 5, 30, 10, 0, tzinfo=<UTC>) >>> _.astimezone(pytz.timezone('Asia/Shanghai')) datetime.datetime(2019, 5, 30, 18, 0, tzinfo=<DstTzInfo 'Asia/Shanghai' CST+8:00:00 STD>) >>> _.strftime('%Y/%m/%d %H:%M:%S') '2019/05/30 18:00:00'