How do I parse an ISO 8601-formatted date?
我需要将RFC3339字符串(如
我在python标准库中找到了
最好的方法是什么?
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) |
。
请注意,
pypi名称是
1 | pip install python-dateutil |
。
如果您使用的是python 3.7,请看一下这个关于
注意,在python 2.6+和py3k中,%f字符捕捉微秒。
1 | >>> datetime.datetime.strptime("2008-09-03T20:56:35.450686Z","%Y-%m-%dT%H:%M:%S.%fZ") |
。
请参阅此处的问题
这里有几个答案建议使用
1 | 2008-09-03T20:56:35.450686Z |
这是个坏主意。
假设您希望支持完整的RFC3339格式,包括对除零之外的UTC偏移量的支持,那么这些答案建议的代码将不起作用。实际上,它不能工作,因为使用
问题是UTC补偿。RFC3339 Internet日期/时间格式要求每个日期时间都包含一个UTC偏移量,这些偏移量可以是
因此,这些都是有效的RFC3339日期:
- 江户十一〔七〕号
- 埃多克斯1〔8〕
- 埃多克斯1〔9〕
唉,
%z
UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive).
Example: (empty), +0000, -0400, +1030
号
这与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中,我们失败的原因更为简单,即
这里建议
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日期时间,而不需要其他时区偏移,否则不要使用
python 3.7中的新功能+
classmethod datetime.fromisoformat(date_string) :Return a
datetime corresponding to adate_string in one of the formats
emitted bydate.isoformat() anddatetime.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 ofdatetime.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()) |
号
只需使用
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时间戳或
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允许有许多可选冒号和破折号的变化,基本上是
1 | datetime.datetime.strptime(timestamp.translate(None, ':-'),"%Y%m%dT%H%M%S.%fZ") |
。
如果要处理时区偏移,如
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" ) |
。
如果您的系统不支持
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) |
。
注意,如果字符串不是以