Skip to main content

时区

概述

当启用时区支持时,Django在数据库中以UTC为单位存储datetime信息,在内部使用时区感知datetime对象,并将它们转换为模板和表单中的最终用户的时区。

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

即使您的网站只有一个时区,仍然是一个好的做法,以数据库中的UTC存储数据。主要原因是夏令时(DST)。许多国家都有DST系统,其中钟在春季向前移动,在秋季向后移动。如果您在当地时间工作,每当转换发生时,您可能会遇到两次错误。 (pytz 文档更详细地讨论了 these issues)。这可能对你的博客没有影响,但如果你超过一个小时,每年两次,每年超过你的客户,这是一个问题。这个问题的解决方案是在代码中使用UTC,并且仅在与最终用户交互时使用本地时间。

默认情况下禁用时区支持。要启用它,请在设置文件中设置 USE_TZ = True。强烈建议安装 pytz,但可能不是强制的,具体取决于您的特定数据库后端,操作系统和时区。如果您遇到查询日期或时间的例外,请在提交错误之前尝试安装它。它很简单:

$ pip install pytz

注解

为方便起见,django-admin startproject 创建的默认 settings.py 文件包括 USE_TZ = True

注解

还有一个独立但相关的 USE_L10N 设置,控制Django是否应激活格式本地化。有关详细信息,请参阅 格式本地化

如果你正在处理一个特定的问题,从 时区常见问题 开始。

概念

初始和感知datetime对象

Python的 datetime.datetime 对象有一个 tzinfo 属性,可用于存储时区信息,表示为 datetime.tzinfo 子类的实例。当设置此属性并描述偏移量时,datetime对象为 知道的。否则,它是 幼稚

您可以使用 is_aware()is_naive() 来确定数据时间是意识还是初始。

当禁用时区支持时,Django在本地时间使用天真的datetime对象。这对于许多使用情况是简单和充分的。在这种模式下,要获取当前时间,你会写:

import datetime

now = datetime.datetime.now()

当启用时区支持(USE_TZ=True)时,Django使用时区感知datetime对象。如果你的代码创建datetime对象,他们也应该知道。在此模式下,上面的示例变为:

from django.utils import timezone

now = timezone.now()

警告

处理感知的datetime对象并不总是直观的。例如,标准datetime构造函数的 tzinfo 参数对于具有DST的时区不可靠地工作。使用UTC通常是安全的;如果您使用其他时区,应仔细查看 pytz 文档。

注解

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

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

天真的datetime对象的解释

USE_TZTrue 时,Django仍然接受朴素的datetime对象,以保持向后兼容性。当数据库层接收到它时,它尝试通过在 默认时区 中解释它来提醒它,并发出警告。

不幸的是,在DST转换期间,一些数据时间不存在或不明确。在这种情况下,pytz 引发异常。其他 tzinfo 实现,例如在未安装 pytz 时用作回退的本地时区,可能会引发异常或返回不准确的结果。这就是为什么你应该总是创建意识datetime对象时区支持启用时。

在实践中,这很少是一个问题。 Django给你在模型和表单中的datetime对象,通常,新的datetime对象是通过 timedelta 算法从现有的对象创建的。在应用程序代码中经常创建的唯一日期时间是当前时间,timezone.now() 自动执行正确的操作。

默认时区和当前时区

默认时区 是由 TIME_ZONE 设置定义的时区。

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

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

注解

正如在 TIME_ZONE 的文档中所解释的,Django设置环境变量,以使其进程在默认时区运行。无论 USE_TZ 的值和当前时区的值如何,都会发生这种情况。

USE_TZTrue 时,这对于仍然依赖于本地时间的应用程序保持向后兼容性是有用的。但是,如上所述,这不是完全可靠的,你应该总是在自己的代码中使用UTC的知名数据时间。例如,使用 fromtimestamp() 并将 tz 参数设置为 utc

选择当前时区

当前时区等效于翻译的当前 语言环境。但是,没有等效的 Accept-Language HTTP头,Django可以用来自动确定用户的时区。相反,Django提供了 时区选择功能。使用它们来构建对您有意义的时区选择逻辑。

大多数关心时区的网站只是询问用户居住的时区,并将此信息存储在用户的个人资料中。对于匿名用户,他们使用其主要受众群体或UTC的时区。 pytz 提供 helpers,如每个国家/地区的时区列表,您可以使用它预先选择最可能的选择。

下面是一个将当前时区存储在会话中的示例。 (为了简单起见,它完全跳过错误处理。)

将以下中间件添加到 MIDDLEWARE:

import pytz

from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin

