跨語言時區處理與Epoch


國際化通用程序或標准協議通常都涉及到時區問題,比如最近項目用到的OIDC(OpenID Connect)。
OIDC基於OAuth2協議,其id_token中包含了exp來表達該Token的過期時間,值為Unix Epoch(Timestamp,時間戳),通常各語言的日期實現會將該時間戳轉換為本地日期,然后進行日期的比較。

0 時區與Unix Epoch

0.1 時區

為了統一地球上各地區的時間,建立了世界時,格林威治標准時間即作為第一個標准時間。地球以格林威治子午線為標准即0時區,按經度划分為24時區,東半球早於標准時間為正時區,西半球晚於標准時間為負時區。中國采用北京時間即正8區。格林威治標准時間縮寫為GMT,東8區用GMT+08:00表示,即格林威治子午線的時間加上8小時為北京當地時間。

隨着計時精度要求的提高,出現了原子計時器,形成了新的世界時,又稱世界協調時簡寫UTC,就日常生活需求GMT與UTC等同。同時時區的划分也是一致的,北京時區用UTC+08:00表示。

0.2 Unix Epoch

在Unix系統上,為了用一個整數表示具體的時間,以1970年1月1日0時0分0秒0為基准, 將經過的秒數記為一個數值,然后用該數值表示某個時間。比如時間戳3600即表示1970年1月1日1時0分0秒,也就是在基准時間上經過了3600秒。由於時區問題,對於UTC+00:00時區的1970年1月1日0時0分0秒0時間點,實際為北京當地時間的1970年1月1日8時0分0秒0,這樣對於時間戳3600即為北京當地時間的1970年1月1日9時0分0秒0 表示為 1970-01-01 09:00:00+08:00

0.3 時間表示

時間表示一般分為帶時區信息的、不帶時區的、Unix Epoch。不帶時區的日期串稱為Naive Date,如1970-01-01 09:00:00。在日常生活中特指當地時間,在程序語言中有的沒有特指,就是時區缺失,可以加上時區信息,有的語言默認為操作系統默認時區。

下面以Pythongolang為例, 進行API操作描述

1 Python 時區操作

1.0 使用模塊

import time
from datetime import datetime, timezone, timedelta
# 下面dateutil 可用,可不用
from dateutil import tz # 該package需要安裝, pip install dateutil

1.1 當前時間

# Python 的datetime模塊提供了兩個函數來返回時間
naive_now = datetime.now() # 返回一個naive,本地時區的時間
# 如在世界時2017-04-25 10:00:03+00:00在北京時區的機器上執行該代碼
# 返回 datetime(2017,4,25,18,0,3) # 還有微妙部分,假設為0,省略
naive_utcnow = datetime.utcnow() # 返回一個naive,UTC+0:00 <簡寫UTC>的時間
# 即返回 datetime(2017,4,25,10,0,3) # 微妙同上

1.2 有時區的時間

# 方法一:通過解析獲取
china_date = datetime.strptime('2017-04-25 18:00:03+0800', '%Y-%m-%d %H:%M:%S%z')
# 返回 datetime(2017, 4, 25, 18, 0, 3, tzinfo=datetime.timezone(datetime.timedelta(0, 28800)))
# 可以看到返回的日期上tzinfo不為None了
# 當解析的日期不帶時區時,返回的日期對象tzinfo為None即naive日期對象
naive_date = datetime.strptime('2017-04-25 18:00:03', '%Y-%m-%d %H:%M:%S')
naive_utcdate = datetime.strptime('2017-04-25 10:00:03', '%Y-%m-%d %H:%M:%S')
# 當naive日期與帶時區的日期對象比較時,即使年月日時分秒以及微妙一致也是不相等的(因為時區不同)
# 即naive_date != china_date
# 兩個naive的對象可以正常比較,兩個非naive的對象也可以正常比較

# 方法二:強制設置naive日期對象的時區
tz_utcdate = naive_utcdate.replace(tzinfo=timezone.utc)
# 返回 datetime(2017, 4, 25, 10, 0, 3, tzinfo=datetime.timezone.utc)
# 該方法強制替換某個時間(日期)對象的時區屬性即使是非naive的,並生成新的一個日期對象

1.3 獲取時區

#通過安裝dateutil這個Package
# 獲取 當前系統時區,第一個參數為name,可以任意設置
TZCUR = tz.tzoffset('current', -time.timezone) # 注意 time.timezone 與時區正負號相反, 用秒表示
# 或者 
TZCUR = timezone(timedelta(minutes=-time.timezone/60)) # 作為timezone的參數,timedelta的參數最好用hours或minutes

# 獲取 UTC 時區
TZUTC = timezone.utc

# +8:00 時區/ China
TZCHINA = tz.tzoffset('UTC+8:00', 8*3600)
# 或
TZCHINA = timezone(timedelta(hours=8), 'UTC+08:00') # 第二個參數為name可以為空或不傳

# +9:00 時區/ Japan
TZJAPAN = tz.tzoffset('UTC+9:00', 9*3600)
# 或
TZJAPAN = timezone(timedelta(hours=9), 'UTC+09:00')

1.4 轉換時區

將某個時區的日期轉換為另一個時區的日期,如北京時間的晚18點,轉換為東京時間,即為晚19點。

# 使用astimezone函數,轉換帶時區的日期對象的時區
japan_date = china_date.astimezone(TZJAPAN)
# 返回 datetime(2017, 4, 25, 19, 0, 3, 0, tzinfo=tzoffset('UTC+9:00', 32400)
# 對於naive的日期對象,即tzinfo屬性為None時,該方法無法進行日期轉換,拋出異常
# 也就是 datetime.now()或datetime.utcnow()的結果都無法執行astimezone方法

