时区¶
概述¶
启用时区支持后,Django 会将日期时间信息存储在数据库中的 UTC 中,在内部使用时区感知的日期时间对象,并在模板和表单中将其转换为最终用户的时区。
如果您用户的居住地跨越多个时区,并且希望根据每个用户的时钟显示日期时间信息,则此功能非常方便。
即使您的网站仅在一个时区可用,也仍然建议将数据存储在数据库中的 UTC 中。主要原因是夏令时 (DST)。许多国家/地区都采用夏令时制度,在春季将时钟拨快,在秋季将时钟拨慢。如果您使用的是本地时间,则每年在转换发生时,您可能会遇到两次错误。这对您的博客可能无关紧要,但如果您每年两次对客户少计费或多计费一小时,则这是一个问题。解决此问题的方案是在代码中使用 UTC,仅在与最终用户交互时使用本地时间。
默认情况下启用时区支持。要禁用它,请在 settings 文件中设置 USE_TZ = False
。
在旧版本中,默认情况下禁用时区支持。
时区支持使用 zoneinfo
,它是 Python 3.9 及更高版本中 Python 标准库的一部分。
如果您正在解决某个特定问题,请从 时区常见问题解答 开始。
概念¶
朴素和感知的日期时间对象¶
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()
警告
处理感知的日期时间对象并不总是直观的。例如,标准 datetime 构造函数的 tzinfo
参数对于具有 DST 的时区来说并不总是可靠。使用 UTC 通常是安全的;如果您使用的是其他时区,则应仔细查看 zoneinfo
文档。
注意
Python 的 datetime.time
对象也具有 tzinfo
属性,并且 PostgreSQL 有一个匹配的 time with time zone
类型。但是,正如 PostgreSQL 的文档所说,这种类型“表现出导致其实用性值得怀疑的属性”。
Django 仅支持朴素的时间对象,并且如果尝试保存感知的时间对象,则会引发异常,因为对于没有关联日期的时间来说,时区没有意义。
朴素日期时间对象的解释¶
当 USE_TZ
为 True
时,Django 仍然接受朴素的日期时间对象,以保持向后兼容性。当数据库层接收到一个朴素的日期时间对象时,它会尝试通过在 默认时区 中解释它来使其感知,并发出警告。
不幸的是,在 DST 转换期间,某些日期时间不存在或模棱两可。这就是为什么在启用时区支持时,您应该始终创建感知的日期时间对象。(有关使用 fold
属性指定在 DST 转换期间应应用于日期时间的偏移量的示例,请参阅 zoneinfo
文档的 Using ZoneInfo section of the zoneinfo docs
部分)。
在实践中,这很少是一个问题。Django 在模型和表单中为您提供了感知的日期时间对象,并且大多数情况下,新的日期时间对象是通过 timedelta
算术从现有对象创建的。应用程序代码中经常创建的唯一日期时间是当前时间,而 timezone.now()
会自动执行正确的操作。
默认时区和当前时区¶
默认时区是由 TIME_ZONE
设置定义的时区。
当前时区是用于呈现的时区。
您应该使用 activate()
将当前时区设置为最终用户的实际时区。否则,将使用默认时区。
选择当前时区¶
当前时区相当于翻译的当前 语言环境。但是,没有等效于 Django 可以用来自动确定用户时区的 Accept-Language
HTTP 标头。相反,Django 提供了 时区选择函数。使用它们构建对您有意义的时区选择逻辑。
大多数关心时区的网站都会询问用户居住在哪个时区,并将此信息存储在用户的个人资料中。对于匿名用户,他们使用其主要受众的时区或 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 会以 当前时区 解释表单中输入的日期时间,并在 cleaned_data
中返回时区感知的日期时间对象。
由于处于夏令时转换期间而导致不存在或模棱两可的转换日期时间将被报告为无效值。
模板中的时区感知输出¶
启用时区支持后,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
之间自由切换。数据库连接的时区将分别设置为 DATABASE-TIME_ZONE
或 UTC
,以便 Django 在所有情况下都能获取正确的日期时间。您无需执行任何数据转换。
其他数据库¶
其他后端存储日期时间时不包含时区信息。如果您从 USE_TZ = False
切换到 USE_TZ = True
,则必须将数据从本地时间转换为 UTC——如果您的本地时间有夏令时,则这并非确定性的。
代码¶
第一步是将 USE_TZ = True
添加到您的设置文件中。此时,事情应该大部分都能正常工作。如果您在代码中创建了朴素的日期时间对象,则 Django 会在必要时将其设为时区感知的。
但是,这些转换可能会在夏令时转换附近失败,这意味着您还没有获得时区支持的全部好处。此外,您可能会遇到一些问题,因为无法将朴素的日期时间与时区感知的日期时间进行比较。由于 Django 现在为您提供了时区感知的日期时间,因此您将在将来自模型或表单的日期时间与您在代码中创建的朴素的日期时间进行比较的任何地方遇到异常。
因此,第二步是在您实例化日期时间对象以使其成为时区感知的任何地方重构您的代码。这可以逐步完成。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
的模型,这种差异使得无法编写既适用于时区支持又适用于不支持时区的夹具。
使用 USE_TZ = False
或在 Django 1.4 之前生成的夹具使用“朴素”格式。如果您的项目包含此类夹具,则启用时区支持后,加载它们时您会看到 RuntimeWarning
。要消除警告,您必须将夹具转换为“时区感知”格式。
您可以使用 loaddata
然后 dumpdata
重新生成初始数据。或者,如果数据量足够小,您可以编辑它们,为每个序列化日期时间添加与您的 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
选项设置为DATABASES
设置中此数据库的相应时区。当
USE_TZ
为True
时,这对于连接到不支持时区且未由 Django 管理的数据库很有用。
故障排除¶
我的应用程序崩溃并显示
TypeError: can't compare offset-naive
and offset-aware datetimes
– 什么问题?让我们通过比较朴素日期时间和感知时区的日期时间来重现此错误。
>>> 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
。我看到了很多
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 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'))
如本例所示,相同的日期时间具有不同的日期,具体取决于表示它的时区。但真正的问题更根本。
日期时间表示一个时间点。它是绝对的:它不依赖于任何事物。相反,日期是一个日历概念。它是一段时间,其边界取决于考虑日期的时区。如您所见,这两个概念从根本上不同,将日期时间转换为日期不是确定性操作。
这在实践中意味着什么?
通常,您应该避免将
datetime
转换为date
。例如,您可以使用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)
我收到错误“
Are time zone definitions for your database installed?
”如果您使用的是 MySQL,请参阅 MySQL 说明中的 时区定义 部分,了解有关加载时区定义的说明。
用法¶
我有一个字符串
"2012-02-21 10:28:45"
并且我知道它位于"Europe/Helsinki"
时区。如何将其转换为感知时区的日期时间?在这里,您需要创建所需的
ZoneInfo
实例并将其附加到朴素日期时间。>>> 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'))
如何获取当前时区的本地时间?
嗯,第一个问题是,您真的需要这样做吗?
只有在与人类交互时才应使用本地时间,并且模板层提供了 过滤器和标签 来将日期时间转换为您选择的时区。
此外,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=zoneinfo.ZoneInfo(key='Europe/Paris'))
在此示例中,当前时区为
"Europe/Paris"
。如何查看所有可用的时区?
zoneinfo.available_timezones()
提供了系统可用的所有有效 IANA 时区的键集。请参阅文档以了解使用注意事项。