# 关于时间

# 时间标准

  • 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)。
  • 人们在世界各地存放了多个铯原子钟。经过协调同步,它们的秒数会同时递增,几乎没有误差。通过这种方式得到的时间,称为协调世界时(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 一致。
  • 上面介绍的几种时间标准,是以秒数的格式查看时间。但普通人更习惯以字符串格式(包括年份、月份、日期)查看时间,此时推荐采用 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 补齐。
  • 夏令时(Daylight Saving Time,DST)

    • 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 的标准库,用于获取时间。

  • 官方文档 (opens new window)

  • 原理: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

# 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'