时区

概述¶ T0>

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

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

即使您的网站只有一个时区可用,在您的数据库中以UTC存储数据仍然是一个好习惯。 主要原因是夏令时(DST)。 许多国家都有夏令时制,春秋时节时钟前进。 如果你在当地工作,那么在转换发生的时候,你可能会每年遇到两次错误。 pytz文档详细讨论了这些问题)。 这可能对您的博客没有任何影响,但如果您每年以超过一个小时,每年两次的方式向客户收费或欠费,则会出现问题。 解决此问题的方法是在代码中使用UTC,仅在与最终用户交互时使用本地时间。

时区支持默认是禁用的。 要启用它,请在您的设置文件中设置USE_TZ = True 时区支持使用pytz,这是在安装Django时安装的。

在Django 1.11中更改:

旧版本不需要pytz或自动安装。

注意

The default settings.py file created by django-admin startproject includes USE_TZ = True for convenience.

注意

There is also an independent but related USE_L10N setting that controls whether Django should activate format localization. 有关更多详细信息,请参阅Format localization

如果您遇到特定问题,请从time zone FAQ开始。

概念¶ T0>

天真的和有意识的日期时间对象

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通常是安全的;如果您使用其他时区,则应仔细阅读pytz文档。

注意

Python’s datetime.time objects also feature a tzinfo attribute, and PostgreSQL has a matching time with time zone type. 但是,正如PostgreSQL的文档所说,这种类型“展现出导致可用的有用性的属性”。

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

天真的日期对象的解释

USE_TZTrue时,Django仍然接受朴素的datetime对象,以保持向后兼容性。 当数据库层接收到一个数据库层时,它会尝试通过在default time zone中解释它,并发出警告。

不幸的是,在DST转换期间,某些日期时间不存在或不明确。 在这种情况下,pytz引发了一个例外。 这就是为什么当启用时区支持时,应始终创建有意识的日期时间对象。

在实践中,这很少是一个问题。 Django为您提供了模型和表单中的日期时间对象,而且大多数情况下,新的日期时间对象是通过timedelta算法从现有对象创建的。 The only datetime that’s often created in application code is the current time, and timezone.now() automatically does the right thing.

默认时区和当前时区

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

当前时区是用于渲染的时区。

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

注意

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

USE_TZTrue时,这对保持仍然依赖本地时间的应用程序的向后兼容性很有用。 但是,as explained above,这不是完全可靠的,您应该始终使用自己的代码中的UTC日期时间。 For instance, use fromtimestamp() and set the tz parameter to utc.

选择当前时区

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

大多数关心时区的网站只是询问用户在哪个时区居住,并将这些信息存储在用户的个人资料中。 对于匿名用户,他们使用主要受众或UTC的时区。 pytz provides helpers, like a list of time zones per country, that you can use to pre-select the most likely choices.

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

添加以下中间件到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{% endif %}>{{ tz }}</option>
        {% endfor %}
    </select>
    <input type="submit" value="Set" />
</form>

表单中的时区感知输入

当您启用时区支持时,Django会解释current time zone中输入表单的日期时间,并在cleaned_data中返回已知日期时间对象。

如果当前时区对于不存在的或由于落入DST转换(由pytz提供的时区)执行此操作而不明确的日期时间引发异常,则此类日期时间将被报告为无效值。

模板中的时区感知输出

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

警告

Django不会转换朴素的日期时间对象,因为它们可能不明确,而且因为当启用时区支持时,您的代码永远不应该生成天真的日期时间。 但是,您可以使用下面介绍的模板过滤器强制进行转换。

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

模板标签

localtime

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

就模板引擎而言,该标签与USE_TZ设置具有完全相同的效果。 它允许更精细的转换控制。

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

{% load tz %}

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

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

注意

{% 本地时间 %} t3中不考虑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支持的时区之前启动的项目。

数据库¶ T0>

的PostgreSQL ¶ T0>

PostgreSQL后端将日期时间存储为timestamp 与 tt> 时间 区域 实际上,这意味着它将日期时间从连接的时区转换为UTC存储时间,从UTC转换为连接时区。

