Time zones¶
概况¶
当启用了时区支持,Django 在数据库里以 UTC 存储日期信息,在内部使用时区感知日期对象,并且在模板和表单中转换为最终用户的时区。
如果用户居住在多个时区时,这会很方便。你要根据用户的时间来显示日期信息。
即便你的网站只在一个时区可用,在数据库中以 UTC 来存储数据是个好习惯。主要原因是夏令时。很多地方都有夏令时系统,时钟在春季向前调整,秋季向后调整。如果你以当地时间起居,那么时区转换时,每年都可能会遇到两次错误。也许这对你的博客没什么影响,但是每年两次,一次一小时多收或是少收用户的费用,这就是个问题。解决办法就是在代码里使用 UTC,并且在与最终用户交互时,使用当地时间。
时区支持模式默认是关闭的,如果要启用它,在配置文件里设置 USE_TZ = True
。时区支持模式使用 pytz ,在安装 Django 的时候就已经安装好它了。
注解
方便起见,在执行 django-admin startproject
后创建默认的 settings.py
文件包含 USE_TZ = True
。
如果你在解决一个特定问题,从阅读 time zone FAQ 开始。
概念¶
Naive 日期对象与 Aware 日期对象¶
Python 的 datetime.datetime
对象有一个 tzinfo
属性,它可以存储时区信息,表示为 datetime.tzinfo
子类的一个实例。当设置这个属性并描述了偏移量后,日期对象就是 aware 的,否则就是 naive 的。
你可以使用 is_aware()
和 is_naive()
来决定日期是 aware 还是 naive 的。
当关闭了时区支持,Django 会在本地时间里使用原生日期对象。这对很多用例来说足够了。在这个模式下,如果你想获取当前时间,你可以这么写:
import datetime
now = datetime.datetime.now()
当启用了时区支持 (USE_TZ=True
) ,Django 使用 time-zone-aware 日期对象。如果你的代码创建了日期对象,她们应该也是 aware 的。在这个模式下,上面的例子变成:
from django.utils import timezone
now = timezone.now()
警告
处理 aware 日期对象并不一直都是直观的。比如,标准日期构造函数的``tzinfo`` 参数对于使用 DST 的时区并不可靠。使用 UTC 一般是安全的;如果你正在使用其他时区,你应该认真地看一下 pytz 文档。
注解
Python 的 datetime.time
对象还具有 tzinfo
属性,PostgreSQL 具有匹配 带有时区时间
的类型。但是,正如 PostgreSQL 所描述的,这个类型 "表现出导致问题的可用性问题"。
Django 只支持 naive 时间对象,如果打算保存 aware 时间对象会引发异常,因为没有关联日期的时区的时间是没有意义的。
Naive 日期对象的说明¶
当 USE_TZ
为 True
时,Django 仍然接受 naive 日期对象,以保持向后兼容。当数据库层收到一个日期对象,会试着让日期对象在 default time zone 里进行解释将其变为 aware 日期对象,并发出警告。
不幸的是,在 DST 转换期间,一些日期时间不存在或不明确。在这些情况下,pytz 会引发一个异常。这就是为什么当启用时区支持的时候,需要始终创建 aware 时间对象的原因。
实际上,这种情况很罕见。Django 在模型和表单里为你提供了 aware 日期对象,并且在大部分时候,新的日期对象是通过 timedelta
算法从现有的对象创建的。常在应用代码里创建的日期时间是当前时间,timezone.now()
会自动完成这个操作。
默认时区和当前时区¶
默认时区 是通过 TIME_ZONE
定义的时区。
当前时区 是用来渲染的时区。
你应该使用 activate()
将当前时区设置为最终用户的实际时区。
注解
如 TIME_ZONE
文档描述,Django 设置了环境变量,因此它的进程在默认时区里运行。无论 USE_TZ
的值和当前时区如何,都会发生这种情况。
当 USE_TZ
设置为 True
时,这样有助于保持仍然需要依赖当地时间的应用程序的后端兼容性。然而,就像前面所说(as explained above),这样并不完全可靠,你应该始终在代码里使用 UTC 里的 aware 日期来工作。比如,使用 fromtimestamp()
并且将 tz
参数设置为 utc
。
选择当前时区¶
当前时区相当于转换 current locale 。但是,Django 没有可用于自动确定用户时区的 Accept-Language
HTTP header 。相反,Django 提供了时区选择函数( time zone selection functions )。使用它们来建立对你有用的时区选择逻辑。
很多关心用户时区的站点会询问用户所在的时区,并且把这个信息保存在用户资料里。对于匿名用户,他们使用网站主要受众群体的时区或者 UTC。pytz 提供了帮助( helpers ),例如每个国家时区的列表,这样你可以预先选择最有可能的选项。
这里有个在会话(session)里存储当前时区的例子。(为简单起见,它完全跳过了错误处理)
在 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>
表单里的时区感知(aware)输入¶
当启用了时区支持,Django 会解释当前时区中以表格形式输入的日期时间,并且在 cleaned_data
中返回 aware 日期对象。
如果当前时区在夏令时转换(由 pytz 提供的时区)时因日期不存在或者不明确而引发了异常,此类日期时间将被报告为无效值。
模板中时区感知(aware)输出¶
当启用了时区支持,Django 会在模板中渲染 aware 日期时间时,将其转换为当前时区。这非常类似于本地化格式( format localization )。
警告
Django 不会转换 naive 日期对象,因为它们是不确定的,而且当开启了时区支持后,代码里绝不能生成 naive 日期。但是,你可以使用下面描述的模板过滤器那样强制转换。
转换为本地时间并不总是合适的——你或许是为计算机而不是为人类生成输出。下面的过滤器和标签,由 tz
模板标签库支持,允许你控制时区转换。
模板标签¶
localtime
¶
在包含块里开启或关闭将 aware 日期时间对象的转换为当前时区。
就模板引擎而言,该标签与 USE_TZ
具有完全相同的效果。它可以更精细地控制转换。
要模板块激活或关闭转换,使用:
{% load tz %}
{% localtime on %}
{{ value }}
{% endlocaltime %}
{% localtime off %}
{{ value }}
{% endlocaltime %}
注解
USE_TZ
的值在 {% localtime %}
块内被忽略。
timezone
¶
在包含块内设置或取消当前时区。当没有设置当前时区时,会使用默认时区。
{% load tz %}
{% timezone "Europe/Paris" %}
Paris time: {{ value }}
{% endtimezone %}
{% timezone None %}
Server time: {{ value }}
{% endtimezone %}
迁移指南¶
以下是迁移在 Django 支持时区之前已有项目的方法。
数据库¶
PostgreSQL¶
PostgreSQL 后端存储将日期时间存储为 带时区的时间戳
。事实上,这意味着它在存储时会将日期从连接的时区转换为UTC,并在检索时将UTC转换为连接的时区。
因此,如果你正在使用 PostgreSQL,你可以在 USE_TZ = False
和 USE_TZ = True
之间自由选择,数据库连接的时区将分别设置为 TIME_ZONE
或 UTC
,以便 Django 在所有情况下将得到正确的日期。你不需要执行任何数据转换。
其他数据库¶
其他后端存储没有时区信息的日期。如果你的选择从 USE_TZ = False
变为 USE_TZ = True
,你必须将你的数据从本地时间转换为UTC —— 如果你的当地时间有夏令时时,则不确定。
邮政编码¶
第一步是将 USE_TZ = True
添加到你的配置文件里。在这点上,大多数情况下都应该起作用。如果你在代码里创建 naive 日期时间对象,Django 会在必要时将它们转换为 aware 日期时间对象。
然而,这些转换有可能在夏令时转换时失败,这意味着你并有获得时区支持的所有好处。而且,在运行的时候很可能会遇到一些问题,因为它无法将 naive 日期时间和 aware 日期时间进行比较。由于 Django 现在为你提供了 aware 时间,你在比较来自模型或表单的日期与代码里创建的naive日期时间时,会遇到一些异常。
第二步是重构你在任何地方实例化的日期时间,将它们转换为 aware 日期时间。这可以逐步完成。django.utils.timezone
为了代码兼容性而定义了一些方法:now()
, is_aware()
, is_naive()
, make_aware()
, 和 make_naive()
。
最后,为了帮助你定位需要升级的代码,Django 会在你试图保存 naive 日期代码到数据库的时候,引发一个告警。
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',
)
辅助工具¶
当序列化 aware 日期时间时,会包含 UTC 偏移,如下:
"2011-09-01T13:20:30+03:00"
对于 naive 时间,它不能这样:
"2011-09-01T13:20:30"
对于带有 DateTimeField
的模型,这种差异使得编写一个支持和不支持时区的辅助工具变得不可能。
使用 USE_TZ = False
生成辅助工具,或者 Django 1.4 版本之前,使用 "naive" 格式化。如果你的项目包含这些辅助工具,则在开始时区支持后,你将会在加载它们时,看到 RuntimeWarning
。为了避免这些告警,你必须将辅助工具转换为 "aware" 格式。
你可以先使用 loaddata
然后 dumpdata
重新生成辅助工具。或者,如果它们足够小,你可以对它们编辑,与将匹配到 TIME_ZONE
的 UTC 偏移量添加到每个序列化日期时间。
FAQ¶
安装¶
我不需要多时区服务。我应该开启时区支持吗?
是的。开启时区支持后,Django 会使用更准确的本地时间模型,这样使得你在夏令时转换时避免一些微秒、不可复制的错误。
开启时区支持后,会遇到一些错误,因为你使用 naive 日期时间,而 Django 期望使用 aware 日期时间。这些错误在测试的时候会显示出来。
另一方面,由于缺乏时区支持导致的bug很难预防、诊断和修复。任何涉及计划任务或日期计算的事务都有可能是潜在的bug。
基于这些原因,在新项目时要启用时区支持,除非有很好的理由,否则应该保留它。
我已经开启了时区支持。我安全了吗?
也许吧。你可以更好地避免夏令时相关错误,但你仍然可以把 naive 日期时间转换为 aware 日期时间,反之亦然。
如果你的应用连接了其他系统——比如,查询一个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,这取决于你的业务需求。
我改如何与存储本地日期时间的数据库进行交互?
在
DATABASES
里将:setting:TIME_ZONE <DATABASE-TIME_ZONE> 选项设置为适合该数据库的时区。当
USE_TZ
为True
时,这对连接到不支持时区以及不受 Django 管理的数据库很有用。
错误调试¶
我的程序因不能比较带有偏移的 naive 日期时间与带有偏移的 aware 日期时间而崩溃——怎么回事?
我们通过比较 naive 时间和 aware 时间来重现一下这个错误:
>>> 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 支持的日期——比如,读取来自表单或模型字段的值。因为你开启了时区支持,所以它是 aware 的。
- 通过代码生成了日期,它是 naive 的(或者你不会读取它)。
一般来说,正确的解决办法是修改代码,用 aware 日期时间代替它。
如果你正在编写可插拔的应用独立于
USE_TZ
的值运行,你会发现django.utils.timezone.now()
很有用。这个函数会在USE_TZ = False
时将当前日期和时间作为 naive 返回,并且在USE_TZ = True
时将当前日期和时间作为 aware 返回。你可以在需要时添加或减少datetime.timedelta
。我发现了很多告警:在时区支持激活后,DateTimeField 收到了 naive 日期时间((YYYY-MM-DD HH:MM:SS)) ——这样不好吗?
当启用时区支持后,数据库层会期望仅从你的代码里收到 aware 日期。这个告警发生在数据库收到 naive 日期时间时。这表明你没有为时区支持而完成代码移植。请参考 migration guide 来获取此过程的提示。
在这期间,为了向后兼容,日期时间被认为处于默认时区内,通常这是你期望的。
now.date()
方法得到的结果 为什么是昨天(或明天)?如果你一直在使用 naive 日期时间,你或许相信可以通过调用
date()
方法来将日期时间转换为日期。你也认为date
和datetime
很像,除了它的准确性较差。在 aware 日期的环境里,这些都不是正确的:
>>> 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,这取决于它所代表的时区。但真实问题更重要。
一个 datetime 代表一个 时间点。它是客观存在的,不依赖任何事物。另一方面,一个 date 是一个 日历概念 。它是一个时间段,其范围取决于 date 所在的时区。如你所见,这两个概念在根本上是不同的,将 datetime 转换为 date 并不是确定性的操作。
这在实践中意味着什么?
通常,你应该避免将
datetime
转换为date
。比如,你可以使用date
模板过滤器来只展示 datetime 的 date 部分。这个过滤器在格式化之前将 datetime 转换为为 当前时间,确保显示正确的结果。如果你确实需要自己进行转换,你必须首先确保 datetime 转换为合适的时区。通常,这将是当前时区:
>>> 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)
收到“你的数据库安装时区定义了吗?”的错误
如果你正在使用 MySQL,查看 时区定义 部分,以获取有关加载时区定义的说明。
用法¶
有一个字符串
"2012-02-21 10:28:45"
并且时区是"Europe/Helsinki"
。我该如何将其转换为 aware 日期时间?这正是 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
的 pytz 扩展。另外,你或许想捕捉pytz.InvalidTimeError
。pytz 的文档包含了很多例子( more examples )。你应该在尝试操作 aware 日期时间之前看一下它。如何在当前时区里获取当地时间?
好吧,首先要问下自己,你真的需要这么做吗?
当与用户进行交互的时候,你只能使用当地时间,并且在模板层提供过滤器和标签,将日期时间转换为你所选择的时区。
而且,Python 知道如何去比较 aware 日期时间,并在必要时考虑 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"
。如何查看所有可用时区?