class TimezoneMiddleware(MiddlewareMixin):
    def process_request(self, request):
        tzname = request.session.get('django_timezone')
        if tzname:
            timezone.activate(pytz.timezone(tzname))
        else:
            timezone.deactivate()

创建一个可以设置当前时区的视图:

from django.shortcuts import redirect, render

def set_timezone(request):
    if request.method == 'POST':
        request.session['django_timezone'] = request.POST['timezone']
        return redirect('/')
    else:
        return render(request, 'template.html', {'timezones': pytz.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 tz in timezones %}
        <option value="{{ tz }}"{% if tz == TIME_ZONE %} selected="selected"{% endif %}>{{ tz }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set" />
</form>

表单中的时区感知输入

当您启用时区支持时,Django解释在 当前时区 中以表单形式输入的数据时间,并在 cleaned_data 中返回可识别的datetime对象。

如果当前时区为不存在或不明确的数据时间引发异常,因为它们落在DST转换中(pytz 提供的时区执行此操作),则此类数据时间将报告为无效值。

模板中的时区感知输出

当您启用时区支持时,Django将在模板中呈现的知道datetime对象转换为 当前时区。这个行为非常像 格式定位

警告

Django不转换天真的datetime对象,因为它们可能是模糊的,并且因为您的代码不应该产生幼稚的数据时间,当时区支持启用。但是,您可以使用下面描述的模板过滤器强制转换。

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

模板标签

localtime

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

对于模板引擎,此标签与 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 子类的实例或时区名称。如果是时区名称,则需要 pytz

例如:

{% 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 = False 切换到 USE_TZ = True,您必须将您的数据从本地时间转换为UTC - 这是不确定的,如果您的本地时间有DST。

第一步是将 USE_TZ = True 添加到您的设置文件并安装 pytz (如果可能)。在这一点上,事情应该主要工作。如果你在你的代码中创建天真的datetime对象,Django使他们知道在必要时。

但是,这些转换可能会在DST转换周围失败,这意味着您没有得到时区支持的全部好处。此外,你可能会遇到一些问题,因为它是不可能比较一个天真的datetime与意识datetime。由于Django现在给你知道的数据时间,你会得到异常,无论你比较来自一个模型或表单的日期时间与您在代码中创建的天真datetime。

所以第二步是重构代码,无论你实例化datetime对象,以使他们意识到。这可以递增地完成。 django.utils.timezone 为兼容性代码定义了一些方便的帮助器:now()is_aware()is_naive()make_aware()make_naive()

最后,为了帮助您定位需要升级的代码,当您尝试将无效datetime保存到数据库时,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',
)

夹具

当序列化意识的datetime时,包括UTC偏移,像这样:

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

对于天真的datetime,它显然不是:

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

对于具有 DateTimeField 的模型,这种差异使得不可能编写一个工作在有和没有时区支持的夹具。

使用 USE_TZ = False 或在Django 1.4之前生成的灯具使用“幼稚”格式。如果您的项目包含这样的灯具,在启用时区支持后,您将在加载它们时看到 RuntimeWarning。要摆脱警告,你必须将你的灯具转换为“意识”的格式。

您可以使用 loaddata,然后 dumpdata 重新生成灯具。或者,如果它们足够小,您可以简单地编辑它们以将与您的 TIME_ZONE 匹配的UTC偏移量添加到每个序列化的datetime。

常问问题

建立

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

    是。当启用时区支持时,Django使用更准确的本地时间模型。这将屏蔽您在夏令时(DST)转换周围的微妙和不可再现的错误。

    在这方面,时区与Python中的 unicode 相当。起初很难。您得到编码和解码错误。然后你学习规则。并且一些问题消失 - 你的应用程序接收到非ASCII输入时,不会再次出现错误的输出。

    当启用时区支持时,您将遇到一些错误,因为您使用的是天真的数据时间,其中Django需要知道数据时间。这样的错误显示在运行测试时,它们很容易解决。您将快速了解如何避免无效操作。

    另一方面,由于缺乏时区支持而导致的错误很难预防,诊断和修复。任何涉及计划任务或日期时间算法的事情都是一个微妙的错误,每年只会咬一次或一两次。

    由于这些原因,默认情况下在新项目中启用时区支持,并且您应该保留它,除非您有非常好的理由不要。

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

    也许。你更好地保护免受DST相关的错误,但你仍然可以通过不经意地将幼稚的数据时间转换为感知的数据时间拍摄自己,反之亦然。

    如果您的应用程序连接到其他系统 - 例如,如果它查询Web服务 - 确保正确指定了数据时间。要安全地传输数据时间,它们的表示应该包括UTC偏移量,或者它们的值应该是UTC(或两者都是!)。

    最后,我们的日历系统包含计算机的有趣陷阱:

    >>> import datetime
    >>> def one_year_before(value):       # DON'T DO THAT!
    ...     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. 我应该安装pytz吗?

    是。 Django有一个不需要外部依赖的策略,因此 pytz 是可选的。但是,安装它更安全。

    一旦激活时区支持,Django需要定义默认时区。当pytz可用时,Django从 tz database 加载此定义。这是最准确的解决方案。否则,它依赖于操作系统报告的本地时间和UTC之间的差异来计算转换。这不太可靠,特别是在DST转换周围。

    此外,如果你想支持用户在多个时区,pytz是时区定义的参考。

  4. 如何与在本地时间存储数据时间的数据库进行交互?

    DATABASES 设置中将 TIME_ZONE 选项设置为此数据库的相应时区。

    这对于连接到不支持时区并且在 USE_TZTrue 时不由Django管理的数据库非常有用。

故障排除

  1. 我的应用程序崩溃 TypeError: can't compare offset-naive and offset-aware datetimes ** - 怎么了?**

    让我们通过比较一个朴素的和一个意识到的datetime重现这个错误:

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

    如果你遇到这个错误,很可能你的代码是比较这两个东西:

    • 由Django提供的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被认为是在默认时区,这通常是你期望的。

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

    如果你总是使用天真的数据时间,你可能相信你可以通过调用它的 date() 方法将日期时间转换为日期。你也认为 date 很像 datetime,除了它不太准确。

    在时区感知环境中这不是真的:

    >>> import datetime
    >>> import pytz
    >>> paris_tz = pytz.timezone("Europe/Paris")
    >>> new_york_tz = pytz.timezone("America/New_York")
    >>> paris = paris_tz.localize(datetime.datetime(2012, 3, 3, 1, 30))
    # This is the correct way to convert between time zones with pytz.
    >>> new_york = new_york_tz.normalize(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=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    >>> new_york
    datetime.datetime(2012, 3, 2, 19, 30, tzinfo=<DstTzInfo 'America/New_York' EST-1 day, 19:00:00 STD>)
    

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

    datetime表示 时间点。它是绝对的:它不依赖于任何东西。相反,日期是 日历概念。这是一段时间,其范围取决于考虑日期的时区。如您所见,这两个概念根本不同,将日期时间转换为日期不是确定性操作。

    这在实践中意味着什么?

    一般来说,你应该避免将 datetime 转换为 date。例如,您可以使用 date 模板过滤器仅显示日期时间的日期部分。此过滤器将格式化之前将datetime转换为当前时区,以确保结果正确显示。

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

    >>> from django.utils import timezone
    >>> timezone.activate(pytz.timezone("Asia/Singapore"))
    # For this example, we just 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()
    # Again, this is the correct way to convert between time zones with pytz.
    >>> local = current_tz.normalize(paris.astimezone(current_tz))
    >>> local
    datetime.datetime(2012, 3, 3, 8, 30, tzinfo=<DstTzInfo 'Asia/Singapore' SGT+8:00:00 STD>)
    >>> local.date()
    datetime.date(2012, 3, 3)
    
  4. 我得到一个错误Are time zone definitions for your database and pytz installed?pytz是安装的,所以我想问题是我的数据库?

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

用法

  1. 我有一个字符串 "2012-02-21 10:28:45" 我知道它在 "Europe/Helsinki" 时区。我如何把它变成一个知道的datetime?

    这正是 pytz 的目的。

    >>> from django.utils.dateparse import parse_datetime
    >>> naive = parse_datetime("2012-02-21 10:28:45")
    >>> import pytz
    >>> pytz.timezone("Europe/Helsinki").localize(naive, is_dst=None)
    datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=<DstTzInfo 'Europe/Helsinki' EET+2:00:00 STD>)
    

    注意,localizetzinfo API的pytz扩展。此外,你可能想抓住 pytz.InvalidTimeError。 pytz的文档包含 more examples。您应该在尝试操作感知的数据时间之前查看它。

  2. 如何获取当前时区的当地时间?

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

    您应该只在与人交互时使用本地时间,模板层提供 过滤器和标签 将数据时间转换为您选择的时区。

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

    为了完整起见,如果你真的想要当前时区的当地时间,这里是如何获得它:

    >>> from django.utils import timezone
    >>> timezone.localtime(timezone.now())
    datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    

    在此示例中,安装了 pytz,当前时区为 "Europe/Paris"

  3. 如何查看所有可用的时区?

    pytz 提供 helpers,包括当前时区的列表和所有可用时区的列表 - 其中一些仅具有历史价值。