关于python:如何解析ISO 8601格式的日期?

How do I parse an ISO 8601-formatted date?

我需要将RFC3339字符串(如"2008-09-03T20:56:35.450686Z"解析为python的datetime类型。

我在python标准库中找到了strptime,但它不太方便。

最好的方法是什么?


python-dateutil包不仅可以分析与问题类似的RFC3339日期时间字符串,还可以分析不符合RFC3339的其他ISO 8601日期和时间字符串(例如那些没有UTC偏移量的字符串,或者那些只表示日期的字符串)。

1
2
3
4
5
6
7
8
9
>>> import dateutil.parser
>>> dateutil.parser.parse('2008-09-03T20:56:35.450686Z') # RFC 3339 format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
>>> dateutil.parser.parse('2008-09-03T20:56:35.450686') # ISO 8601 extended format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.parse('20080903T205635.450686') # ISO 8601 basic format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.parse('20080903') # ISO 8601 basic format, date only
datetime.datetime(2008, 9, 3, 0, 0)

请注意,dateutil.parser是故意黑客的:它试图猜测格式,并在不明确的情况下做出不可避免的假设(只能手工定制)。因此,只有在需要解析未知格式的输入并且可以容忍偶尔的错误读取时才使用它。(谢谢Ivan Pozdeev)

pypi名称是python-dateutil,而不是dateutil(感谢代码3monk3y):

1
pip install python-dateutil

如果您使用的是python 3.7,请看一下这个关于datetime.datetime.fromisoformat的答案。


注意,在python 2.6+和py3k中,%f字符捕捉微秒。

1
>>> datetime.datetime.strptime("2008-09-03T20:56:35.450686Z","%Y-%m-%dT%H:%M:%S.%fZ")

请参阅此处的问题


这里有几个答案建议使用datetime.datetime.strptime来解析带时区的RFC3339或ISO 8601日期时间,如问题中所示:

1
2008-09-03T20:56:35.450686Z

这是个坏主意。

假设您希望支持完整的RFC3339格式,包括对除零之外的UTC偏移量的支持,那么这些答案建议的代码将不起作用。实际上,它不能工作,因为使用strptime分析RFC3339语法是不可能的。python的datetime模块使用的格式字符串无法描述RFC3339语法。

问题是UTC补偿。RFC3339 Internet日期/时间格式要求每个日期时间都包含一个UTC偏移量,这些偏移量可以是Z(zulu time的缩写),也可以是+HH:MM-HH:MM格式,如+05:00-10:30

因此,这些都是有效的RFC3339日期:

  • 江户十一〔七〕号
  • 埃多克斯1〔8〕
  • 埃多克斯1〔9〕

唉,strptimestrftime使用的格式字符串没有对应于RFC3339格式的UTC偏移量的指令。他们支持的指令的完整列表可以在https://docs.python.org/3/library/datetime.html strftime和strptime behavior中找到,列表中包含的唯一UTC偏移指令是%z

%z

UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive).

Example: (empty), +0000, -0400, +1030

这与RFC3339偏移量的格式不匹配,实际上,如果我们尝试在格式字符串中使用%z并分析RFC3339日期,我们将失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
>>> from datetime import datetime
>>> datetime.strptime("2008-09-03T20:56:35.450686Z","%Y-%m-%dT%H:%M:%S.%f%z")
Traceback (most recent call last):
  File"", line 1, in
  File"/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File"/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686Z' does not match format '%Y-%m-%dT%H:%M:%S.%f%z'
>>> datetime.strptime("2008-09-03T20:56:35.450686+05:00","%Y-%m-%dT%H:%M:%S.%f%z")
Traceback (most recent call last):
  File"", line 1, in
  File"/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File"/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%f%z'

(实际上,上面就是您将在Python3中看到的内容。在python 2中,我们失败的原因更为简单,即strptime根本没有在python2中实现%z指令。)

这里建议strptime的多个答案都是通过在其格式字符串中包含一个文本Z,它与提问者示例日期时间字符串中的Z相匹配(并丢弃它,生成一个没有时区的datetime对象)来解决这一问题的:

1
2
>>> datetime.strptime("2008-09-03T20:56:35.450686Z","%Y-%m-%dT%H:%M:%S.%fZ")
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)

因为这会丢弃原始日期时间字符串中包含的时区信息,所以我们是否应该将此结果视为正确的结果是值得怀疑的。但更重要的是,由于这种方法涉及到将特定的UTC偏移量硬编码为格式字符串,因此当它尝试用不同的UTC偏移量分析任何RFC3339日期时间时,它将阻塞:

1
2
3
4
5
6
7
8
>>> datetime.strptime("2008-09-03T20:56:35.450686+05:00","%Y-%m-%dT%H:%M:%S.%fZ")
Traceback (most recent call last):
  File"", line 1, in
  File"/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime
    tt, fraction = _strptime(data_string, format)
  File"/usr/lib/python3.4/_strptime.py", line 337, in _strptime
    (data_string, format))
ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%fZ'

除非您确定只需要在祖鲁时间内支持RFC3339日期时间,而不需要其他时区偏移,否则不要使用strptime。使用这里答案中描述的许多其他方法之一。


python 3.7中的新功能+

datetime标准库引入了一个用于反转datetime.isoformat()的函数。

classmethod datetime.fromisoformat(date_string):

Return a datetime corresponding to a date_string in one of the formats
emitted by date.isoformat() and datetime.isoformat().

Specifically, this function supports strings in the format(s):

YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]][+HH:MM[:SS[.ffffff]]]]

where * can match any single character.

Caution: This does not support parsing arbitrary ISO 8601 strings - it is only intended as the inverse
operation of datetime.isoformat().

使用示例:

1
2
3
from datetime import datetime

date = datetime.fromisoformat('2017-01-01T12:30:59.000000')


尝试iso8601模块;它可以做到这一点。

在python.org wiki的WorkingWithTime页面上还提到了其他几个选项。


1
2
3
import re,datetime
s="2008-09-03T20:56:35.450686Z"
d=datetime.datetime(*map(int, re.split('[^\d]', s)[:-1]))


你到底犯了什么错误?是这样吗?

1
2
>>> datetime.datetime.strptime("2008-08-12T12:20:30.656234Z","%Y-%m-%dT%H:%M:%S.Z")
ValueError: time data did not match format:  data=2008-08-12T12:20:30.656234Z  fmt=%Y-%m-%dT%H:%M:%S.Z

如果是,可以将输入字符串拆分为".",然后将微秒添加到所得到的日期时间中。

试试这个:

1
2
3
4
5
6
7
8
>>> def gt(dt_str):
        dt, _, us= dt_str.partition(".")
        dt= datetime.datetime.strptime(dt,"%Y-%m-%dT%H:%M:%S")
        us= int(us.rstrip("Z"), 10)
        return dt + datetime.timedelta(microseconds=us)

>>> gt("2008-08-12T12:20:30.656234Z")
datetime.datetime(2008, 8, 12, 12, 20, 30, 656234)


从python 3.7开始,strptime支持以UTC偏移量(source)表示的冒号分隔符。因此,您可以使用:

1
2
import datetime
datetime.datetime.strptime('2018-01-31T09:24:31.488670+00:00', '%Y-%m-%dT%H:%M:%S.%f%z')


现在,arrow还可以用作第三方解决方案:

1
2
3
4
>>> import arrow
>>> date = arrow.get("2008-09-03T20:56:35.450686Z")
>>> date.datetime
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())


只需使用python-dateutil模块:

1
2
3
4
5
>>> import dateutil.parser as dp
>>> t = '1984-06-02T19:05:00.000Z'
>>> parsed_t = dp.parse(t)
>>> print(parsed_t)
datetime.datetime(1984, 6, 2, 19, 5, tzinfo=tzutc())

文档


如果不想使用dateutil,可以尝试此函数:

1
2
3
4
5
6
def from_utc(utcTime,fmt="%Y-%m-%dT%H:%M:%S.%fZ"):
   """
    Convert UTC time string to time.struct_time
   """

    # change datetime.datetime to time, return time.struct_time type
    return datetime.datetime.strptime(utcTime, fmt)

测试:

1
from_utc("2007-03-04T21:08:12.123Z")

结果:

1
datetime.datetime(2007, 3, 4, 21, 8, 12, 123000)


如果您使用的是Django,那么它提供了日期分析模块,该模块接受一系列类似于ISO格式的格式,包括时区。

如果您不使用django,并且不想使用这里提到的其他库之一,那么您可能可以将django源代码用于dateparse以适应您的项目。


我发现ciso8601是解析iso 8601时间戳的最快方法。顾名思义,它是在C中实现的。

1
2
import ciso8601
ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30')

Github repo自述文件显示它们的速度比其他答案中列出的所有其他库都快10倍以上。

我的个人项目涉及很多ISO 8601解析。能把电话换成10倍的速度真是太好了。:)

编辑:我已经成为CISO8601的维护者。现在比以往任何时候都快!


我是ISO8601实用程序的作者。它可以在Github或Pypi上找到。下面是您如何解析示例:

1
2
3
>>> from iso8601utils import parsers
>>> parsers.datetime('2008-09-03T20:56:35.450686Z')
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)


在所有受支持的python版本中,将类似iso 8601的日期字符串转换为unix时间戳或datetime.datetime对象而不安装第三方模块的一种简单方法是使用sqlite的日期分析器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/env python
from __future__ import with_statement, division, print_function
import sqlite3
import datetime

testtimes = [
   "2016-08-25T16:01:26.123456Z",
   "2016-08-25T16:01:29",
]
db = sqlite3.connect(":memory:")
c = db.cursor()
for timestring in testtimes:
    c.execute("SELECT strftime('%s', ?)", (timestring,))
    converted = c.fetchone()[0]
    print("%s is %s after epoch" % (timestring, converted))
    dt = datetime.datetime.fromtimestamp(int(converted))
    print("datetime is %s" % dt)

输出:

1
2
3
4
2016-08-25T16:01:26.123456Z is 1472140886 after epoch
datetime is 2016-08-25 12:01:26
2016-08-25T16:01:29 is 1472140889 after epoch
datetime is 2016-08-25 12:01:29


我已经为iso8601标准编写了一个解析器,并将其放到了github上:https://github.com/boxed/iso8601。此实现支持规范中的所有内容,除了持续时间、间隔、周期间隔和不在Python日期时间模块支持的日期范围内的日期。

包括测试!P


这适用于python 3.2之后的stdlib(假设所有时间戳都是UTC):

1
2
3
from datetime import datetime, timezone, timedelta
datetime.strptime(timestamp,"%Y-%m-%dT%H:%M:%S.%fZ").replace(
    tzinfo=timezone(timedelta(0)))

例如,

1
2
>>> datetime.utcnow().replace(tzinfo=timezone(timedelta(0)))
... datetime.datetime(2015, 3, 11, 6, 2, 47, 879129, tzinfo=datetime.timezone.utc)


django的parse_datetime()函数支持带UTC偏移量的日期:

1
2
parse_datetime('2016-08-09T15:12:03.65478Z') =
datetime.datetime(2016, 8, 9, 15, 12, 3, 654780, tzinfo=<UTC>)

因此,它可以用于分析整个项目中字段中的ISO 8601日期:

1
2
3
4
5
6
7
8
9
10
11
12
from django.utils import formats
from django.forms.fields import DateTimeField
from django.utils.dateparse import parse_datetime

