無論是運營全球服務,還是僅僅為用戶提供符合其訪問時間的內容,時區(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.py 的 MIDDLEWARE 中包含 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 的核心原則很簡單。
- 保持
USE_TZ = True設定。 - 存儲到 DB 的當前時間始終使用
timezone.now()。 - 使用
TimezoneMiddleware使時區自動根據用戶請求激活。 - 當向用戶顯示 DB 的 UTC 時間時,使用模板中的
{{ value|localtime }}過濾器或在視圖中使用localtime()函數。 - 對於收到的 Naive 時間,必須使用
make_aware()將其轉換為 Aware 時間後再進行處理(存儲)。
目前沒有評論。