总是记错弄乱,自己整理一份。

基本概念

格林尼治标准时间(GMT)

英语:Greenwich Mean Time,简称 GMT

是指位于英国伦敦郊区的皇家格林尼治天文台当地的平太阳时,因为本初子午线被定义为通过那里的经线。

由于地球每天的自转是有些不规则的,而且正在缓慢减速,GMT 已经被原子钟报时的 UTC 顶替

协调世界时(UTC)

英语:Coordinated Universal Time,法语:Temps Universel Coordonné,简称 UTC

其以原子时秒长为基础,是最主要的世界时间标准。

关于缩写:

国际电信联盟希望协调世界时能够在所有语言有单一的缩写。英语和法语区的人同时希望各自的语言缩写-CUT 和 TUC 能够成为国际标准,结果最后妥协使用 UTC。

GMT 与 UTC

目前能看到 GMT 字眼的,常见于 JavaScript 的 Date() 和谷歌搜索「伦敦时间」时展示的。

我也不是很理解为什么显示 GMT,但是即便显示的是 GMT,也是经过校准的,与正确时间相差不到一秒。

网络时间协议(NTP)

英语:Network Time Protocol,缩写:NTP

NTP 是一个在电脑系统之间进行时钟同步的网络协议,意图将所有参与电脑的协调世界时(UTC)时间同步到几毫秒的误差内。

自1985年以来,NTP是目前仍在使用的最古老的互联网协议之一。

时区与本地时间

一般说 UTC 时间也就是指零时区 +00:00(或写成 UTC+0、UTC±00:00) 时间,比北京时间早八个小时。

2021-01-01 08:00:00 +08:00 也就是北京时间的上午八点钟,而这时 UTC 时间为 2021-01-01 00:00:00 +00:00,英国伦敦的午夜,这是同一个时刻。

时间戳

这里指的时间戳是指代码中的 timestamp 那一串长数字。

编程中大家讨论和使用的皆是 Unix 时间戳(Unix timestamp 或 POSIX timestamp),是从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在的秒数,有的语言也支持了毫秒、微秒等等。

  • 1 秒 = 1000 毫秒
  • 1 毫秒 = 1000 微秒

很多编程语言都有生成当前时间戳的方法,目前的时间戳长度:

  • 10 位长度,精确到秒
  • 13 位长度,精确到毫秒
  • 16 位长度,精确到微秒

在 2286 年 11 月 21 日 1 时 46 分 39 秒之后,时间戳会加长一位数。

时间戳有时区吗?

当我们讨论时间戳的时候,也就是前后端交换数据,数据库存储日期时间时,皆是 Unix 时间戳,时间戳是没有时区概念的。

或者可以理解为,时间戳是固定死零时区的。

2021-01-01 00:00:00+00:002021-01-01 08:00:00+08:00 的时间戳都是 1609459200

所以建议在数据库或数据传输时使用 Unix 时间戳,时间戳可以被简单地解析为 UTC 时间或任何时区的时间,不会被时区概念束缚和混淆。

ISO 8601

国际标准 ISO 8601,是国际标准化组织的日期和时间的表示方法,全称为《数据存储和交换形式·信息交换·日期和时间的表示方法》。

简单来说,日期与时间的规定写法是这样的:

2000-01-23                # 年使用四位数,月、日、时、分、秒皆使用两位数,不够的前面补零。
2000-01-23T04:05:06       # 用 T 分割日期与时间。
2000-01-23T04:05:06Z      # 用 Z 代表 UTC+0(零时区)
2000-01-23T04:05:06+00:00 # 也可以使用 +00:00 表示零时区
2000-01-23T04:05:06+08:00 # 东八区
2000-01-23T04:05:06.123456  # Python 中 datetime.now().isoformat() 的默认格式

实际上 ISO 8601 也允许了一些其他的写法。

RFC 3339 与 ISO 8601

Python 用的是 ISO 8601:

datetime.now().isoformat()
datetime.utcnow().isoformat()

Golang 用的是 RFC 3339:

time.Now().Format(time.RFC3339)
time.Now().UTC().Format(time.RFC3339)

What’s the difference between ISO 8601 and RFC 3339 Date Formats?

RFC 3339 被列为 ISO 8601 的配置文件。有一些很小的差异,比如 RFC 3339 不允许两位数的年份,RFC 3339 允许用空格代替 T

