無論是運營全球服務,還是僅僅為用戶提供符合其訪問時間的內容,時區(Timezone)處理都是網頁開發中最棘手的問題之一。用戶可能記得自己是在“上午9點”寫的文章,但伺服器卻記錄為“凌晨0點”(UTC),而來自其他國家的用戶卻在“下午5點”(PST)查看,這樣怎麼辦呢?

Django提出了明確的哲學來解決這個困擾。

“數據庫始終以UTC(協調世界時)存儲,僅在顯示給用戶時轉換為當地時間。”

django.utils.timezone 模塊提供了實現這一哲學所需的所有工具。當 Django 的 settings.py 中設置 USE_TZ = True(默認值)時,此模塊可以完美運行。

本篇文章將詳細探討 django.utils.timezone 的關鍵功能。


1. 用 timezone.now() 取代 datetime.datetime.now()



在 Django 項目中記錄當前時間時,不應使用 Python 標準庫中的 datetime.datetime.now()

  • datetime.datetime.now(): 除非 USE_TZ=False,否則它將返回‘naive’(無時區信息)的 datetime 對象。這個時間基於伺服器運行的本地時間。如果伺服器在 KST(韓國)則是 KST 時間,若在 AWS 美國區則是(主要是 UTC)該伺服器的時間,將會導致一致性被破壞。
  • timezone.now(): 當 USE_TZ=True 時,返回 ‘aware’(有時區信息) 的 datetime 對象,並且始終基於 UTC

在數據庫中存儲時間時,必須使用 timezone.now()

示例:

from django.db import models
from django.utils import timezone
# from datetime import datetime # 不要這麼做!

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField()
    # 使用 default=timezone.now,將會以 UTC 基準存儲到 DB。
    created_at = models.DateTimeField(default=timezone.now)

# 創建新帖子時
# 此時的 UTC 時間將存儲在 created_at 中。
new_post = Post.objects.create(title="標題", content="內容")

2. ‘Naive’ 與 ‘Aware’ (基本概念)

要理解這個模塊,您需要知道兩種時間對象的類型。

  • Aware (認知 O): 包含時區信息(tzinfo)的 datetime 對象。(例如:2025-11-14 10:00:00+09:00
  • Naive (認知 X): 無時區信息的 datetime 對象。(例如:2025-11-14 10:00:00)Naive 時間僅僅是“上午10點”,卻無法得知是哪個國家的10點。

django.utils.timezone 提供了函數來檢查和轉換這兩者。

  • is_aware(value): 若為 Aware 對象則返回 True
  • is_naive(value): 若為 Naive 對象則返回 True

對於調試非常有用。


3. 轉換為當地時間: localtime()activate()



如果已將時間以 UTC 存儲到 DB,接下來就要向用戶展示了。

  • localtime(value, timezone=None): 將 Aware 的 datetime 對象(主要是 UTC)轉換為‘當前激活的時區’的時間。

那麼,‘當前激活的時區’該如何設置呢?

  • activate(timezone): 設置當前線程(請求)的默認時區。(通常使用 pytz 或 Python 3.9+ 的 zoneinfo 對象)
  • deactivate(): 將激活的時區重新設置為默認值(通常為 UTC)。
  • get_current_timezone(): 返回當前激活的時區對象。

重要:在視圖(View)中手動調用 activate() 的情況較少。若 settings.pyMIDDLEWARE 中包含 django.middleware.timezone.TimezoneMiddleware,Django 將自動根據用戶的 Cookie 或個人設定來調用 activate()

localtime() 可以在視圖中手動轉換,或在模板中用過濾器({{ post.created_at|localtime }})使用。

示例:

from django.utils import timezone
import pytz # 或者在 Python 3.9+ 中使用 from zoneinfo import ZoneInfo

# 1. 從 DB 加載帖子(created_at 為 UTC)
#    (假設:在 2025 年 11 月 14 日 01:50:00 UTC 時撰寫)
post = Post.objects.get(pk=1) 
# post.created_at -> datetime.datetime(2025, 11, 14, 1, 50, 0, tzinfo=<UTC>)

# 2. 假設用戶的時區為 'Asia/Seoul' (+09:00) 並激活該時區
seoul_tz = pytz.timezone('Asia/Seoul')
timezone.activate(seoul_tz)

# 3. 轉換為當地時間
local_created_at = timezone.localtime(post.created_at)

print(f"UTC 時間: {post.created_at}")
print(f"首爾時間: {local_created_at}")

# 4. 使用後禁用(通常由中間件自動處理)
timezone.deactivate()

輸出結果:

UTC 時間: 2025-11-14 01:50:00+00:00
首爾時間: 2025-11-14 10:50:00+09:00

4. 將 Naive 時間轉換為 Aware 時間: make_aware()

當我們與外部 API 聯動、進行爬蟲,或是用戶僅用 YYYY-MM-DD HH:MM 格式輸入數據時,會遇到沒有時區信息的 ‘Naive’ datetime 對象。

如果我們嘗試將這個 Naive 時間直接存儲到 DB,可能會發生警告(Django 4.0+),或存儲為不正確的時間。

make_aware(value, timezone=None) 會將 Naive datetime 對象明確賦予時區信息,使其變為 Aware 對象。

注意:make_aware 並不是轉換時間,而是宣告“這個 Naive 時間是基於此時區的時間”。

示例:

from datetime import datetime
from django.utils import timezone
import pytz

# 從外部 API 獲得的時間(Naive)
# "2025年11月14日上午10點"
naive_dt = datetime(2025, 11, 14, 10, 0, 0)

# 假設我們知道這個時間是以'首爾'為基準時間
seoul_tz = pytz.timezone('Asia/Seoul')

# 將 Naive 時間轉為 'Asia/Seoul' Aware 時間
aware_dt = timezone.make_aware(naive_dt, seoul_tz)

print(f"Naive: {naive_dt}")
print(f"Aware (首爾): {aware_dt}")

# 現在如果將這個 aware_dt 對象存儲到 DB,
# Django 會自動將其轉換為 UTC (2025-11-14 01:00:00+00:00)並進行存儲。
# Post.objects.create(title="API 聯動", event_time=aware_dt)

輸出結果:

Naive: 2025-11-14 10:00:00
Aware (首爾): 2025-11-14 10:00:00+09:00

總結:Django 時區管理原則

正確使用 django.utils.timezone 的核心原則很簡單。

  1. 保持 USE_TZ = True 設定。
  2. 存儲到 DB 的當前時間始終使用 timezone.now()
  3. 使用 TimezoneMiddleware 使時區自動根據用戶請求激活。
  4. 當向用戶顯示 DB 的 UTC 時間時,使用模板中的{{ value|localtime }} 過濾器或在視圖中使用 localtime() 函數。
  5. 對於收到的 Naive 時間,必須使用make_aware()將其轉換為 Aware 時間後再進行處理(存儲)。