1.5 Epoch 的 生成

# 在Python3中datetime對象可以直接執行timestamp方法,如
epoch = naive_date.timestamp()
# 返回 1493114403.0 # 假設naive_date的微妙為0
# 即該epoch表示從基准時間開始,經過1493114403秒
# 對於沒有時區信息的naive_date,默認使用系統時區
# 即datetime(2017,4,25,18,0,3)當作datetime(2017,4,25,18,0,3,tzinfo=tzoffset('UTC+8:00', 28800))處理,對應世界時為2017-04-25 10:00:03+00:00,距離基准1970-01-01 00:00:00+00:00為1493114403秒。
# 因此盡管naive_date 與 china_date對象不等,但執行timestamp方法后的結果一樣
# 而且 china_date通過astimezone方法轉換為其他時區的對象后執行timestamp方法得到的結果也一樣
# 所以 datetime.now().timestamp() 與 datetime.utcnow().timestamp() 在非+00:00時區執行時不相等,因為兩者實際是不同的時區,但由於缺失時區信息,強制按系統默認時區處理,兩者將差time.timezone

1.6 按Epoch計算日期

# 同now函數一樣,Python提供了兩個函數生成本地和世界時日期
# 同樣也是生成naive的日期,對象的時區屬性為None
naive_date_from_epoch = datetime.fromtimestamp(1493114403)
naive_utcdate_from_epoch = datetime.utcfromtimestamp(1493114403)
# 盡管兩個epoch數值一樣,得到naive日期對象將差8小時(若系統默認時區為+08:00)
# 由於返回結果為naive日期,因此兩個結果比較是不相等的,執行timestamp得到的數值也不相同
# 將naive對象分別設置正確的時區后,兩者將一直
local_date_from_epoch = naive_date_from_epoch.replace(tzinfo=TZCUR) # 系統時區
utc_date_from_epoch = naive_utcdate_from_epoch.replace(tzinfo=TZUTC) # UTC+0時區
# 此時 local_date_from_epoch == utc_date_from_epoch 
# 兩者生成的Epoch也都是1493114403

# Epoch 為1時,可以更好的看到該情況
datetime.utcfromtimestamp(1) # 返回 datetime(1970, 1, 1, 0, 0, 1)
datetime.fromtimestamp(1) # 返回 datetime(1970, 1, 1, 8, 0, 1)
由於naive日期 datetime(1970, 1, 1, 0, 0, 1) 執行timestamp方法,將系統時區處理即按1970-01-01 00:00:01+0800處理時(對應1969-12-31 16:00:01+0000)在Epoch的基准線之前,Python3.5的版本會拋出OverflowError的異常

1.7 跨編程語言

// golang 內置time模塊進行日期相關處理
// A 獲取當前時間
now = time.Now() // 與Python不同,返回的是一個帶時區的'日期'對象
// golang使用time.Date傳入年月日,時分秒,微妙去構造一個日期對象, 還必須傳入時區信息,如
time.Date(2017, time.April, 25, 18, 0, 3, 0, time.Local)

// B 時區轉換
now.UTC() // 轉換為 UTC時間,即UTC+0時區時間

// C 按日期生成Epoch
now.Unix() // 生成到秒,返回整數
// 其中 now.Unix() == now.UTC().Unix() ,與Python一致

// D 按Epoch計算日期
time.Unix(seconds, microseconds) // 返回一個帶時區的'日期'對象
// 以上兩個日期對象都帶本地時區
// 也就是 golang的 time.Unix等同於python的datetime.fromtimestamp(seconds).replace(tzinfo=TZCUR)

// 采用系統時區,基本是各語言的默認行為

2 總結

在進行跨時區處理時,只要正確區分naive日期對象和帶時區的日期對象,基本就保證了時間處理的正確性,而Epoch值表示相對於基准時間的差值,有效的回避了該問題(不同時區基准naive不一樣)。避免了傳遞不帶日期的時間字符串的時區問題。

以Token過期為例,北京時間2017-04-25 18:00:03生成一個Token,一小時后過期,即北京時間2017-04-25 19:00:03過期。該Token傳給東京的服務器后,按東京當地時間應該在東京2017-04-25 20:00:03時過期。

交互時傳遞參數為2017-04-25 19:00:03+0800時,由於帶有時區信息可以准確表達,若缺失時區如2017-04-25 19:00:03時,雙方都沒有處理時會出錯。

若用Epoch表示,Token生成時間為北京時間2017-04-25 18:00:03對應Epoch值為1493114403(即距離基准北京時間1970-01-01 08:00:00 相隔1493114403秒),Token過期時間為北京時間2017-04-25 19:00:03對應Epoch為1493118003(即生成時間之后3600秒)。 傳遞該Epoch值至東京服務器,收到后,解釋為東京基准時間起之后1493118003秒的那個時間點該Token過期,自然就對應東京時間2017-04-25 20:00:03了。此過程,無需時區處理。

也就是說Epoch 1對應

  • 1970-01-01 00:00:01+0000(UTC)
  • 1970-01-01 08:00:01+0800(北京當地時間)
  • 1970-01-01 09:00:01+0900(東京當地時間)

而Epoch 1493118003對應

  • 2017-04-25 11:00:03+0000(UTC)
  • 2017-04-25 19:00:03+0800(北京)
  • 2017-04-25 20:00:03+0900(東京)

這樣使用Epoch進行跨時區、跨語言交互時,處理與平常(無時區交互時)一致,無需任何特殊處理。若進行特殊處理,又處理不對應時,反倒會畫蛇添足。

當采用的框架提供修改時區的功能是,可能會導致與語言默認行為不一致,此時要特別注意。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM