時區

總覽

當啟用時區支援時,Django 會在資料庫中以 UTC 格式儲存日期時間資訊,內部使用時區感知(time-zone-aware)的日期時間物件,並在範本和表單中將它們轉換為最終使用者的時區。

如果您的使用者位於多個時區,並且您希望根據每個使用者的時鐘時間顯示日期時間資訊,這會非常方便。

即使您的網站僅在一個時區提供,在資料庫中以 UTC 格式儲存資料仍然是一個好的做法。主要原因是日光節約時間 (DST)。許多國家都有 DST 系統,在春季將時鐘往前撥,在秋季往後撥。如果您以本地時間工作,您可能一年會遇到兩次錯誤,當轉換發生時。這可能對您的部落格來說沒什麼關係,但如果您每年兩次,每次多收或少收客戶一小時的費用,這就會是一個問題。解決這個問題的方法是在程式碼中使用 UTC,並且僅在與最終使用者互動時使用本地時間。

時區支援預設為啟用。若要停用它,請在設定檔案中設定 USE_TZ = False

在 Django 5.0 中變更

在舊版本中,時區支援預設為停用。

時區支援使用 zoneinfo,這是 Python 3.9 起 Python 標準函式庫的一部分。

如果您正在與特定問題搏鬥,請從時區常見問題解答開始。

概念

天真型和感知型日期時間物件

Python 的 datetime.datetime 物件有一個 tzinfo 屬性,可用於儲存時區資訊,表示為 datetime.tzinfo 的子類別的實例。當設定此屬性並描述偏移量時,日期時間物件是感知型的。否則,它是天真型的。

您可以使用 is_aware()is_naive() 來判斷日期時間是否為感知型或天真型。

當停用時區支援時,Django 會在本地時間中使用天真型日期時間物件。這對於許多使用案例來說已足夠。在此模式下,要取得目前時間,您會寫入

import datetime

now = datetime.datetime.now()

當啟用時區支援 (USE_TZ=True) 時,Django 會使用時區感知型日期時間物件。如果您的程式碼建立日期時間物件,它們也應該是感知型的。在此模式下,上述範例會變成

from django.utils import timezone

now = timezone.now()

警告

處理感知型日期時間物件並不總是直覺的。例如,標準日期時間建構子的 tzinfo 引數對於具有 DST 的時區無法可靠運作。使用 UTC 通常是安全的;如果您正在使用其他時區,您應該仔細查看 zoneinfo 文件。

注意

Python 的 datetime.time 物件也具有 tzinfo 屬性,而 PostgreSQL 有一個相符的 time with time zone 類型。但是,正如 PostgreSQL 的文件所說,此類型「顯示出導致其用途值得懷疑的屬性」。

Django 僅支援天真型時間物件,如果您嘗試儲存感知型時間物件,它會引發例外,因為沒有相關聯的日期的時間時區沒有意義。

天真型日期時間物件的解釋

USE_TZTrue 時,Django 仍然會接受天真型日期時間物件,以保留回溯相容性。當資料庫層接收到一個時,它會嘗試透過在預設時區中解釋它來使其成為感知型,並引發警告。

不幸的是,在 DST 轉換期間,某些日期時間不存在或含糊不清。這就是為什麼當啟用時區支援時,您應該始終建立感知型日期時間物件的原因。(請參閱 zoneinfo 文件的 使用 ZoneInfo 區段,其中包含使用 fold 屬性來指定在 DST 轉換期間應套用到日期時間的偏移量的範例。)

實際上,這很少是問題。Django 在模型和表單中提供感知型日期時間物件,而且大多數時候,新的日期時間物件是透過 timedelta 算術從現有的日期時間物件建立的。在應用程式程式碼中經常建立的唯一日期時間是目前時間,而 timezone.now() 會自動執行正確的操作。

預設時區和目前時區

預設時區是由 TIME_ZONE 設定定義的時區。

目前時區是用於呈現的時區。

您應該使用 activate() 將目前時區設定為最終使用者的實際時區。否則,會使用預設時區。

注意

TIME_ZONE 的文件中所說明,Django 會設定環境變數,使其程序在預設時區中執行。無論 USE_TZ 的值和目前時區如何,都會發生這種情況。

