如何解析ISO 8601格式的日期?

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

我在Python标准库中找到了strptime,但不是很方便。

最好的方法是什么?

643899 次浏览

你得到的确切错误是什么?是不是像下面这样?

>>> 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

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

试试这个:

>>> 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)
import reimport datetimes = "2008-09-03T20:56:35.450686Z"d = datetime.datetime(*map(int, re.split(r'[^\d]', s)[:-1]))

尝试iso8601模块;它正是这样做的。

python.orgwiki的使用时间页面上提到了其他几个选项。

请注意,在Python 2.6+和Py3K中,%f字符捕获微秒。

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

问题这里

对于适用于2. X标准库的东西,请尝试:

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

calendar.timegm是缺少的通用版本time.mktime.

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

测试包括在内!: P

isoparse函数来自python-dateutil

python-日期util包有dateutil.parser.isoparse不仅可以解析RFC 3339日期时间字符串(如问题中的那个),还可以解析其他ISO8601不符合RFC 3339的日期和时间字符串(例如没有UTC偏移量的字符串,或仅代表日期的字符串)。

>>> import dateutil.parser>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686Z') # RFC 3339 formatdatetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686') # ISO 8601 extended formatdatetime.datetime(2008, 9, 3, 20, 56, 35, 450686)>>> dateutil.parser.isoparse('20080903T205635.450686') # ISO 8601 basic formatdatetime.datetime(2008, 9, 3, 20, 56, 35, 450686)>>> dateutil.parser.isoparse('20080903') # ISO 8601 basic format, date onlydatetime.datetime(2008, 9, 3, 0, 0)

python-dateutil包也有dateutil.parser.parse。与isoparse相比,它可能不那么严格,但它们都相当宽容,并将尝试解释您传入的字符串。如果您想消除任何误读的可能性,您需要使用比这两个函数中的任何一个更严格的东西。

与Python 3.7+的内置datetime.datetime.fromisoformat的比较

dateutil.parser.isoparse是一个完整的ISO-8601格式解析器,但在Python≤3.10中,fromisoformat故意没有。在Python 3.11中,fromisoformat支持有效的ISO 8601中的几乎所有字符串。有关此警告警告,请参阅fromisoformat的文档。(见这个答案)。

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

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

如果你不想使用dateutil,你可以尝试这个函数:

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 typereturn datetime.datetime.strptime(utcTime, fmt)

测试:

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

结果:

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

在这些日子里,Arrow也可以用作第三方解决方案:

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

这适用于Python 3.2以上的stdlib(假设所有时间戳都是UTC):

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

例如,

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

从Python 3.7开始,您可以基本上(下面的警告)使用datetime.datetime.strptime解析RFC 3339日期时间,如下所示:

from datetime import datetime
def parse_rfc3339(datetime_str: str) -> datetime:try:return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S.%f%z")except ValueError:# Perhaps the datetime has a whole number of seconds with no decimal# point. In that case, this will work:return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S%z")

这有点尴尬,因为我们需要尝试两种不同的格式字符串来支持带小数秒数的日期时间(如2022-01-01T12:12:12.123Z)和不带小数秒数的日期时间(如2022-01-01T12:12:12Z),这两者在RFC 3339下都是有效的。但只要我们做了那一点繁琐的逻辑,这就有效了。

关于这种方法需要注意的一些注意事项:

  • 技术上不完全支持RFC 3339,因为RFC 3339奇怪地允许您使用空格而不是T来分隔日期和时间,尽管RFC 3339声称是ISO 8601的配置文件,而ISO 8601不允许没有。如果你想支持RFC 3339的这个愚蠢的怪癖,你可以在函数的开头添加datetime_str = datetime_str.replace(' ', 'T')
  • 我上面的实现比严格的RFC 3339解析器更宽松,因为它允许像+0500这样没有冒号的时区偏移,而RFC 3339不支持。如果你不仅仅想解析已知的RFC-3339日期时间,还想严格验证你得到的日期时间是RFC 3339,请使用另一种方法或添加你自己的逻辑来验证时区偏移格式。
  • 此函数绝对不支持所有ISO8601,其中包括比RFC 3339更广泛的格式阵列。(例如,2009-W01-1是有效的ISO 8601日期。)
  • 它在Python 3.6或更早版本中确实可以使用没有,因为在那些旧版本中,%z说明符只匹配像+0500-0430+0000这样的时区偏移,而不是像+05:00-04:30Z这样的RFC 3339时区偏移。

如果您正在使用Django,它提供了日期解析模块,它接受一堆类似于ISO格式的格式,包括时区。

如果您不使用Django并且不想使用此处提到的其他库之一,您可能会将dateparse的Django源代码适应您的项目。