class DateTimeFieldFixed(DateTimeField):
    def strptime(self, value, format):
        if format == 'iso-8601':
            return parse_datetime(value)
        return super().strptime(value, format)

DateTimeField.strptime = DateTimeFieldFixed.strptime
formats.ISO_INPUT_FORMATS['DATETIME_INPUT_FORMATS'].insert(0, 'iso-8601')


因为iso 8601允许有许多可选冒号和破折号的变化,基本上是CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]。如果你想使用strptime,你需要先去掉那些变化。目标是生成一个UTC日期时间对象。如果您只需要一个适用于UTC的基本情况,并使用z后缀,如2016-06-29T19:36:29.3453Z

1
datetime.datetime.strptime(timestamp.translate(None, ':-'),"%Y%m%dT%H%M%S.%fZ")

如果要处理时区偏移,如2016-06-29T19:36:29.3453-04002008-09-03T20:56:35.450686+05:00,请使用以下选项。这些将把所有的变化转换成没有变量定界符的东西,比如20080903T205635.450686+0500,使其更一致/更容易解析。

1
2
3
4
5
import re
# this regex removes all colons and all
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)
datetime.datetime.strptime(conformed_timestamp,"%Y%m%dT%H%M%S.%f%z" )

如果您的系统不支持%zstrptime指令(您看到类似ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z'的指令),则需要手动从Z抵消时间。注:在python版本<3的情况下,%z可能无法在您的系统上工作,因为它依赖于C库支持,而C库支持在系统/python构建类型(即jython、cython等)中有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import re
import datetime

# this regex removes all colons and all
# dashes EXCEPT for the dash indicating + or - utc offset for the timezone
conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp)

# split on the offset to remove it. use a capture group to keep the delimiter
split_timestamp = re.split(r"[+|-]",conformed_timestamp)
main_timestamp = split_timestamp[0]
if len(split_timestamp) == 3:
    sign = split_timestamp[1]
    offset = split_timestamp[2]
else:
    sign = None
    offset = None

# generate the datetime object without the offset at UTC time
output_datetime = datetime.datetime.strptime(main_timestamp +"Z","%Y%m%dT%H%M%S.%fZ" )
if offset:
    # create timedelta based on offset
    offset_delta = datetime.timedelta(hours=int(sign+offset[:-2]), minutes=int(sign+offset[-2:]))
    # offset datetime with timedelta
    output_datetime = output_datetime + offset_delta


如果分析无效的日期字符串,python-dateutil将引发异常,因此您可能希望捕获该异常。

1
2
3
4
5
6
from dateutil import parser
ds = '2012-60-31'
try:
  dt = parser.parse(ds)
except ValueError, e:
  print '"%s" is an invalid date' % ds

如果要使用2.x标准库,请尝试:

1
calendar.timegm(time.strptime(date.split(".")[0]+"UTC","%Y-%m-%dT%H:%M:%S%Z"))

calendar.time gm是缺少的gm版本time.mktime。


现在有玛雅:人类的约会时间?,来自《流行请求:人类HTTP》的作者?包裹:

1
2
3
4
>>> import maya
>>> str = '2008-09-03T20:56:35.450686Z'
>>> maya.MayaDT.from_rfc3339(str).datetime()
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=<UTC>)

由于马克·阿米里的回答很棒,我设计了一个函数来解释日期时间的所有可能的ISO格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class FixedOffset(tzinfo):
   """Fixed offset in minutes: `time = utc_time + utc_offset`."""
    def __init__(self, offset):
        self.__offset = timedelta(minutes=offset)
        hours, minutes = divmod(offset, 60)
        #NOTE: the last part is to remind about deprecated POSIX GMT+h timezones
        #  that have the opposite sign in the name;
        #  the corresponding numeric value is not used e.g., no minutes
        self.__name = '<%+03d%02d>%+d' % (hours, minutes, -hours)
    def utcoffset(self, dt=None):
        return self.__offset
    def tzname(self, dt=None):
        return self.__name
    def dst(self, dt=None):
        return timedelta(0)
    def __repr__(self):
        return 'FixedOffset(%d)' % (self.utcoffset().total_seconds() / 60)
    def __getinitargs__(self):
        return (self.__offset.total_seconds()/60,)