USE_TZTrue 時,這對於保留與仍然依賴本地時間的應用程式的回溯相容性非常有用。但是,如上所述,這並非完全可靠,您應該始終在自己的程式碼中使用 UTC 中的感知型日期時間。例如,使用 fromtimestamp() 並將 tz 參數設定為 utc

選擇目前時區

目前時區相當於翻譯的目前地區設定。但是,沒有 Django 可以用來自動判斷使用者時區的 Accept-Language HTTP 標頭的等效項。相反,Django 提供時區選擇函式。使用它們來建構對您有意義的時區選擇邏輯。

大多數關心時區的網站都會詢問使用者他們居住在哪个時區,並將此資訊儲存在使用者的設定檔中。對於匿名使用者,他們會使用主要受眾的時區或 UTC。zoneinfo.available_timezones() 提供一組可用的時區,您可以用來建構從可能的位置到時區的對應。

以下是一個將目前時區儲存在工作階段中的範例。(為了簡單起見,它完全跳過了錯誤處理。)

將以下中介軟體新增至 MIDDLEWARE

import zoneinfo

from django.utils import timezone


class TimezoneMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        tzname = request.session.get("django_timezone")
        if tzname:
            timezone.activate(zoneinfo.ZoneInfo(tzname))
        else:
            timezone.deactivate()
        return self.get_response(request)

建立一個可以設定目前時區的檢視

from django.shortcuts import redirect, render

# Prepare a map of common locations to timezone choices you wish to offer.
common_timezones = {
    "London": "Europe/London",
    "Paris": "Europe/Paris",
    "New York": "America/New_York",
}


def set_timezone(request):
    if request.method == "POST":
        request.session["django_timezone"] = request.POST["timezone"]
        return redirect("/")
    else:
        return render(request, "template.html", {"timezones": common_timezones})

template.html 中包含一個表單,該表單將 POST 到此檢視

