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

917
我需要解析类似于"2008-09-03T20:56:35.450686Z"RFC 3339字符串为Python的datetime类型。
我在Python标准库中找到了strptime,但它并不是很方便。
有什么更好的方法吗?

11
明确一点:ISO 8601 是主要的标准。 RFC 3339 是自称为 ISO 8601 的一个“配置文件”,并对 ISO 8601 规则进行了一些不明智的覆盖。 - Basil Bourque
29个回答

671

isoparse函数来自python-dateutil

python-dateutil包拥有dateutil.parser.isoparse函数,它可以解析不仅仅是问题中提到的RFC 3339日期时间字符串,还能够解析其它ISO 8601格式的日期和时间字符串,即使这些字符串不符合RFC 3339标准(例如没有UTC偏移量或只表示日期的字符串)。

>>> import dateutil.parser
>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686Z') # RFC 3339 format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
>>> dateutil.parser.isoparse('2008-09-03T20:56:35.450686') # ISO 8601 extended format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.isoparse('20080903T205635.450686') # ISO 8601 basic format
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
>>> dateutil.parser.isoparse('20080903') # ISO 8601 basic format, date only
datetime.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的文档以了解此警告性警告。(请参见this answer)。


107
对于懒惰的人来说,它是通过python-dateutil而不是dateutil安装的,所以:pip install python-dateutil - cod3monk3y
32
请注意,dateutil.parser 故意设计得比较差:它试图猜测日期格式并在歧义的情况下做出不可避免的假设(只能手动定制),因此仅在需要解析未知格式的输入并且可以容忍偶尔的错误时才使用。 - ivan_pozdeev
2
同意。比如传递一个值为9999的“日期”。这将返回与datetime(9999, 当前月份,当前日期)相同。在我看来,这并不是一个有效的日期。 - timbo
2
@ivan_pozdeev,您会推荐哪个包用于非猜测解析? - bgusach
2
@ivan_pozdeev,模块有更新,可读取iso8601日期: https://dateutil.readthedocs.io/en/stable/parser.html#dateutil.parser.isoparse - theEpsilon
显示剩余9条评论

472
自 Python 3.11 开始,标准库的 `datetime.fromisoformat` 方法支持任何有效的 ISO 8601 输入。在早期版本中,它只解析特定的子集,请参阅文档中的注意事项。如果您正在使用 Python 3.10 或更早版本,请参考其他答案中的标准库之外的函数。文档链接: classmethod datetime.fromisoformat(date_string): 返回一个与任何有效的 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()生成的格式。

12
这很奇怪。因为datetime可能包含tzinfo,从而输出时区,但是datetime.fromisoformat()不解析tzinfo?看起来像是一个bug.. - Hendy Irawan
68
在文档中不要错过那个提示,这个程序并不接受所有有效的ISO 8601格式的字符串,只接受由isoformat生成的字符串。它不接受问题中的例子 "2008-09-03T20:56:35.450686Z",因为它有一个尾随的 Z,但它可以接受 "2008-09-03T20:56:35.450686" - Flimm
81
为了正确支持Z,可以使用date_string.replace("Z", "+00:00")修改输入脚本。 - jox
17
请注意,对于秒数,它仅处理0、3或6位小数。如果输入数据有1、2、4、5、7或更多位小数,则解析将失败! - Felk
4
不,datetime.fromisoformat似乎需要另一种格式。我刚测试了这两个版本,虽然使用+00:00可以正常工作,但是当我使用+0000时,会出现“ValueError:Invalid isoformat string”的错误。 - jox
显示剩余4条评论

233

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

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

请查看此处的问题


4
注意 - 如果使用朴素日期时间,则可能根本没有时区信息,Z 也可能与任何东西都不匹配。 - Danny Staple
2
在我的情况下,%f 捕获了微秒而不是 Z,所以 datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%f') 就解决了问题。 - ashim888
2
Py3K是指Python 3000吗?!? - Robino
4
“Python 3000”是指现在称为Python 3的编程语言的旧称。 - Throw Away Account
1
该答案(在其当前编辑的形式中)依赖于将特定的UTC偏移量(即“Z”,表示+00:00)硬编码到格式字符串中。这是一个不好的想法,因为它将无法解析任何具有不同UTC偏移量的日期时间并引发异常。另外,即使您使用此方法解析具有“Z”偏移量的日期时间,您将返回一个“naive” datetime对象而不是带有UTC时区的“timezone-aware”对象,后者更加正确。 - Mark Amery
显示剩余2条评论

194

从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,请使用其他方法或添加自己的逻辑来验证时区偏移量格式。
  • 该函数绝对不支持ISO 8601的所有格式,因为ISO 8601包括比RFC 3339更广泛的格式。 (例如,2009-W01-1是有效的ISO 8601日期。)
  • 它在Python 3.6或更早版本中不起作用,因为在那些旧版本中,%z说明符仅匹配时区偏移量,例如+0500-0430+0000,而不是RFC 3339时区偏移量,如+05:00-04:30Z

85

尝试使用iso8601模块,它恰好能够实现这个功能。

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


1
像这样简单:iso8601.parse_date("2008-09-03T20:56:35.450686Z") - Pakman
3
问题不是“我如何解析ISO 8601日期”,而是“我如何解析这个确切的日期格式。” - Nicholas Riley
3
@tiktak ,原帖问到“我需要解析类似X的字符串”,我的回复是,经过两个库的尝试后,建议使用另一个库,因为iso8601仍存在重要问题。我的参与或不参与这样一个项目与答案完全无关。 - Tobia
6
pyiso8601,即iso8601,最近于2014年2月进行了更新。最新版本支持更广泛的ISO 8601字符串集合。我在一些项目中使用了它并且效果很好。 - Dave Hein
1
很遗憾,pypi上那个名为“iso8601”的库非常不完整。它明确表示无法处理基于周数的日期,这只是其中一个例子。 - boxed
@Tobia:iso8601 似乎又开始更新了。 - Georg Schölly

71

Python >= 3.11

fromisoformat 现在可以直接解析 Z

from datetime import datetime

s = "2008-09-03T20:56:35.450686Z"

datetime.fromisoformat(s)
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=datetime.timezone.utc)

Python 3.7 到 3.10

从评论中的一个简单选项:将'Z'替换为'+00:00' - 并使用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)

为什么更喜欢使用fromisoformat

尽管strptime%z可以解析'Z'字符为UTC时间,但fromisoformat的速度更快,大约快40倍(甚至对于Python 3.11来说,速度可能快60倍):

from datetime import datetime
from dateutil import parser

s = "2008-09-03T20:56:35.450686Z"

# Python 3.11+
%timeit datetime.fromisoformat(s)
85.1 ns ± 0.473 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

# Python 3.7 to 3.10
%timeit datetime.fromisoformat(s.replace('Z', '+00:00'))
134 ns ± 0.522 ns per loop (mean ± std. dev. of 7 runs, 10,000,000 loops each)

%timeit parser.isoparse(s)
4.09 µs ± 5.2 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

%timeit datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')
5 µs ± 9.26 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)

%timeit parser.parse(s)
28.5 µs ± 99.2 ns per loop (mean ± std. dev. of 7 runs, 10,000 loops each)

(Python 3.11.3 x64 在 GNU/Linux 上)
另请参阅:更快的 strptime

2
@mikerodent:重点是fromisoformat可以解析+00:00但不能解析Z,并将其转换为带有UTC时区信息的aware datetime。如果您的输入以Z+00:00结尾,则可以在将其提供给fromisoformat之前删除Z。其他UTC偏移量(例如+05:30)将被解析为静态UTC偏移量(而不是实际的时区)。 - FObersteiner
现在完全不同的观点了(我对“意识”有了更多的理解)。但是我在文档中注意到:“在3.11版本中更改:以前,此方法仅支持可以由date.isoformat()或datetime.isoformat()生成的格式。”和“对应于任何有效的ISO 8601格式的日期字符串”。这实际上可能不是人们想要的。datetime.datefromisoformat更加明确:“返回与以任何有效的ISO 8601格式给出的日期字符串相对应的日期... ”... 它还提供了一些令人惊讶的字符串示例,它们不仅仅是简单的YYYY-MM-DD格式。 - mike rodent
现在完全不同的观点(我对“意识”有了更多的理解)。但我在文档中注意到:“在3.11版本中更改:以前,此方法仅支持可以由date.isoformat()或datetime.isoformat()发出的格式。”和“对应于任何有效的ISO 8601格式的date_string”。这实际上可能不是人们想要的。datetime.datefromisoformat更加明确:“返回与以任何有效的ISO 8601格式给出的date_string相对应的日期...”...它给出了一些令人惊讶的字符串示例,这些字符串不仅仅是简单的YYYY-MM-DD。 - undefined