def parse_isoformat_datetime(isodatetime):
    try:
        return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S.%f')
    except ValueError:
        pass
    try:
        return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S')
    except ValueError:
        pass
    pat = r'(.*?[+-]\d{2}):(\d{2})'
    temp = re.sub(pat, r'\1\2', isodatetime)
    naive_date_str = temp[:-5]
    offset_str = temp[-5:]
    naive_dt = datetime.strptime(naive_date_str, '%Y-%m-%dT%H:%M:%S.%f')
    offset = int(offset_str[-4:-2])*60 + int(offset_str[-2:])
    if offset_str[0] =="-":
        offset = -offset
    return naive_dt.replace(tzinfo=FixedOffset(offset))

最初我尝试了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from operator import neg, pos
from time import strptime, mktime
from datetime import datetime, tzinfo, timedelta

class MyUTCOffsetTimezone(tzinfo):
    @staticmethod
    def with_offset(offset_no_signal, signal):  # type: (str, str) -> MyUTCOffsetTimezone
        return MyUTCOffsetTimezone((pos if signal == '+' else neg)(
            (datetime.strptime(offset_no_signal, '%H:%M') - datetime(1900, 1, 1))
          .total_seconds()))

    def __init__(self, offset, name=None):
        self.offset = timedelta(seconds=offset)
        self.name = name or self.__class__.__name__

    def utcoffset(self, dt):
        return self.offset

    def tzname(self, dt):
        return self.name

    def dst(self, dt):
        return timedelta(0)


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        dt, sign, offset = strptime(dt[:-6], fmt), dt[-6], dt[-5:]
        return datetime.fromtimestamp(mktime(dt),
                                      tz=MyUTCOffsetTimezone.with_offset(offset, sign))
    elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

但这对负时区不起作用。不过,在python 3.7.3中,我的工作做得很好:

1
2
3
4
5
6
7
8
9
10
from datetime import datetime


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        return datetime.strptime(dt, fmt + '%z')
    elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

一些测试中,注意到输出的精度只有微秒的不同。我的机器有6位数的精度,但YMMV:

1
2
3
4
5
6
7
for dt_in, dt_out in (
        ('2019-03-11T08:00:00.000Z', '2019-03-11T08:00:00'),
        ('2019-03-11T08:00:00.000+11:00', '2019-03-11T08:00:00+11:00'),
        ('2019-03-11T08:00:00.000-11:00', '2019-03-11T08:00:00-11:00')
    ):
    isoformat = to_datetime_tz(dt_in).isoformat()
    assert isoformat == dt_out, '{} != {}'.format(isoformat, dt_out)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def parseISO8601DateTime(datetimeStr):
    import time
    from datetime import datetime, timedelta

    def log_date_string(when):
        gmt = time.gmtime(when)
        if time.daylight and gmt[8]:
            tz = time.altzone
        else:
            tz = time.timezone
        if tz > 0:
            neg = 1
        else:
            neg = 0
            tz = -tz
        h, rem = divmod(tz, 3600)
        m, rem = divmod(rem, 60)
        if neg:
            offset = '-%02d%02d' % (h, m)
        else:
            offset = '+%02d%02d' % (h, m)

        return time.strftime('%d/%b/%Y:%H:%M:%S ', gmt) + offset

    dt = datetime.strptime(datetimeStr, '%Y-%m-%dT%H:%M:%S.%fZ')
    timestamp = dt.timestamp()
    return dt + timedelta(hours=dt.hour-time.gmtime(timestamp).tm_hour)

注意,如果字符串不是以Z结尾,我们可以使用%z进行解析。