感谢伟大的马克·阿梅里的回答,我设计了一个函数来解释所有可能的日期时间ISO格式:

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 minutesself.__name = '<%+03d%02d>%+d' % (hours, minutes, -hours)def utcoffset(self, dt=None):return self.__offsetdef tzname(self, dt=None):return self.__namedef 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:passtry:return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S')except ValueError:passpat = 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 = -offsetreturn naive_dt.replace(tzinfo=FixedOffset(offset))

因为ISO 8601允许存在许多可选冒号和破折号的变体,基本上是CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]。如果你想使用strptime,你需要先去掉这些变体。

目标是生成一个utc datetime对象。


如果您只是想要一个适用于UTC的基本情况,例如Z后缀2016-06-29T19:36:29.3453Z

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这样的变量分隔符的东西,使其更一致/更容易解析。

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


如果您的系统不支持%z strptime指令(您会看到类似ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z'的内容),那么您需要手动从Z(UTC)偏移时间。注意%z可能无法在Python版本<3的系统上工作,因为它取决于c库支持,该库支持因系统/python构建类型(即Jython、Cython等)而异。

import reimport datetime
# this regex removes all colons and all# dashes EXCEPT for the dash indicating + or - utc offset for the timezoneconformed_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 delimitersplit_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 = Noneoffset = None
# generate the datetime object without the offset at UTC timeoutput_datetime = datetime.datetime.strptime(main_timestamp +"Z", "%Y%m%dT%H%M%S.%fZ" )if offset:# create timedelta based on offsetoffset_delta = datetime.timedelta(hours=int(sign+offset[:-2]), minutes=int(sign+offset[-2:]))# offset datetime with timedeltaoutput_datetime = output_datetime + offset_delta
def parseISO8601DateTime(datetimeStr):import timefrom datetime import datetime, timedelta
def log_date_string(when):gmt = time.gmtime(when)if time.daylight and gmt[8]:tz = time.altzoneelse:tz = time.timezoneif tz > 0:neg = 1else:neg = 0tz = -tzh, 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进行解析。

在所有受支持的Python版本中将类似ISO 8601的日期字符串转换为UNIX时间戳或datetime.datetime对象而无需安装第三方模块的一种直接方法是使用SQLite的日期解析器

#!/usr/bin/env pythonfrom __future__ import with_statement, division, print_functionimport sqlite3import 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)

输出:

2016-08-25T16:01:26.123456Z is 1472140886 after epochdatetime is 2016-08-25 12:01:262016-08-25T16:01:29 is 1472140889 after epochdatetime is 2016-08-25 12:01:29

Django的parse_datetime()函数支持带有UTC偏移的日期:

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

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

from django.utils import formatsfrom django.forms.fields import DateTimeFieldfrom 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.strptimeformats.ISO_INPUT_FORMATS['DATETIME_INPUT_FORMATS'].insert(0, 'iso-8601')

我是iso8601utils的作者。它可以在在githubPyPI上找到。以下是您如何解析示例:

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

只需使用python-dateutil模块:

>>> 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())

文档

我发现ciso8601是解析典型ISO 8601时间戳的最快方法,尽管它只支持ISO 8601的一个子集(特别是,它不支持星期日期或序数日期,这两者都不常见,但仍然是ISO 8601标准的一部分)。

确实完全支持RFC 3339,特别是解析RFC 3339日期的专用功能。

示例用法:

>>> import ciso8601>>> ciso8601.parse_datetime('2014-01-09T21')datetime.datetime(2014, 1, 9, 21, 0)>>> ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30')datetime.datetime(2014, 1, 9, 21, 48, 0, 921000, tzinfo=datetime.timezone(datetime.timedelta(seconds=19800)))>>> ciso8601.parse_rfc3339('2014-01-09T21:48:00.921000+05:30')datetime.datetime(2014, 1, 9, 21, 48, 0, 921000, tzinfo=datetime.timezone(datetime.timedelta(seconds=19800)))

GitHub回购自述文件显示了它们与其他答案中列出的所有其他库相比>10倍的加速。

我的个人项目涉及大量的ISO 8601解析。很高兴能够切换调用并加快10倍。:)

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

从Python 3.7开始,strptime支持UTC偏移中的冒号分隔符(来源)。所以你可以使用:

import datetime
def parse_date_string(date_string: str) -> datetime.datetimetry:return datetime.datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S.%f%z')except ValueError:return datetime.datetime.strptime(date_string, '%Y-%m-%dT%H:%M:%S%z')

编辑:

正如Martijn所指出的,如果您使用isoform()创建了datetime对象,则可以简单地使用datetime.fromisoformat()

编辑2:

正如马克·阿梅里所指出的,我添加了一个try…除了块来解释丢失的小数秒。