49

自Python 3.7开始,strptime支持UTC偏移量中的冒号分隔符()。因此,您可以使用以下内容:

import datetime

def parse_date_string(date_string: str) -> datetime.datetime
    try:
       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所指出的那样,如果您使用isoformat()创建了datetime对象,则可以简单地使用datetime.fromisoformat()

编辑2:

正如Mark Amery所指出的,我添加了一个尝试..异常块来处理缺少小数秒的情况。


8
但在Python 3.7中,你还可以使用datetime.fromisoformat()来自动处理类似于输入字符串'2018-01-31T09:24:31.488670+00:00'的日期时间格式。 - Martijn Pieters
2
好的建议。我同意,我建议使用 datetime.fromisoformat()datetime.isoformat() - Andreas Profous
这是唯一符合问题要求的答案。如果你必须使用strptime,那么这就是正确的答案。 - Danielo515
你的示例在Python 3.6上失败,报错为:ValueError: time data '2018-01-31T09:24:31.488670+00:00' does not match format '%Y-%m-%dT%H:%M:%S.%f%z',这是因为 %z 不匹配 +00:00。然而,+0000 匹配 %z,请参考 Python 文档 https://docs.python.org/3.6/library/datetime.html#strftime-and-strptime-behavior。 - Eric
3
Python 3.11进一步改进了fromisoformat()函数,现在它可以处理Z时区标识符了。例如:datetime.fromisoformat('2018-01-31T09:24:31Z')会返回datetime.datetime(2018, 1, 31, 9, 24, 31, tzinfo=datetime.timezone.utc) - Martijn Pieters
显示剩余3条评论

39

您收到的错误信息是什么?是否像以下内容?

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

12
不能仅仅删除.Z,因为它代表时区,并且可能不同。我需要将日期转换为UTC时区。 - Alexander Artemenko
如果时区不是 """Z",那么它必须是以小时/分钟为单位的偏移量,可以直接添加到/从日期时间对象中。你可以创建一个 tzinfo 子类来处理它,但这可能不是推荐的做法。 - SingleNegationElimination
9
此外,"%f" 是微秒指示器,因此一个(不包含时区信息的)strptime字符串看起来是这样的:"%Y-%m-%dT%H:%M:%S.%f"。 - quodlibetor
1
如果给定的日期时间字符串具有除“Z”以外的UTC偏移量,则会引发异常。它不支持整个RFC 3339格式,并且是处理UTC偏移量正确的其他答案的劣等答案。 - Mark Amery
1
Python 3.11拥有大大改进的datetime.fromisoformat,可以处理大多数ISO8601和RFC3339格式。https://docs.python.org/3.11/library/datetime.html#datetime.datetime.fromisoformat - Nelson
显示剩余2条评论

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

90
我不同意,这段文字几乎无法阅读,并且据我所知并未考虑到“祖鲁时间”(Z)的存在,即使提供了时区数据,该日期时间仍然是“朴素”的。 - umbrae
15
我认为它非常易读。实际上,这可能是在不安装其他软件包的情况下进行转换最简单和最有效的方法。 - Tobia
3
我想这相当于d=datetime.datetime(*map(int, re.split('\D', s)[:-1]))。 - Xuan
4
一个变化:datetime.datetime(*map(int, re.findall('\d+', s)) 的翻译:日期时间模块中的一个变体,将字符串中的数字提取出来并转换为整数后作为参数传递给 datetime.datetime() 函数。 - jfs
6
这将导致一个没有时区的朴素日期时间对象,对吗?所以UTC信息在翻译中丢失了? - w00t
显示剩余5条评论

22

现在,Arrow 也可以作为第三方解决方案使用:

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

3
请使用python-dateutil - arrow需要python-dateutil。 - danizen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接