{% load tz %}
{% get_current_timezone as TIME_ZONE %}
<form action="{% url 'set_timezone' %}" method="POST">
    {% csrf_token %}
    <label for="timezone">Time zone:</label>
    <select name="timezone">
        {% for city, tz in timezones %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected{% endif %}>{{ city }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set">
</form>

表單中時區感知型輸入

當您啟用時區支援時,Django 會將表單中輸入的日期時間,根據目前時區進行解讀,並在 cleaned_data 中返回帶有時區資訊的日期時間物件。

若轉換後的日期時間因落在日光節約時間 (DST) 的轉換期間而導致不存在或不明確,則會被回報為無效值。

範本中具時區意識的輸出

當您啟用時區支援時,Django 會在範本中渲染時,將帶有時區資訊的日期時間物件轉換為目前時區。其行為非常類似於格式本地化

警告

Django 不會轉換不帶時區資訊的日期時間物件,因為它們可能含糊不清,而且在啟用時區支援時,您的程式碼不應該產生不帶時區資訊的日期時間。不過,您可以使用下面描述的範本篩選器強制轉換。

轉換為本地時間並非總是適當的 — 您可能正在為電腦而不是人類產生輸出。以下由 tz 範本標籤庫提供的篩選器和標籤,可讓您控制時區轉換。

範本標籤

localtime

啟用或停用將帶有時區資訊的日期時間物件,轉換為包含區塊中目前時區的功能。

這個標籤的效果與 USE_TZ 設定對範本引擎的效果完全相同。它允許更細緻地控制轉換。

若要啟動或停用範本區塊的轉換,請使用

{% load tz %}

{% localtime on %}
    {{ value }}
{% endlocaltime %}

{% localtime off %}
    {{ value }}
{% endlocaltime %}

注意

{% localtime %} 區塊內,不適用 USE_TZ 的值。

timezone

設定或取消設定包含區塊中的目前時區。當目前時區未設定時,則會採用預設時區。

{% load tz %}

{% timezone "Europe/Paris" %}
    Paris time: {{ value }}
{% endtimezone %}

{% timezone None %}
    Server time: {{ value }}
{% endtimezone %}

get_current_timezone

您可以使用 get_current_timezone 標籤來取得目前時區的名稱

{% get_current_timezone as TIME_ZONE %}

或者,您可以啟動 tz() 環境處理器,並使用 TIME_ZONE 環境變數。

範本篩選器

這些篩選器接受帶有時區資訊和不帶時區資訊的日期時間。為了轉換,它們假設不帶時區資訊的日期時間位於預設時區。它們始終會返回帶有時區資訊的日期時間。

localtime

強制將單個值轉換為目前時區。

例如

{% load tz %}

{{ value|localtime }}

utc

強制將單個值轉換為 UTC。

例如

{% load tz %}

{{ value|utc }}

timezone

強制將單個值轉換為任意時區。

參數必須是 tzinfo 子類別或時區名稱的實例。

例如

{% load tz %}

{{ value|timezone:"Europe/Paris" }}

遷移指南

以下是如何遷移在 Django 支援時區之前啟動的專案。

資料庫

PostgreSQL

PostgreSQL 後端將日期時間儲存為 timestamp with time zone。實際上,這表示它會在儲存時將日期時間從連線的時區轉換為 UTC,並在檢索時從 UTC 轉換為連線的時區。

因此,如果您使用的是 PostgreSQL,您可以自由地在 USE_TZ = FalseUSE_TZ = True 之間切換。資料庫連線的時區將分別設定為 DATABASE-TIME_ZONEUTC,以便 Django 在所有情況下都能取得正確的日期時間。您不需要執行任何資料轉換。

時區設定

DATABASES 設定中為連線配置的time zone 與一般 TIME_ZONE 設定不同。

其他資料庫

其他後端會儲存不帶時區資訊的日期時間。如果您從 USE_TZ = False 切換到 USE_TZ = True,您必須將資料從本地時間轉換為 UTC — 如果您的本地時間有 DST,則這是不確定的。

程式碼

第一步是在您的設定檔中加入 USE_TZ = True。此時,大部分的事情應該都能正常運作。如果您在程式碼中建立不帶時區資訊的日期時間物件,Django 會在必要時使其帶有時區資訊。

但是,這些轉換在 DST 轉換期間可能會失敗,這表示您尚未充分利用時區支援的所有好處。此外,您可能會遇到一些問題,因為不可能比較不帶時區資訊的日期時間與帶有時區資訊的日期時間。由於 Django 現在為您提供帶有時區資訊的日期時間,因此只要您將來自模型或表單的日期時間與您在程式碼中建立的不帶時區資訊的日期時間進行比較,就會出現例外狀況。

因此,第二步是在您實例化日期時間物件的任何地方重構您的程式碼,使其具有時區資訊。這可以逐步完成。django.utils.timezone 定義了一些方便的輔助程式碼,以用於相容性程式碼:now()is_aware()is_naive()make_aware()make_naive()

最後,為了幫助您找到需要升級的程式碼,當您嘗試將不帶時區資訊的日期時間儲存到資料庫時,Django 會發出警告

RuntimeWarning: DateTimeField ModelName.field_name received a naive
datetime (2012-01-01 00:00:00) while time zone support is active.

在開發期間,您可以將這類警告轉換為例外狀況,並在設定檔中新增以下內容來取得追溯

import warnings

warnings.filterwarnings(
    "error",
    r"DateTimeField .* received a naive datetime",
    RuntimeWarning,
    r"django\.db\.models\.fields",
)

固定裝置

在序列化帶有時區資訊的日期時間時,會包含 UTC 偏移量,如下所示

"2011-09-01T13:20:30+03:00"

而不帶時區資訊的日期時間則不會

"2011-09-01T13:20:30"

對於具有 DateTimeField 的模型,此差異導致無法編寫一個適用於有和沒有時區支援的固定裝置。

使用 USE_TZ = False 或在 Django 1.4 之前產生的固定裝置,會使用「不帶時區資訊」的格式。如果您的專案包含這類固定裝置,在您啟用時區支援後,當您載入這些固定裝置時,會看到 RuntimeWarning。若要消除警告,您必須將固定裝置轉換為「帶有時區資訊」的格式。

您可以使用 loaddata 重新產生固定裝置,然後使用 dumpdata。或者,如果它們夠小,您可以編輯它們,將與您的 TIME_ZONE 相符的 UTC 偏移量,新增到每個序列化的日期時間。

常見問題

設定

  1. 我不需要多個時區。我應該啟用時區支援嗎?

    是的。啟用時區支援後,Django 會使用更精確的本地時間模型。這可讓您免於受到日光節約時間 (DST) 轉換期間,細微且無法重現的錯誤影響。

    當您啟用時區支援時,會遇到一些錯誤,因為您使用的是不帶時區資訊的日期時間,但 Django 期望帶有時區資訊的日期時間。執行測試時會顯示這類錯誤。您將快速了解如何避免無效的操作。

    另一方面,由缺乏時區支援所造成的錯誤,更難以預防、診斷和修正。任何涉及排程任務或日期時間算術的事情,都可能出現一年只會發生一兩次的細微錯誤。

    基於這些原因,在新的專案中,時區支援預設為啟用,除非您有充分的理由不啟用,否則您應該保留它。

  2. 我已啟用時區支援。我安全了嗎?

    也許是。你可能受到時區轉換(DST)相關錯誤的較好保護,但你仍然可能因為不小心將未感知時區的 datetime 物件轉換為已感知時區的 datetime 物件,反之亦然,而自找麻煩。

    如果你的應用程式連接到其他系統 – 例如,如果它查詢網路服務 – 請確保正確指定 datetime 物件。為了安全地傳輸 datetime 物件,它們的表示形式應包含 UTC 偏移量,或者它們的值應該以 UTC 為單位(或兩者兼具!)。

    最後,我們的日曆系統包含一些有趣的邊緣情況。例如,你不能總是直接從給定的日期減去一年

    >>> import datetime
    >>> def one_year_before(value):  # Wrong example.
    ...     return value.replace(year=value.year - 1)
    ...
    >>> one_year_before(datetime.datetime(2012, 3, 1, 10, 0))
    datetime.datetime(2011, 3, 1, 10, 0)
    >>> one_year_before(datetime.datetime(2012, 2, 29, 10, 0))
    Traceback (most recent call last):
    ...
    ValueError: day is out of range for month
    

    要正確實作這樣的函數,你必須決定 2012-02-29 減去一年是 2011-02-28 還是 2011-03-01,這取決於你的業務需求。

  3. 我該如何與將 datetime 物件儲存在本地時間的資料庫互動?

    DATABASES 設定中,將 TIME_ZONE 選項設定為此資料庫的適當時區。

    USE_TZTrue 時,這對於連接到不支援時區且非由 Django 管理的資料庫非常有用。

疑難排解

  1. 我的應用程式當機並出現 TypeError: can't compare offset-naive and offset-aware datetimes – 怎麼了?

    讓我們透過比較未感知時區和已感知時區的 datetime 物件來重現此錯誤

    >>> from django.utils import timezone
    >>> aware = timezone.now()
    >>> naive = timezone.make_naive(aware)
    >>> naive == aware
    Traceback (most recent call last):
    ...
    TypeError: can't compare offset-naive and offset-aware datetimes
    

    如果你遇到此錯誤,很可能是你的程式碼正在比較這兩件事

    • 由 Django 提供的 datetime 物件 – 例如,從表單或模型欄位讀取的值。由於你啟用了時區支援,它是已感知時區的。

    • 由你的程式碼產生的 datetime 物件,它是未感知時區的(否則你就不會讀到這個)。

    一般來說,正確的解決方案是變更你的程式碼以改用已感知時區的 datetime 物件。

    如果你正在編寫一個可插拔應用程式,該應用程式預計獨立於 USE_TZ 的值而運作,你可能會發現 django.utils.timezone.now() 非常有用。當 USE_TZ = False 時,此函數會以未感知時區的 datetime 物件形式傳回目前的日期和時間;當 USE_TZ = True 時,則以已感知時區的 datetime 物件形式傳回。你可以根據需要新增或減去 datetime.timedelta

  2. 我看到很多 RuntimeWarning: DateTimeField received a naive datetime (YYYY-MM-DD HH:MM:SS) while time zone support is active – 這很糟糕嗎?

    啟用時區支援時,資料庫層會期望僅從你的程式碼接收已感知時區的 datetime 物件。當它收到未感知時區的 datetime 物件時,就會發生此警告。這表示你尚未完成程式碼對時區支援的移植。請參閱移轉指南,以取得此流程的提示。

    同時,為了向後相容性,datetime 物件會被視為在預設時區中,這通常是你所期望的。

  3. now.date() 是昨天!(或明天)

    如果你一直使用未感知時區的 datetime 物件,你可能會認為你可以透過呼叫其 date() 方法將 datetime 物件轉換為日期。你也會認為 date 很像 datetime,只是它不夠精確。

    在感知時區的環境中,這些都不是真的

    >>> import datetime
    >>> import zoneinfo
    >>> paris_tz = zoneinfo.ZoneInfo("Europe/Paris")
    >>> new_york_tz = zoneinfo.ZoneInfo("America/New_York")
    >>> paris = datetime.datetime(2012, 3, 3, 1, 30, tzinfo=paris_tz)
    # This is the correct way to convert between time zones.
    >>> new_york = paris.astimezone(new_york_tz)
    >>> paris == new_york, paris.date() == new_york.date()
    (True, False)
    >>> paris - new_york, paris.date() - new_york.date()
    (datetime.timedelta(0), datetime.timedelta(1))
    >>> paris
    datetime.datetime(2012, 3, 3, 1, 30, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
    >>> new_york
    datetime.datetime(2012, 3, 2, 19, 30, tzinfo=zoneinfo.ZoneInfo(key='America/New_York'))
    

    如這個範例所示,相同的 datetime 物件在不同的時區中表示時,會有不同的日期。但真正的問題更根本。

    datetime 物件表示時間點。它是絕對的:它不依賴於任何事物。相反地,日期是日曆概念。它是時間的週期,其界限取決於考慮日期的時區。如你所見,這兩個概念根本不同,將 datetime 物件轉換為日期並非確定性的操作。

    這在實務上意味著什麼?

    一般而言,你應避免將 datetime 轉換為 date。例如,你可以使用 date 範本篩選器,只顯示 datetime 物件的日期部分。此篩選器會在格式化之前將 datetime 物件轉換為目前的時區,確保結果正確顯示。

    如果你真的需要自己進行轉換,則必須先確保將 datetime 物件轉換為適當的時區。通常,這將是目前的時區

    >>> from django.utils import timezone
    >>> timezone.activate(zoneinfo.ZoneInfo("Asia/Singapore"))
    # For this example, we set the time zone to Singapore, but here's how
    # you would obtain the current time zone in the general case.
    >>> current_tz = timezone.get_current_timezone()
    >>> local = paris.astimezone(current_tz)
    >>> local
    datetime.datetime(2012, 3, 3, 8, 30, tzinfo=zoneinfo.ZoneInfo(key='Asia/Singapore'))
    >>> local.date()
    datetime.date(2012, 3, 3)
    
  4. 我收到錯誤Are time zone definitions for your database installed?

    如果你正在使用 MySQL,請參閱 MySQL 注意事項的 時區定義 部分,以取得載入時區定義的說明。

用法

  1. 我有一個字串 "2012-02-21 10:28:45" 並且我知道它在 "Europe/Helsinki" 時區。我該如何將其轉換為已感知時區的 datetime 物件?

    在這裡,你需要建立所需的 ZoneInfo 實例,並將其附加到未感知時區的 datetime 物件

    >>> import zoneinfo
    >>> from django.utils.dateparse import parse_datetime
    >>> naive = parse_datetime("2012-02-21 10:28:45")
    >>> naive.replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki"))
    datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Europe/Helsinki'))
    
  2. 我如何取得目前時區的當地時間?

    好吧,第一個問題是,你真的需要嗎?

    你應該僅在與人類互動時使用當地時間,而範本層提供了篩選器和標籤,以將 datetime 物件轉換為你選擇的時區。

    此外,Python 知道如何比較已感知時區的 datetime 物件,並在必要時考慮 UTC 偏移量。以 UTC 撰寫所有模型和檢視程式碼更容易(且可能更快)。因此,在大多數情況下,django.utils.timezone.now() 傳回的 UTC datetime 物件就足夠了。

    但為了完整起見,如果你真的想要目前時區的當地時間,以下是如何取得它的方法

    >>> from django.utils import timezone
    >>> timezone.localtime(timezone.now())
    datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=zoneinfo.ZoneInfo(key='Europe/Paris'))
    

    在此範例中,目前的時區是 "Europe/Paris"

  3. 我如何查看所有可用的時區?

    zoneinfo.available_timezones() 提供系統可用的所有 IANA 時區有效金鑰集合。請參閱文件以瞭解使用上的考量。

返回頂部