从Python 3.7开始,datetime标准库有一个用于反转datetime.isoformat()的函数。

类方法#0

以任何有效的ISO 8601格式返回与date_string对应的datetime,但以下情况除外:

  1. 时区偏移可能具有小数秒。
  2. T分隔符可以替换为任何单个Unicode字符。
  3. 当前不支持序号日期。
  4. 不支持小数小时和分钟。

示例:

>>> from datetime import datetime>>> datetime.fromisoformat('2011-11-04')datetime.datetime(2011, 11, 4, 0, 0)>>> datetime.fromisoformat('20111104')datetime.datetime(2011, 11, 4, 0, 0)>>> datetime.fromisoformat('2011-11-04T00:05:23')datetime.datetime(2011, 11, 4, 0, 5, 23)>>> datetime.fromisoformat('2011-11-04T00:05:23Z')datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)>>> datetime.fromisoformat('20111104T000523')datetime.datetime(2011, 11, 4, 0, 5, 23)>>> datetime.fromisoformat('2011-W01-2T00:05:23.283')datetime.datetime(2011, 1, 4, 0, 5, 23, 283000)>>> datetime.fromisoformat('2011-11-04 00:05:23.283')datetime.datetime(2011, 11, 4, 0, 5, 23, 283000)>>> datetime.fromisoformat('2011-11-04 00:05:23.283+00:00')datetime.datetime(2011, 11, 4, 0, 5, 23, 283000, tzinfo=datetime.timezone.utc)>>> datetime.fromisoformat('2011-11-04T00:05:23+04:00')datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone(datetime.timedelta(seconds=14400)))

新版本3.7。

在3.11版更改:以前,此方法仅支持可以由date.isoformat()或datetime.isoformat()发出的格式。

如果您还没有升级到Python 3.11,请务必阅读文档中的警告!

现在有玛雅:人类的日期™,来自流行的请求:HTTP for Humans™包的作者:

>>> 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>)

最初我尝试过:

from operator import neg, posfrom time import strptime, mktimefrom datetime import datetime, tzinfo, timedelta
class MyUTCOffsetTimezone(tzinfo):@staticmethoddef with_offset(offset_no_signal, signal):  # type: (str, str) -> MyUTCOffsetTimezonereturn 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) -> datetimefmt = '%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中,我做得很好:

from datetime import datetime

def to_datetime_tz(dt):  # type: (str) -> datetimefmt = '%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:

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)

另一种方法是为ISO-8601使用专用解析器,即使用dateutil解析器的等宽函数:

from dateutil import parser
date = parser.isoparse("2008-09-03T20:56:35.450686+01:00")print(date)

输出:

2008-09-03 20:56:35.450686+01:00

这个函数也在为标准Python函数留档datetime.fromisoformat中提到:

功能更全面的ISO 8601解析器,dateutil.parser.isoparse在第三方包dateutil中可用。

其中一条评论中的一个简单选项:将'Z'替换为'+00:00'-并使用Python 3.7+的fromisoformat

from datetime import datetime
s = "2008-09-03T20:56:35.450686Z"
datetime.fromisoformat(s.replace('Z', '+00:00'))# datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=datetime.timezone.utc)

虽然strptime可以将'Z'字符解析为UTC,但#2快了~x40(另请参阅:更快的strptime):

%timeit datetime.fromisoformat(s.replace('Z', '+00:00'))388 ns ± 48.3 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit dateutil.parser.isoparse(s)11 µs ± 1.05 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')15.8 µs ± 1.32 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)
%timeit dateutil.parser.parse(s)87.8 µs ± 8.54 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)

(Windows 10上的Python 3.9.12 x64)

如果使用pandas,我可以推荐pandas中的Timestamp。在那里你可以

ts_1 = pd.Timestamp('2020-02-18T04:27:58.000Z')ts_2 = pd.Timestamp('2020-02-18T04:27:58.000')

Rant:令人难以置信的是,我们仍然需要担心2021年的日期字符串解析等问题。

datetime.fromisoformat()在Python 3.11中得到了改进,可以解析大多数ISO 8601格式

datetime.fromisoformat()现在可用于解析大多数ISO 8601格式,仅支持小数小时和分钟的格式除外。以前,此方法仅支持可以由datetime.isoformat()发出的格式。

>>> from datetime import datetime>>> datetime.fromisoformat('2011-11-04T00:05:23Z')datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)>>> datetime.fromisoformat('20111104T000523')datetime.datetime(2011, 11, 4, 0, 5, 23)>>> datetime.fromisoformat('2011-W01-2T00:05:23.283')datetime.datetime(2011, 1, 4, 0, 5, 23, 283000)