时区

概述¶ T0>

当时区支持开启时,Django将时间用UTC格式存储到数据库中,在内部使用时区相关的对象,并且在模板(templates)与表单(forms)中将时间转换为终端用户所在时区的时间

当你的用户生活在多个时区,并且你希望根据他们所在的位置显示当地时间时很有用

即使您的网站仅在一个时区可用,仍然是在UTC数据库中存储数据的好习惯。 主要原因是夏令时(DST)。 许多国家都拥有自己的一套夏令时系统,在这套系统里,春季的时间会提前,而秋季的时间便会后延。 如果你只以当前时间为标准来开发,每年都会因为夏令时而引起两次错误 pytz文档更详细地讨论了这些问题。) 这个对于你的博客可能没有什么影响,但是如果涉及到按年,按月,按小时来收费的话,那么就会是一个问题 ,解决这个问题的方法便是在代码中使用UTC时间,仅在与最终用户进行交互的时候使用本地时间。

Django默认关闭时区支持, 如欲开启时区支持,则需在settings中设置USE_TZ = True 时区支持使用pytz,它是在安装Django时安装的。

在Django 1.11中被修改:

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

为方便起见,在由django-admin startproject 创建的settings.py文件中已设置 USE_TZ = True

另外在settings中,还有一个 USE_L10N设置选项,使用它可控制Django是否激活格式本地化。 更多细节请参见Format localization

如果你正为某个与时区相关的特殊问题而纠结, 请阅读time zone FAQ

概念¶ T0>

Naive和aware datetime对象

Python的 datetime.datetime 对象有一个 tzinfo 属性,该属性是datetime.tzinfo子类的一个实例,它被用来存储时区信息。 当某个datetime对象的tzinfo属性被设置并给出一个时间偏移量时,我们称该datetime对象是aware(已知)的。 否则称其为 naive(原生)的。

可用is_aware()is_naive() 函数来判断某个datetime对象是aware类型或naive类型。

当关闭时区支持时(USE_TZ=False), Django使用原生的datetime对象保存本地时间。 在许多应用中,这是最简单的方式,并且足以满足要求。 在这种情况下,可使用下列代码获取当前时间:I

import datetime

now = datetime.datetime.now()

当开启时区支持时 (USE_TZ=True), Django使用已知(aware)的datetime对象存储本地时间。 如果在代码中创建了datetime对象, 那么它们也应该是aware类型的datetime对象。 在此情况下,上述例子变成:

from django.utils import timezone

now = timezone.now()

警告:

对aware类型的datetime对象的处理并非总是非常直观。 例如:对DST时区来说,标准datetime对象构造器的tzinfo 参数并不能可靠地工作。 使用UTC通常是安全的;如果您正在使用其他时区,请仔细阅读pytz文档。

Python的 datetime.time 对象也具有包含一个 tzinfo属性的特点 , 并且在PostgreSQL数据库引擎中,有一个time with time zone 类型与该属性相匹配。 但是,正如PostgreSQL的文档所指出的那样,该类型 “禁止使用那些可能导致出现问题的属性“。

Django仅支持naive(原生)类型的time对象, 并且,当试图保存一个aware类型的time对象时将会触发一个异常。这是因为,对于时区来说,如果保存的时间没有带有相关日期信息那就没有什么意义。.

天真的日期时间对象的解释

USE_TZTrue, Django 为了保持向后兼容性依旧接受 naive datetime 对象 . 当数据库层收到一个数据库时,它会尝试通过在default time zone中解释它来提醒它,并发出警告。

不幸的是,在DST转换期间,一些数据时间不存在或不明确。 在这种情况下,pytz引发异常。 这就是为什么当启用时区支持时,您应该始终创建感知datetime对象。

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

默认时区和当前时区

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

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

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

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

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

选择当前时区

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

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

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

将以下中间件添加到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使用当前时区解释表单输入的时间,并在cleaned_data中返回aware datetime对象。

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

Time zone aware output in templates

启用时区支持时,Django在模板中呈现时,将知道的datetime对象转换为current time zone 这与format localization

警告:

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

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

模板标签

localtime

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

对于模板引擎,此标记与USE_TZ设置具有完全相同的效果。 它允许更细粒度的转换控制。

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

{% load tz %}

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

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

USE_TZ的值在{% localtime %} t3 >块。

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 时间 区域 实际上,这意味着它将数据时间从连接的时区转换为存储上的UTC,以及从UTC转换为检索时的连接的时区。

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

其他数据库

其他后端存储没有时区信息的数据时间。 如果您从USE_TZ = False切换到USE_TZ = True,您必须将您的数据从本地时间转换为UTC - 如果您的当地时间有DST,这是不确定的。

代码¶ T0>

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

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

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

夹具¶ T0>

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

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

对于naive datetime,它显然不是:

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

对于具有DateTimeField的模型,此差异使得无法编写支持和不支持时区支持的灯具。

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

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

FAQ ¶ T0>

设置¶ T0>

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

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

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

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

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

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

  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. 如何与在当地时间存储数据时间的数据库进行交互?

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

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

故障排除¶ T0>

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

    让我们通过比较一个naive和一个意识datetime重现这个错误:

    >>> import datetime
    >>> from django.utils import 时区
    >>> naive = datetime.datetime.utcnow()
    >>> aware = 时区.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时,此函数将当前日期和时间返回为原始日期时间, USE_TZ = True 您可以根据需要添加或减少datetime.timedelta

  2. 我看到很多 RuntimeWarning: DateTimeField字段 收到 a 幼稚 约会时间 (YYYY-MM-DD HH:MM:SS) 时间 区域 支持 活动 - 那不好吗?

    当启用时区支持时,数据库层希望从代码中仅接收知道的数据时间。 当它收到一个天真的datetime时,会发生此警告。 这表示您尚未完成移植您的代码以支持时区。 有关此过程的提示,请参阅migration guide

    在此期间,为了向后兼容,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 时区
    >>> 时区.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 = 时区.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 时区
    >>> 时区.localtime(timezone.now())
    datetime.datetime(2012, 3, 3, 20, 10, 53, 873365, tzinfo=<DstTzInfo 'Europe/Paris' CET+1:00:00 STD>)
    

    In this example, the current time zone is "Europe/Paris".

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

    pytz提供帮助,包括当前时区列表和所有可用时区列表,其中一些只有历史价值。