python 日期时间处理
当试图使事情与datetime模块一起工作时,大多数Python用户在我们诉诸猜测直到错误消失之前都面临着一个问题。 datetime是似乎易于使用的API之一,但要求开发人员对一些实际含义有深刻的了解。 否则,考虑到与日期和时间有关的问题的复杂性,引入意外错误很容易。
时间标准
在处理时间时要掌握的第一个概念是定义如何测量时间单位的标准。 我们拥有定义公斤或米的重量或长度标准的方法相同,我们也需要一种精确的方法来定义“ 秒”的含义。 然后,我们可以使用日历标准为秒的倍数来使用其他时间参考(例如天,周或年)(请参阅公历 )。
UT1
考虑到我们可以可靠地保证太阳每天都会升起并落下(在大多数地方),最简单的测量秒数的方法之一就是一天的一小部分。 这催生了格林尼治标准时间(GMT)的后继世界时( UT1 )。 今天,我们使用恒星和类星体来测量地球绕太阳旋转一周所需的时间。 即使这看起来足够精确,它仍然存在问题。 由于月亮的引力,潮汐和地震的影响,这些日子一年四季都在改变。 尽管对于大多数应用程序来说这不是问题,但当我们需要真正精确的测量时,这将成为一个不小的问题。 GPS三角剖分是时间敏感过程的一个很好的例子,其中秒差会导致地球上的位置完全不同。
泰
结果,国际原子时间( TAI )被设计为尽可能精确。 在地球上多个实验室中使用原子钟 ,我们可以得到最精确,最恒定的秒数,这使我们能够以最高准确度计算时间间隔。 这种精确度既是福也是祸,因为TAI如此精确以至于它偏离UT1(或所谓的民用时间) 。 这意味着我们最终将使我们的时钟正午与太阳正午大大偏离。
世界标准时间
这促成了协调世界时( UTC )的发展,该技术汇集了两个部门中的佼佼者。 UTC使用TAI定义的秒的度量。 这样可以在引入leap秒的同时准确地测量时间,以确保时间与UT1的偏差不超过0.9秒。
所有这些如何在您的计算机上一起发挥作用
在所有这些背景下,您现在应该能够了解操作系统在任何给定时刻的服务时间。 虽然计算机内部没有原子钟,但使用的是通过网络时间协议( NTP )与世界其他地方同步的内部时钟。
在类似Unix的系统中,最常用的时间测量方法是使用POSIX time ,它定义为经过Unix纪元(1970年1月1日,星期四)的秒数,而没有考虑leap秒。 由于POSIX时间不能处理leap秒(Python也不可以),因此一些公司已经定义了自己的处理时间的方式,方法是通过NTP服务器在整个时间中涂抹the秒(请参阅Google时间示例)。
时区
我已经解释了什么是UTC,以及它如何允许我们定义日期和时间,但是一些国家/地区喜欢将其中午时间与中午的太阳时间相匹配,因此太阳在下午12点位于天空的最高处。 这就是UTC定义偏移量的原因,因此我们可以有12 AM,与UTC的偏移量为+4小时。 这实际上意味着没有偏移的实际时间是8 AM。
各国政府定义了地理位置所遵循的与UTC的标准偏移量,从而有效地创建了时区。 时区最常见的数据库称为Olson数据库 。 可以使用dateutil.tz在Python中进行检索 :
>>>
from dateutil.
tz
import gettz
>>> gettz
(
"Europe/Madrid"
)
gettz的结果为我们提供了一个对象,可用于在Python中创建可识别时区的日期:
>>>
import
datetime
as dt
>>> dt.
datetime .
now
(
) .
isoformat
(
)
'2017-04-15T14:16:56.551778'
# This is a naive datetime
>>> dt.
datetime .
now
( gettz
(
"Europe/Madrid"
)
) .
isoformat
(
)
'2017-04-15T14:17:01.256587+02:00'
# This is a tz aware datetime, always prefer these
我们可以看到如何通过datetime的now函数获取当前时间。 在第二个调用中,我们传递了一个tzinfo对象,该对象设置时区并以该日期时间的ISO字符串表示形式显示偏移量。
如果我们只想在Python 3中使用普通UTC,则不需要任何外部库:
>>> dt.
datetime .
now
( dt.
timezone .
utc
) .
isoformat
(
)
'2017-04-15T12:22:06.637355+00:00'
夏令时
一旦掌握了所有这些知识,我们可能会准备好使用时区,但是我们必须意识到在某些时区还会发生的另一件事:夏令时(DST)。
遵循DST的国家/地区在Spring将时钟向前移动一小时,在秋季将时钟向后移动一小时,以返回时区的标准时间。 这实际上意味着单个时区可以具有多个偏移量,如下面的示例所示:
>>> dt.
datetime
(
2017
,
7
,
1
, tzinfo
= dt.
timezone .
utc
) .
astimezone
( gettz
(
"Europe/Madrid"
)
)
'2017-07-01T02:00:00+02:00'
>>> dt.
datetime
(
2017
,
1
,
1
, tzinfo
= dt.
timezone .
utc
) .
astimezone
( gettz
(
"Europe/Madrid"
)
)
'2017-01-01T01:00:00+01:00'
这给了我们23或25个小时的工作日,从而产生了非常有趣的时间算法。 根据时间和时区,增加一天并不一定意味着增加24小时:
>>> today
= dt.
datetime
(
2017
,
10
,
29
, tzinfo
= gettz
(
"Europe/Madrid"
)
)
>>> tomorrow
= today + dt.
timedelta
( days
=
1
)
>>> tomorrow.
astimezone
( dt.
timezone .
utc
) - today.
astimezone
( dt.
timezone .
utc
)
datetime .
timedelta
(
1
,
3600
)
# We've added 25 hours
使用时间戳时,最好的策略是使用不支持DST的时区(最好是UTC + 00:00)。
序列化日期时间对象
您将需要使用JSON发送datetime对象的日子到了,您将获得以下内容:
>>> now
= dt.
datetime .
now
( dt.
timezone .
utc
)
>>> json.
dumps
( now
)
TypeError : Object of
type
'datetime'
is
not JSON serializable
在JSON中序列化日期时间的主要方法有以下三种:
串
datetime有两个主要功能,可以在给定特定格式的字符串之间来回转换: strftime和strptime 。 最好的方法是使用标准ISO_8601将与时间相关的对象序列化为字符串,方法是对datetime对象调用isoformat :
>>> now
= dt.
datetime .
now
( gettz
(
"Europe/London"
)
)
>>> now.
isoformat
(
)
'2017-04-19T22:47:36.585205+01:00'
要获得从使用isoformat与UTC时区格式化字符串DateTime对象,我们可以依靠strptime:
>>> dt.
datetime .
strptime
( now_str
,
"%Y-%m-%dT%H:%M:%S.%f+00:00"
) .
replace
( tzinfo
= dt.
timezone .
utc
)
datetime .
datetime
(
2017
,
4
,
19
,
21
,
49
,
5
,
542320
, tzinfo
=
datetime .
timezone .
utc
)
在此示例中,我们将偏移量硬编码为UTC,然后在创建日期时间对象后对其进行设置。 完全解析包括偏移量的字符串的更好方法是使用外部库dateutil:
>>>
from dateutil.
parser
import parse
>>> parse
(
'2017-04-19T21:49:05.542320+00:00'
)
datetime .
datetime
(
2017
,
4
,
19
,
21
,
49
,
5
,
542320
, tzinfo
= tzutc
(
)
)
>>> parse
(
'2017-04-19T21:49:05.542320+01:00'
)
datetime .
datetime
(
2017
,
4
,
19
,
21
,
49
,
5
,
542320
, tzinfo
= tzoffset
(
None
,
3600
)
)
注意,一旦我们进行了序列化和反序列化,我们将丢失时区信息,仅保留偏移量。
整数
通过使用自特定时期(参考日期)以来经过的秒数,我们能够将日期时间存储为整数。 正如我前面提到的,计算机系统中最著名的纪元是Unix纪元,它是1970年以来的第一秒。这意味着5代表1970年1月1日的第五个纪元。
Python标准库为我们提供了一些工具,可将当前时间作为Unix时间获取,以及在日期时间对象及其int表示形式之间进行转换(作为Unix时间)。
获取当前时间为整数:
>>>
import
datetime
as dt
>>>
from dateutil.
tz
import gettz
>>>
import
time
>>> unix_time
=
time .
time
(
)
Unix时间到日期时间:
>>> unix_time
1492636231.597816
>>>
datetime
= dt.
datetime .
fromtimestamp
( unix_time
, gettz
(
"Europe/London"
)
)
>>>
datetime .
isoformat
(
)
'2017-04-19T22:10:31.597816+01:00'
获取给定日期时间的Unix时间:
>>>
time .
mktime
(
datetime .
timetuple
(
)
)
1492636231.0
>>>
# or using the calendar library
>>>
calendar .
timegm
(
datetime .
timetuple
(
)
)
对象
最后一个选项是将对象本身序列化为一个在解码时会具有特殊含义的对象:
import
datetime
as dt
from dateutil.
tz
import gettz
, tzoffset
def json_to_dt
( obj
) :
if obj.
pop
(
'__type__'
,
None
)
!=
"datetime" :
return obj
zone
, offset
= obj.
pop
(
"tz"
)
obj
[
"tzinfo"
]
= tzoffset
( zone
, offset
)
return dt.
datetime
( **obj
)
def dt_to_json
( obj
) :
if
isinstance
( obj
, dt.
datetime
) :
return
{
"__type__" :
"datetime"
,
"year" : obj.
year
,
"month" : obj.
month
,
"day" : obj.
day
,
"hour" : obj.
hour
,
"minute" : obj.
minute
,
"second" : obj.
second
,
"microsecond" : obj.
microsecond
,
"tz" :
( obj.
tzinfo .
tzname
( obj
)
, obj.
utcoffset
(
) .
total_seconds
(
)
)
}
else :
raise
TypeError
(
"Cant serialize {}" .
format
( obj
)
)
现在我们可以编码JSON:
>>>
import json
>>> now
= dt.
datetime .
now
( dt.
timezone .
utc
)
>>> json.
dumps
( now
, default
= dt_to_json
)
# From datetime
'{"__type__": "datetime", "year": 2017, "month": 4, "day": 19, "hour": 22, "minute": 32, "second": 44, "microsecond": 778735, "tz": "UTC"}'
>>>
# Also works with timezones
>>> now
= dt.
datetime .
now
( gettz
(
"Europe/London"
)
)
>>> json.
dumps
( now
, default
= dt_to_json
)
'{"__type__": "datetime", "year": 2017, "month": 4, "day": 19, "hour": 23, "minute": 33, "second": 46, "microsecond": 681533, "tz": "BST"}'
并解码:
>>> input_json
=
'{"__type__": "datetime", "year": 2017, "month": 4, "day": 19, "hour": 23, "minute": 33, "second": 46, "microsecond": 681533, "tz": "BST"}'
>>> json.
loads
( input_json
, object_hook
= json_to_dt
)
datetime .
datetime
(
2017
,
4
,
19
,
23
,
33
,
46
,
681533
, tzinfo
= tzlocal
(
)
)
>>> input_json
=
'{"__type__": "datetime", "year": 2017, "month": 4, "day": 19, "hour": 23, "minute": 33, "second": 46, "microsecond": 681533, "tz": "EST"}'
>>> json.
loads
( input_json
, object_hook
= json_to_dt
)
datetime .
datetime
(
2017
,
4
,
19
,
23
,
33
,
46
,
681533
, tzinfo
= tzfile
(
'/usr/share/zoneinfo/EST'
)
)
>>> json.
loads
( input_json
, object_hook
= json_to_dt
) .
isoformat
(
)
'2017-04-19T23:33:46.681533-05:00'
沃尔时报
此后,您可能会想将所有日期时间对象转换为UTC并仅使用UTC日期时间和固定偏移量。 即使这是到目前为止最好的时间戳记方法,它也会很快中断以适应将来的时间。
我们可以区分两种主要类型的时间点:挂墙时间和时间戳。 时间戳是通用的时间点,与任何地方都没有关系。 例如,星星出生的时间或行记录到文件的时间。 当我们谈论“我们在墙上的钟表上阅读”的时间时,情况发生了变化。 当我们说“明天2点见”时,我们指的不是UTC偏移,而是指当地时间段的明天下午2点,无论此时的偏移量是多少。 我们不能仅将这些间隔时间映射到时间戳(尽管我们可以映射过去的时间戳),因为对于将来发生的事情,国家可能会更改其偏移量,这种偏移的发生频率比您想象的要频繁。
在这种情况下,我们需要将datetime与引用的时区一起保存,而不要与偏移量保存在一起。
使用pytz时的差异
从Python 3.6开始,推荐用于获取Olson数据库的库为dateutil.tz ,但它以前是pytz 。
它们看起来很相似,但是在某些情况下,它们处理时区的方法完全不同。 获取当前时间也很简单:
>>>
import pytz
>>> dt.
datetime .
now
( pytz.
timezone
(
"Europe/London"
)
)
datetime .
datetime
(
2017
,
4
,
20
,
0
,
13
,
26
,
469264
, tzinfo
=< DstTzInfo
'Europe/London' BST+
1 :
00 :
00 DST
>
)
pytz的常见陷阱,它传递pytz时区作为datetime的tzinfo属性:
>>> dt.
datetime
(
2017
,
5
,
1
, tzinfo
= pytz.
timezone
(
"Europe/Helsinki"
)
)
datetime .
datetime
(
2017
,
5
,
1
,
0
,
0
, tzinfo
=< DstTzInfo
'Europe/Helsinki' LMT+
1 :
40 :
00 STD
>
)
>>> pytz.
timezone
(
"Europe/Helsinki"
) .
localize
( dt.
datetime
(
2017
,
5
,
1
)
, is_dst
=
None
)
datetime .
datetime
(
2017
,
5
,
1
,
0
, tzinfo
=< DstTzInfo
'Europe/Helsinki' EEST+
3 :
00 :
00 DST
>
)
我们总是应该在构建的日期时间对象上调用本地化 。 否则, pytz将为时区分配找到的第一个偏移量。
执行时间算术时,可以发现另一个主要区别。 尽管我们看到这些添加操作在dateutil中的工作就像在指定时区中添加壁钟一样,但是当datetime具有pytz tzinfo实例时,将添加绝对小时数,并且调用者必须在操作后调用normalize ,因为它不会处理DST更改。 例如:
>>> today
= dt.
datetime
(
2017
,
10
,
29
)
>>> tz
= pytz.
timezone
(
"Europe/Madrid"
)
>>> today
= tz.
localize
( dt.
datetime
(
2017
,
10
,
29
)
, is_dst
=
None
)
>>> tomorrow
= today + dt.
timedelta
( days
=
1
)
>>> tomorrow
datetime .
datetime
(
2017
,
10
,
30
,
0
,
0
, tzinfo
=< DstTzInfo
'Europe/Madrid' CEST+
2 :
00 :
00 DST
>
)
>>> tz.
normalize
( tomorrow
)
datetime .
datetime
(
2017
,
10
,
29
,
23
,
0
, tzinfo
=< DstTzInfo
'Europe/Madrid' CET+
1 :
00 :
00 STD
>
)
请注意,使用pytz tzinfo ,它增加了24个绝对小时(在墙上时间为23小时)。
下表恢复了使用pytz和dateutil获得wall / timestamps算术的方法 :
pytz | dateutil | |
wall time | obj.tzinfo.localize(obj.replace(tzinfo = None)+ timedelta,is_dst = is_dst) | obj + timedelta |
absolute time | obj.tzinfo.normalize(obj + timedelta) | (obj.astimezone(pytz.utc)+ timedelta).astimezone(obj.tzinfo) |
请注意,发生夏令时更改时,增加挂墙时间可能会导致意外结果。
最后, dateutil可与PEP0495中添加的fold属性很好地配合,并且如果您使用的是早期版本的Python, 则可提供向后兼容性。
快速提示
毕竟,在处理时间问题时,我们应该如何避免常见问题?
- 始终使用时区。 不要依赖隐式的本地时区。
- 使用dateutil / pytz处理时区。
- 使用时间戳记时,请始终使用UTC。
- 请记住,在某些时区,一天并非总是24小时。
- 使您的时区数据库保持最新。
- 始终针对DST更改等情况测试您的代码。
值得一提的图书馆
- dateutil :多个实用程序与时间配合使用
- Frozengun :更轻松地测试与时间相关的应用程序
- 箭头 / 摆锤 :直接替换标准日期时间模块
- astropy :对于天文时间和leap秒工作很有用
Mario Corchero将在PyCon 2017上发表演讲,演讲地点是约会 时间 ,地点是俄勒冈州波特兰。
翻译自: https://opensource.com/article/17/5/understanding-datetime-python-primer
python 日期时间处理