时区

概述

启用对时区的支持后,Django将日期时间信息以UTC格式存储在数据库中,在内部使用时区感知的日期时间对象,并将其转换为模板和表单中的最终用户时区。

如果您的用户居住在多个时区,并且您希望根据每个用户的挂钟显示日期时间信息,那么这很方便。

即使你的网站只在一个时区可用,在你的数据库中以UTC格式存储数据仍然是一个很好的做法。主要原因是夏令时(DST)。许多国家都有夏令时制度,时钟在春天向前拨,在秋天向后拨。如果您在当地时间工作,您可能一年会遇到两次错误,也就是转换发生的时候。这对你的博客可能并不重要,但如果你每年多付或少付一小时,每年两次,这对你的博客来说是个问题。此问题的解决方案是在代码中使用UTC,并仅在与最终用户交互时使用本地时间。

默认情况下启用时区支持。要禁用它,请设置 USE_TZ = False 在您的设置文件中。

Changed in Django 5.0:

在旧版本中,默认情况下禁用时区支持。

时区支持使用 zoneinfo ,它是来自Python3.9的Python标准库的一部分。

如果你要处理一个特定的问题,从 time zone FAQ .

概念

幼稚且有意识的日期时间对象

Python 的 datetime.datetime 对象有一个 tzinfo 可用于存储时区信息的属性,表示为子类的实例 datetime.tzinfo . 设置此属性并描述偏移量时,日期时间对象为 意识到的 . 否则,它的 天真的 .

你可以使用 is_aware()is_naive() 确定日期时间是知道的还是天真的。

禁用时区支持时,Django在本地时间中使用朴素的datetime对象。这对于许多用例来说已经足够了。在这种模式下,要获得当前时间,可以写下:

import datetime

now = datetime.datetime.now()