就一般的使用情况来说,两者之间可视为没有差异。

获取时间戳

Python:

import time
from datetime import datetime

# 正确的
print(time.time())
print(datetime.now().timestamp())

# 错误的
print(datetime.utcnow().timestamp())

timestamp() 经过了 Python 内部处理的,将当前的系统时间(无论你在哪个时区)转换为正确的标准的 Unix 时间戳。

而最下面的那个为什么错了,因为 timestamp() 默认你给的 datetime 是当前系统的时区,如果你在零时区,那的确没有问题;如果不在零时区,timestamp() 会认为我给的是东八区,所以最后返回了错误的时间戳。

可以解决这个错误,只需要在 timestamp() 之前将 datetime 的时区确定即可。

from datetime import datetime, timedelta, timezone

d1 = datetime.now()
d1 = d1.replace(tzinfo=timezone(timedelta(hours=8))) # 绑定时区为东八区
ts1 = d1.timestamp()
print(ts1)

d2 = datetime.utcnow()
d2 = d2.replace(tzinfo=timezone.utc) # 绑定时区为零时区
ts2 = d2.timestamp()
print(ts2)

# ts1 和 ts2 基本相等,只有程序运行顺序的微秒级差距。

Golang:

Golang 中是一样的:

time.Now().Unix()
time.Now().UTC().Unix()

因为 Golang 的 time.Now() 中默认就带有时区信息。

Python 常用的日期与时间方法

datetime 转 str

import datetime

d = datetime.datetime.now()
s = d.strftime('%Y-%m-%d %H:%M:%S')

str 转 datetime

from datetime import datetime, timedelta, timezone, time

s = "2001-02-03 04:05:06"
d = datetime.strptime(s, '%Y-%m-%d %H:%M:%S')
# 为得到的 datetime 增加时区
d_with_timezone = d.replace(tzinfo=timezone(timedelta(hours=8)))

获取当前 UTC 时间(带时区)

不加后面的 replace 就不带时区信息了,下同。

from datetime import datetime, timezone

datetime.utcnow().replace(tzinfo=timezone.utc) 
# 2021-05-29 15:28:10.913530+00:00

获取当前系统时间(带时区)

from datetime import datetime, timezone

# 在中国时区电脑上运行:
datetime.now().replace(tzinfo=timezone(timedelta(hours=8)))
# 2021-05-29 23:38:21.006309+08:00

datetime 转 时间戳

from datetime import datetime, timezone, timedelta

dt = datetime.now()
print(dt, 'datetime.now() 当前系统时间(不含时区)')
print(dt.timestamp(), '不含时区的时间,会认为是系统时间,会自动转换为正确的 UTC 时间戳')
print(dt.replace(tzinfo=timezone(timedelta(hours=8))).timestamp(), '包含东八区时区信息的,同上')
# 2021-07-16 00:15:02.730997 datetime.now() 当前系统时间(不含时区)
# 1626365702.730997 不含时区的时间,会认为是系统时间,会自动转换为正确的 Unix 时间戳
# 1626365702.730997 包含东八区时区信息的,同上

时间戳 转 datetime

from datetime import datetime

datetime.utcfromtimestamp(1609459200)
# 2021-01-01 00:00:00

datetime.fromtimestamp(1609459200, timezone.utc)
# 2021-01-01 00:00:00+00:00

datetime.fromtimestamp(1609459200)
# 2021-01-01 08:00:00

datetime.fromtimestamp(1609459200, timezone(timedelta(hours=8)))
# 2021-01-01 08:00:00+08:00

datetime 转换时区

转换前需要先确定被转换的 datetime 对象的时区,拥有时区属性的 datetime 对象可以转换为任意一个时区的日期时间。

from datetime import datetime, timedelta, timezone

utc_dt = datetime.utcnow().replace(tzinfo=timezone.utc)
print(utc_dt)   # 2021-01-01 00:00:00+00:00
bj_dt = utc_dt.astimezone(timezone(timedelta(hours=8)))
print(bj_dt)    # 2021-01-01 08:00:00+08:00
tokyo_dt = bj_dt.astimezone(timezone(timedelta(hours=9)))
print(tokyo_dt) # 2021-01-01 09:00:00+09:00