时区 — Django 文档
时区
概述
当启用对时区的支持时,Django 将日期时间信息以 UTC 格式存储在数据库中,在内部使用时区感知日期时间对象,并在模板和表单中将它们转换为最终用户的时区。
如果您的用户居住在多个时区并且您希望根据每个用户的挂钟显示日期时间信息,这将非常方便。
即使您的网站仅在一个时区可用,在您的数据库中以 UTC 格式存储数据仍然是一种很好的做法。 主要原因是夏令时 (DST)。 许多国家都有夏令时系统,时钟在春季向前移动,在秋季向后移动。 如果您在当地时间工作,当转换发生时,您每年可能会遇到两次错误。 (pytz 文档更详细地讨论了 这些问题 。)这对您的博客可能无关紧要,但如果您的客户通过一小时,一年两次,每年一次。 解决这个问题的办法是在代码中使用UTC,只有在与最终用户交互时才使用本地时间。
默认情况下禁用时区支持。 要启用它,请设置 :设置:`USE_TZ = True ` 在您的设置文件中。 时区支持使用 pytz,它在您安装 Django 时安装。
如果您正在解决某个特定问题,请从 时区常见问题解答 开始。
概念
天真的和感知日期时间对象
Python 的 datetime.datetime
对象有一个 tzinfo
属性,可用于存储时区信息,表示为 datetime.tzinfo
的子类的实例。 当设置此属性并描述偏移量时,日期时间对象是 aware。 否则,它是 天真 。
您可以使用 is_aware() 和 is_naive() 来确定日期时间是感知还是幼稚。
当禁用时区支持时,Django 使用本地时间的原始日期时间对象。 这对于许多用例来说已经足够了。 在这种模式下,要获取当前时间,您可以编写:
import datetime
now = datetime.datetime.now()
启用时区支持时 ( :设置:`USE_TZ=真 ` ),Django 使用时区感知日期时间对象。 如果您的代码创建了 datetime 对象,它们也应该知道。 在这种模式下,上面的例子变成:
from django.utils import timezone
now = timezone.now()
警告
处理有意识的日期时间对象并不总是直观的。 例如,标准日期时间构造函数的 tzinfo
参数不适用于 DST 时区。 使用 UTC 通常是安全的; 如果您使用其他时区,则应仔细查看 pytz 文档。
笔记
Python 的 datetime.time
对象也具有 tzinfo
属性,而 PostgreSQL 具有匹配的 time with time zone
类型。 然而,正如 PostgreSQL 的文档所说,这种类型“展示了导致可疑用途的属性”。
Django 仅支持原始时间对象,如果您尝试保存一个感知时间对象,则会引发异常,因为没有关联日期的时间的时区是没有意义的。
解释朴素的日期时间对象
当 :setting:`USE_TZ` 是 True
时,Django 仍然接受原始日期时间对象,以保持向后兼容性。 当数据库层收到一个时,它试图通过在 默认时区 中解释它来使其意识到并引发警告。
不幸的是,在 DST 转换期间,某些日期时间不存在或不明确。 在这种情况下, pytz 会引发异常。 这就是为什么在启用时区支持时您应该始终创建感知日期时间对象的原因。
在实践中,这很少成为问题。 Django 为您提供模型和表单中的日期时间对象,并且大多数情况下,新的日期时间对象是通过 timedelta
算法从现有对象创建的。 通常在应用程序代码中创建的唯一日期时间是当前时间,而 timezone.now() 会自动执行正确的操作。
默认时区和当前时区
默认时区是由:setting:`TIME_ZONE`设置定义的时区。
当前时区是用于渲染的时区。
您应该使用 activate() 将当前时区设置为最终用户的实际时区。 否则,将使用默认时区。
笔记
如 :setting:`TIME_ZONE` 文档中所述,Django 设置环境变量,以便其进程在默认时区运行。 无论 :setting:`USE_TZ` 和当前时区的值如何,都会发生这种情况。
当 :setting:`USE_TZ` 是 True
时,这对于保持与仍然依赖本地时间的应用程序的向后兼容性很有用。 但是, 如上所述 ,这并不完全可靠,您应该始终在自己的代码中使用 UTC 中的已知日期时间。 例如,使用 fromtimestamp()
并将 tz
参数设置为 utc。
选择当前时区
当前时区相当于当前的 locale 用于翻译。 但是,没有与 Django 可以用来自动确定用户时区的 Accept-Language
HTTP 标头等效的标头。 相反,Django 提供了 时区选择功能 。 使用它们来构建对您有意义的时区选择逻辑。
大多数关心时区的网站都会询问用户他们居住在哪个时区,并将这些信息存储在用户的个人资料中。 对于匿名用户,他们使用主要受众的时区或 UTC。 pytz 提供 helpers,例如每个国家/地区的时区列表,您可以使用它来预先选择最可能的选项。
这是一个在会话中存储当前时区的示例。 (为了简单起见,它完全跳过了错误处理。)
将以下中间件添加到 :setting:`MIDDLEWARE`:
import pytz
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(pytz.timezone(tzname))
else:
timezone.deactivate()
return self.get_response(request)
创建一个可以设置当前时区的视图:
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
中返回已知的日期时间对象。
如果当前时区对不存在或不明确的日期时间引发异常,因为它们属于 DST 转换(pytz 提供的时区执行此操作),则此类日期时间将报告为无效值。
模板中的时区感知输出
当您启用时区支持时,Django 会在模板中呈现时将感知日期时间对象转换为 当前时区 。 这很像 格式本地化 。
警告
Django 不会转换原始日期时间对象,因为它们可能不明确,并且当启用时区支持时,您的代码不应产生原始日期时间。 但是,您可以使用下面描述的模板过滤器强制转换。
转换为当地时间并不总是合适的——您可能正在为计算机而不是人类生成输出。 tz
模板标签库提供的以下过滤器和标签允许您控制时区转换。
模板过滤器
这些过滤器接受感知和朴素的日期时间。 出于转换目的,他们假设朴素的日期时间在默认时区中。 他们总是返回知道的日期时间。
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 = False
和 USE_TZ = True
之间自由切换。 数据库连接的时区将分别设置为 :setting:`TIME_ZONE` 或 UTC
,以便 Django 在所有情况下都能获得正确的日期时间。 您不需要执行任何数据转换。
其他数据库
其他后端存储没有时区信息的日期时间。 如果您从 USE_TZ = False
切换到 USE_TZ = True
,您必须将您的数据从本地时间转换为 UTC - 如果您的本地时间具有 DST,则这不确定。
代码
第一步是添加 :设置:`USE_TZ = True ` 到您的设置文件。 在这一点上,事情应该主要工作。 如果您在代码中创建了原始日期时间对象,Django 会在必要时让它们知道。
但是,这些转换可能会在 DST 转换时失败,这意味着您还没有获得时区支持的全部好处。 此外,您可能会遇到一些问题,因为无法将原始日期时间与已知日期时间进行比较。 由于 Django 现在为您提供可感知的日期时间,因此无论您将来自模型或表单的日期时间与您在代码中创建的简单日期时间进行比较,您都会得到异常。
所以第二步是重构你的代码,无论你在哪里实例化 datetime 对象,让它们知道。 这可以逐步完成。 django.utils.timezone 为兼容性代码定义了一些方便的助手: now(), is_aware(), is_naive(), [ X140X]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"
对于具有 DateTimeFields 的模型,这种差异使得无法编写同时支持和不支持时区的夹具。
使用 USE_TZ = False
或 Django 1.4 之前生成的装置使用“naive”格式。 如果您的项目包含此类灯具,则在启用时区支持后,您将在加载它们时看到 RuntimeWarning
。 要消除警告,您必须将灯具转换为“感知”格式。
您可以使用 :djadmin:`loaddata` 然后 :djadmin:`dumpdata` 重新生成设备。 或者,如果它们足够小,您可以编辑它们以将与 :setting:`TIME_ZONE` 匹配的 UTC 偏移量添加到每个序列化日期时间。
常见问题
设置
我不需要多个时区。 我应该启用时区支持吗?
是的。 启用时区支持后,Django 使用更准确的本地时间模型。 这可以保护您免受夏令时 (DST) 过渡的微妙和不可重现的错误的影响。
当您启用时区支持时,您会遇到一些错误,因为您正在使用 Django 期望知道日期时间的朴素日期时间。 运行测试时会出现此类错误。 您将很快了解如何避免无效操作。
另一方面,由于缺乏时区支持而导致的错误更难以预防、诊断和修复。 任何涉及计划任务或日期时间算术的东西都是微妙错误的候选者,这些错误每年只会咬你一两次。
由于这些原因,默认情况下在新项目中启用时区支持,除非您有很好的理由不这样做,否则您应该保留它。
我启用了时区支持。 我安全吗?
也许。 您可以更好地避免与 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,这取决于您的业务需求。
如何与以本地时间存储日期时间的数据库交互?
设置 :设置:`TIME_ZONE ` 选择为此数据库中的适当时区 :设置:`数据库` 环境。
当 :setting:`USE_TZ` 是
True
时,这对于连接到不支持时区且不受 Django 管理的数据库很有用。
故障排除
我的应用程序崩溃了
TypeError: can't compare offset-naive
and offset-aware datetimes
- 怎么了?让我们通过比较天真和有意识的日期时间来重现此错误:
>>> 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 提供的日期时间——例如,从表单或模型字段中读取的值。 由于您启用了时区支持,因此它知道。
由您的代码生成的日期时间,这是幼稚的(否则您就不会阅读本文)。
通常,正确的解决方案是更改您的代码以改用可感知的日期时间。
如果你正在编写一个可插拔的应用程序,它应该独立于 :setting:`USE_TZ` 的值工作,你可能会发现 django.utils.timezone.now() 很有用。 此函数返回当前日期和时间作为
USE_TZ = False
时的原始日期时间和USE_TZ = True
时的感知日期时间。 您可以根据需要添加或减去datetime.timedelta
。我看到很多
RuntimeWarning: DateTimeField received a naive datetime
(YYYY-MM-DD HH:MM:SS)
while time zone support is active
- 那不好吗?启用时区支持后,数据库层希望仅从您的代码中接收可感知的日期时间。 当它收到一个天真的日期时间时会发生此警告。 这表明您尚未完成移植代码以获得时区支持。 有关此过程的提示,请参阅 迁移指南 。
同时,为了向后兼容,日期时间被视为默认时区,这通常是您所期望的。
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
转换为date
。 例如,您可以使用 :tfilter:`date` 模板过滤器来仅显示日期时间的日期部分。 此过滤器将在格式化之前将日期时间转换为当前时区,以确保结果正确显示。如果您确实需要自己进行转换,则必须先确保将日期时间转换为适当的时区。 通常,这将是当前时区:
>>> from django.utils import timezone >>> timezone.activate(pytz.timezone("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() # 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)
我收到一个错误“
Are time zone definitions for your database installed?
”如果您使用的是 MySQL,请参阅 MySQL 注释的 时区定义 部分以获取有关加载时区定义的说明。
用法
我有一个字符串
"2012-02-21 10:28:45"
我知道它在"Europe/Helsinki"
时区。 我如何把它变成一个有意识的日期时间?这正是 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>)
请注意,
localize
是tzinfo
API 的 pytz 扩展。 此外,您可能想捕获pytz.InvalidTimeError
。 pytz 的文档包含 更多示例 。 在尝试操纵已知日期时间之前,您应该查看它。如何获取当前时区的当地时间?
那么,第一个问题是,你真的需要吗?
当您与人类互动时,您应该只使用本地时间,并且模板层提供 过滤器和标签 将日期时间转换为您选择的时区。
此外,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"
。如何查看所有可用的时区?