因此,如果您使用PostgreSQL,您可以在USE_TZ = FalseUSE_TZ = True 数据库连接的时区将分别设置为TIME_ZONEUTC,以便Django在所有情况下获得正确的日期时间。 您不需要执行任何数据转换。

其他数据库

其他后端存储没有时区信息的日期时间。 If you switch from USE_TZ = False to USE_TZ = True, you must convert your data from local time to UTC – which isn’t deterministic if your local time has DST.

代码¶ T0>

第一步是在您的设置文件中添加USE_TZ = True 在这一点上,事情应该主要工作。 如果你在你的代码中创建了朴素的日期时间对象,那么Django会在需要的时候让它们知道。

但是,这些转换可能会在DST转换期间失败,这意味着您尚未获得时区支持的全部好处。 此外,您可能会遇到一些问题,因为无法将天真的日期时间与已知的日期时间进行比较。 由于Django现在给你提供了日期时间,所以无论何时将来自模型或表单的日期时间与在代码中创建的天真日期时间进行比较,都会得到异常。

所以,第二步是在实例化日期时间对象的地方重构你的代码,让它们知道。 这可以逐步完成。 django.utils.timezone defines some handy helpers for compatibility code: now(), is_aware(), is_naive(), make_aware(), and 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',
)

夹具¶ T0>

在序列化一个知晓的日期时间时,包含UTC偏移量,如下所示:

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

对于一个天真的日期,显然不是:

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

For models with DateTimeFields, this difference makes it impossible to write a fixture that works both with and without time zone support.

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

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

FAQ ¶ T0>

设置¶ T0>

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

    是。 当启用时区支持时,Django使用更精确的本地时间模型。 这样可以避免夏令时(DST)过渡时出现的细微和不可重复的错误。

    当您启用时区支持时,您会遇到一些错误,因为您正在使用天真的日期时间,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. 如何与在当地时间存储日期时间的数据库进行交互?

    DATABASES设置中,将TIME_ZONE选项设置为该数据库的适当时区。

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

故障排除¶ T0>

  1. 我的应用程序崩溃 类型错误: 不能 比较 偏移天真 偏移量感知 日期时间 - 怎么了?

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

    >>> 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提供的日期时间 - 例如,从表单或模型字段读取的值。 由于您启用了时区支持,所以它是知道的。
    • 由你的代码生成的日期时间,这是天真的(或者你不会读这个)。

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

    如果你正在编写一个可以独立于USE_TZ值工作的可插拔应用程序,你可能会发现django.utils.timezone.now()有用。 This function returns the current date and time as a naive datetime when USE_TZ = False and as an aware datetime when USE_TZ = True. 您可以根据需要添加或减去datetime.timedelta

  2. 我看到很多 RuntimeWarning: DateTimeField字段 收到 a 幼稚 约会时间 (YYYY-MM-DD HH:MM:SS) while time zone support is active - 那不好吗?

    当启用时区支持时,数据库层只希望从您的代码中获得有意义的日期时间。 当收到一个天真的日期时间发生此警告。 这表明你还没有完成移植你的代码的时区支持。 有关此过程的提示,请参阅migration guide

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

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

    如果你一直使用天真的日期时间,你可能会相信你可以通过调用它的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转换为date 例如,您可以使用date模板过滤器仅显示日期时间的日期部分。 格式化此过滤器会将日期时间转换为当前时区,确保结果正确显示。

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

    >>> 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. 我收到一个错误 时间 定义 对于 你的 数据库 安装?

    如果您正在使用MySQL,请参阅MySQL注释的Time zone definitions部分以获取关于加载时区定义的说明。

用法¶ T0>

  1. 我有一个字符串 “2012-02-21 10:28:45” 而且我知道它在 “欧洲/赫尔辛基” T0> 时区。 我如何把它变成一个意识到的日期时间?

    这正是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的文档包含更多的例子 在尝试操作感知日期时间之前,您应该查看它。

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

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

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

    此外,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>)
    

    在这个例子中,当前时区是"Europe/Paris"

  3. 我怎样才能看到所有可用的时区?

    pytz provides helpers, including a list of current time zones and a list of all available time zones – some of which are only of historical interest.