启用时区支持时 (USE_TZ=True ,django使用时区感知的日期时间对象。如果代码创建了日期时间对象,它们也应该知道。在此模式下,上面的示例变为:

from django.utils import timezone

now = timezone.now()

警告

处理可感知的DateTime对象并不总是直观的。例如, tzinfo 对于使用DST的时区,标准DateTime构造函数的参数不能可靠工作。使用UTC通常是安全的;如果您使用的是其他时区,则应查看 zoneinfo 仔细记录。

备注

Python 的 datetime.time 对象还具有 tzinfo 属性,PostgreSQL具有匹配的 time with time zone 类型。然而,正如PostgreSQL的文档所说,这种类型“展示了导致有用性有问题的属性”。

Django只支持幼稚的时间对象,如果试图保存一个感知的时间对象,则会引发异常,因为没有关联日期的时间的时区没有意义。

幼稚的日期时间对象的解释

什么时候? USE_TZTrue ,django仍然接受幼稚的datetime对象,以保持向后兼容性。当数据库层接收到一个时,它试图通过在 default time zone 并发出警告。

不幸的是,在DST转换期间,一些日期时间不存在或不明确。这就是在启用时区支持时应始终创建可识别的DateTime对象的原因。(请参阅 Using ZoneInfo section of the zoneinfo docs 有关使用 fold 属性以指定在DST转换期间应应用于日期时间的偏移量。)

实际上,这很少是一个问题。Django为您提供了模型和表单中的日期时间对象,并且通常,新的日期时间对象是从现有的对象到 timedelta 算术。通常在应用程序代码中创建的唯一日期时间是当前时间,并且 timezone.now() 自动做正确的事情。

默认时区和当前时区

这个 默认时区 时区是由 TIME_ZONE 设置。

这个 当前时区 是用于呈现的时区。

您应该将当前时区设置为最终用户的实际时区 activate() . 否则,将使用默认时区。

备注

如文件所述 TIME_ZONE ,django设置环境变量,使其进程在默认时区内运行。不管 USE_TZ 以及当前时区。

什么时候 USE_TZTrue ,这对于保持与仍然依赖本地时间的应用程序的向后兼容性很有用。然而, as explained above ,这并不完全可靠,您应该始终在自己的代码中使用UTC中的Aware DateTime。例如,使用 fromtimestamp() 并将 tz 参数设置为 utc

选择当前时区

当前时区等于当前时区 locale 用于翻译。然而,没有等价的 Accept-Language Django可以用来自动确定用户时区的HTTP头。相反,Django提供 time zone selection functions . 使用它们构建对您有意义的时区选择逻辑。

大多数关心时区的网站会询问用户他们住在哪个时区,并将这些信息存储在用户的个人资料中。对于匿名用户,他们使用主要受众或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将解释表单中输入的日期时间 current time zone 并返回中的感知日期时间对象 cleaned_data .

转换后的日期时间不存在或不明确,因为它们处于DST转换中,将被报告为无效值。

模板中的时区感知输出

启用时区支持时,Django将感知的日期时间对象转换为 current time zone 当它们在模板中呈现时。这很像 format localization .

警告

Django不转换naive datetime对象,因为它们可能不明确,而且当启用时区支持时,代码不应生成naive datetime。但是,可以使用下面描述的模板过滤器强制转换。

转换到本地时间并不总是合适的——您可能正在为计算机而不是为人类生成输出。以下过滤器和标签由 tz 模板标记库,允许您控制时区转换。

模板标签

localtime

启用或禁用将感知的日期时间对象转换为所包含块中的当前时区。

此标记的效果与 USE_TZ 就模板引擎而言进行设置。它允许对转换进行更细粒度的控制。

要激活或停用模板块的转换,请使用:

{% load tz %}

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

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

备注

价值 USE_TZ 在一个 {{% localtime %}} 块。

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 自由地数据库连接的时区将设置为 TIME_ZONEUTC 这样,Django在所有情况下都能获得正确的日期时间。您不需要执行任何数据转换。

其他数据库

其他后端存储没有时区信息的日期时间。如果你从 USE_TZ = FalseUSE_TZ = True ,您必须将您的数据从本地时间转换为UTC——如果您的本地时间具有DST,则这不是确定性的。

代码

第一步是添加 USE_TZ = True 到您的设置文件。在这一点上,事情应该基本上是可行的。如果您在代码中创建幼稚的日期时间对象,Django会在必要时让它们知道。

但是,这些转换可能会在DST转换中失败,这意味着您还没有获得时区支持的全部好处。此外,您可能会遇到一些问题,因为无法将一个天真的日期时间与一个已知的日期时间进行比较。因为django现在提供了可感知的日期时间,所以当您将来自模型或窗体的日期时间与在代码中创建的原始日期时间进行比较时,都会得到异常。

因此,第二步是在实例化datetime对象的任何位置重构代码,以使它们知道。这可以逐步完成。 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 S,这一区别使得写一个既有时区支持又没有时区支持的夹具是不可能的。

夹具生成 USE_TZ = False 或者在django 1.4之前,使用“naive”格式。如果您的项目包含这样的设备,那么在启用时区支持之后,您将看到 RuntimeWarning 当你装载它们的时候。要消除警告,必须将设备转换为“aware”格式。

你可以用 loaddata 然后 dumpdata . 或者,如果它们足够小,您可以编辑它们以添加与您的 TIME_ZONE 到每个序列化的日期时间。

FAQ

安装程序

  1. 我不需要多个时区。我应该启用时区支持吗?

    是。启用时区支持后,Django使用更精确的本地时间模型。这可以保护您免受夏令时(DST)转换过程中出现的细微且不可重现的错误的影响。

    当您启用时区支持时,您将遇到一些错误,因为您使用的是朴素的日期时间,Django希望使用有意识的日期时间。运行测试时会出现这样的错误。您将很快学会如何避免无效操作。

    另一方面,由于缺乏时区支持而导致的错误更难预防、诊断和修复。任何涉及计划任务或日期时间算法的内容都可能存在一些细微的错误,这些错误一年只会咬你一两次。

    由于这些原因,在新项目中默认启用时区支持,除非您有非常好的理由不启用,否则您应该保留时区支持。

  2. 我启用了时区支持。我安全吗?

    也许吧。你最好避免与DST相关的错误,但是你仍然可以通过不小心地将幼稚的约会时间转变成有意识的约会时间,而反之亦然。

    如果您的应用程序连接到其他系统--例如,如果它查询Web服务--请确保正确指定了日期时间。为了安全地传输日期时间,它们的表示形式应该包括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. 如何与以本地时间存储日期时间的数据库交互?

    设置 TIME_ZONE 为该数据库选择适当的时区 DATABASES 设置。

    这对于连接不支持时区且不由Django管理的数据库非常有用,当 USE_TZTrue .

故障排除

  1. 我的应用程序崩溃 TypeError: can't compare offset-naive and offset-aware datetimes -- what's wrong?

    让我们通过比较一个朴素的和一个有意识的日期时间来重现这个错误:

    >>> 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提供的日期时间——例如,从窗体或模型字段中读取的值。因为您启用了时区支持,所以它知道。

    • 由代码生成的日期时间,这是幼稚的(或者您不会读这个)。

    通常,正确的解决方案是将代码更改为使用已知的日期时间。

    如果您正在编写一个可插拔的应用程序,该应用程序可以独立于 USE_TZ 你可能会发现 django.utils.timezone.now() 有用的。此函数将当前日期和时间作为原始日期时间返回,条件是 USE_TZ = False 作为一个意识到的日期时间 USE_TZ = True . 你可以加减 datetime.timedelta 根据需要。

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

    启用时区支持时,数据库层只希望从代码中接收已知的日期时间。此警告在收到幼稚的日期时间时发生。这表示您尚未完成时区支持代码的移植。请参阅 migration guide 有关此过程的提示。

    同时,为了向后兼容,日期时间被认为是在默认时区,这通常是您所期望的。

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

    如果您一直使用幼稚的日期时间,那么您可能认为可以通过调用日期时间 date() 方法。你也认为 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'))
    

    如本例所示,同一个日期时间具有不同的日期,具体取决于表示日期的时区。但真正的问题更为根本。

    日期时间表示 时间点 . 它是绝对的:它不依赖任何东西。相反,约会是 日历概念 . 这是一段时间,其界限取决于考虑日期的时区。正如您所看到的,这两个概念本质上是不同的,将日期时间转换为日期并不是确定性操作。

    这在实践中意味着什么?

    通常,应避免转换 datetimedate . 例如,您可以使用 date 模板筛选器,仅显示日期时间的日期部分。此筛选器将在格式化日期时间之前将其转换为当前时区,以确保结果正确显示。

    如果您确实需要自己进行转换,则必须首先确保日期时间转换为适当的时区。通常,这将是当前时区:

    >>> 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. 我得到一个错误``是否安装了数据库的时区定义?''

    如果您使用的是MySQL,请参见 时区定义 有关加载时区定义的说明,请参阅MySQL注释的部分。

使用

  1. 我有一个字符串 "2012-02-21 10:28:45" 我知道它在 "Europe/Helsinki" 时区。如何将其转换为一个感知的日期时间?

    在这里,您需要创建所需的 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. 如何获取当前时区的本地时间?

    好吧,第一个问题是,你真的需要吗?

    当您与人类交互时,应该只使用本地时间,模板层提供 filters and tags 将日期时间转换为您选择的时区。

    此外,python知道如何比较已知的日期时间,必要时考虑到UTC偏移。用UTC编写所有模型和视图代码要容易得多(甚至更快)。因此,在大多数情况下,用UTC返回的日期时间 django.utils.timezone.now() 就足够了。

    不过,为了完整起见,如果您确实想要当前时区的本地时间,以下是获取该时间的方法:

    >>> 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时区的所有有效密钥集。有关使用注意事项,请参阅文档。