我需要将RFC 3339字符串(如“2008-09-03T20:56:55.450686Z”)解析为Python的datetime类型。

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

最好的方法是什么?


当前回答

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进行解析。

其他回答

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

from datetime import datetime, timezone, timedelta
datetime.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)

现在有玛雅:人类的日期时间™, 来自流行的Requests: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, 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中工作得很好:

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:

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)

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

如果您没有使用Django,并且不想使用这里提到的其他库之一,那么您可能会将Django的dateparse源代码调整为适合您的项目。

从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)和没有小数秒的(如2021-01-01T12: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不支持。如果您不仅想解析known-to-be-RFC-339日期时间,而且还想严格验证您获得的日期时间是否为RFC 3339,请使用另一种方法或添加您自己的逻辑来验证时区偏移格式。这个函数肯定不支持所有的ISO 8601,它包括比RFC 3339更广泛的格式。(例如,2009-W01-1是有效的ISO 8601日期。)它在Python 3.6或更早版本中不起作用,因为在那些旧版本中,%z说明符只匹配+0500或-0430或+0000等时区偏移,而不是+05:00或-04:30或z等RFC 3339时区偏移。