概述



日期和時間處理因微小差異而容易產生錯誤。這篇文章將快速瀏覽在Django和Python中處理時間時容易混淆的 naive/aware, ISO8601, timestamp(UNIX epoch) 概念,並整理安全使用方法的範例。


datetime的兩個面孔:naive vs aware

datetime 對象依據是否包含時區資訊分為兩類。

  • naive: 沒有時區資訊的對象 (tzinfo=None)

  • aware: 包含時區資訊的對象 (tzinfo=...)

運算規則很簡單。只有相同類型之間才能進行加減運算。

  • naive - naive ✅ (可以)

  • aware - aware (同一時區) ✅ (可以)

  • naive - aware ❌ (發生TypeError)

from datetime import datetime, timezone

# naive: 沒有時區資訊
naive = datetime(2025, 11, 14, 9, 25, 1)                 # tzinfo=None

# aware: 明確指定時區資訊(此例中為UTC)
aware = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)

_ = aware - datetime.now(timezone.utc)  # OK (aware - aware)
# _ = aware - datetime.now()            # TypeError (aware - naive)

Django的timezone和USE_TZ



Django的 settings.py 中的 USE_TZ 設定非常重要。

  • USE_TZ=True (推薦): django.utils.timezone.now() 會返回 基於UTC的aware 對象。

  • USE_TZ=False:返回基於伺服器本地時間的naive 對象。

大多數網路應用程式最安全的做法是 USE_TZ=True,並統一將所有內部邏輯和資料庫存儲設置為UTC,只需在展示給用戶時進行本地時間轉換。

from django.utils import timezone

# 當USE_TZ=True,now()將始終返回aware UTC對象
now = timezone.now()

ISO8601和isoformat()

datetime.isoformat() 方法會生成ISO8601標準字符串。

  • UTC aware 對象通常會包含如 2025-11-14T09:25:01+00:00 的偏移量(+00:00)。

  • Z 是表示UTC(+00:00)的另一種標記法。(例如:...T09:25:01Z

重要的陷阱: Python標準庫中的 datetime.fromisoformat() 無法直接解析 Z 後綴。如果需要處理 Z 標記法,建議使用Django的 django.utils.dateparse.parse_datetime()

from datetime import datetime, timezone
from django.utils.dateparse import parse_datetime

dt = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
s  = dt.isoformat()                      # '2025-11-14T09:25:01+00:00'

_aware = datetime.fromisoformat(s)       # OK
_aware2 = parse_datetime("2025-11-14T09:25:01Z")  # OK (Django實用工具)

正確理解timestamp(UNIX epoch)

Timestamp(時間戳) 是自1970-01-01 00:00:00 UTC (UNIX epoch)以來至今所經過的時間,以秒(second)表示。

  • 整數部分:從基準時刻起的“秒”

  • 小數部分:小於一秒的精度(微秒)

Python的 time.time() 返回當前的UTC epoch秒。datetime 對象的 .timestamp() 方法也使用相同的UTC epoch基準。

import time
from datetime import datetime, timezone

# 當前時間的epoch(這兩個值實際上是同義的)
t1 = time.time()
t2 = datetime.now(timezone.utc).timestamp()

# 特定時間↔ epoch的相互轉換
exp_ts = 1731576301.25  # 小數部分0.25秒(250毫秒)
exp_dt = datetime.fromtimestamp(exp_ts, tz=timezone.utc)  # aware UTC
back_ts = exp_dt.timestamp()  # 1731576301.25

Timestamp在伺服器間交換數據或計算過期時間等需要數值運算的情況下非常有用,能避免時區字符串解析問題。


安全的轉換/計算模式(範例)

1. 字符串(ISO8601)→ datetime

為了安全處理 Z 後綴或各種偏移量,使用 parse_datetime

from django.utils.dateparse import parse_datetime
from django.utils import timezone

s = "2025-11-14T09:25:01Z"  # 或...+00:00
dt = parse_datetime(s)

if dt is None:
    raise ValueError("Invalid datetime string")

# 如果解析出的時間是naive(如果沒有偏移量資訊)
if timezone.is_naive(dt):
    # 根據業務規則明確指定時區(例如,可視為UTC)
    from zoneinfo import ZoneInfo
    dt = timezone.make_aware(dt, ZoneInfo("UTC"))

2. datetime(aware)↔ timestamp

始終基於 timezone.utc 進行轉換可以更清楚。

from datetime import datetime, timezone

dt = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
ts = dt.timestamp()                               # float
dt2 = datetime.fromtimestamp(ts, tz=timezone.utc) # 相互轉換 OK

3. 計算距離到期的時間(秒)

通過相減兩個 aware 對象,獲得 timedelta,然後使用 .total_seconds()

from datetime import datetime, timezone

exp = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
now = datetime.now(timezone.utc)

remaining = (exp - now).total_seconds()  # float seconds(可能為負)

核心:所有運算最好在aware對象(盡可能是UTC)之間進行。 注意事項: datetime() 的 tz|tzinfo 參數中輸入的 timezone.utc 是 datetime.timezone 而非 django.utils.timezone。


常見陷阱與快速避免

  • aware - naive 運算:會發生 TypeError

    • → 將兩者統一為 aware(盡可能是UTC)或統一為 naive,但需要清楚理解其含義與使用方法。
  • fromisoformat() 和 "Z" 後綴:

    • → 標準庫不支持 Z。需使用Django的 parse_datetime(),或需要進行 replace("Z", "+00:00") 處理。
  • 依賴本地時間 (datetime.now()):

    • → 根據伺服器部署環境(本地、開發伺服器、生產環境)的不同,時間可能不同,從而引發錯誤。內部邏輯應始終以UTC(timezone.now())為基準進行編寫。

結論

  • 時間運算的第一原則是不混淆 naiveaware。盡可能統一為aware(UTC)

  • 處理字符串時使用ISO8601格式,但解析時優先考慮可處理 Zparse_datetime()

  • 如需計算過期時間等數值運算,請利用timestamp