如何在Python中解析带有-0400时区字符串的日期?

95
我有一个日期字符串,格式为“2009/05/13 19:19:30 -0400”。似乎早期的Python版本可能支持strptime中的%z格式标签用于尾随时区规定,但2.6.x似乎已经删除了该功能。

如何正确将此字符串解析为datetime对象?

6个回答

131
你可以使用dateutil中的parse函数:
>>> from dateutil.parser import parse
>>> d = parse('2009/05/13 19:19:30 -0400')
>>> d
datetime.datetime(2009, 5, 13, 19, 19, 30, tzinfo=tzoffset(None, -14400))

通过这种方式,您可以获取一个日期时间对象,然后可以使用它。

正如回答的那样,dateutil2.0是为Python 3.0编写的,不适用于Python 2.x。对于Python 2.x,需要使用dateutil1.5。


13
对我来说这很好用(dateutil 2.1),适用于 Python 2.7.2;不需要Python3。请注意,如果您从pip安装,则包名称为python-dateutil - BigglesZX

61

%z在Python 3.2+中支持:

>>> from datetime import datetime
>>> datetime.strptime('2009/05/13 19:19:30 -0400', '%Y/%m/%d %H:%M:%S %z')
datetime.datetime(2009, 5, 13, 19, 19, 30,
                  tzinfo=datetime.timezone(datetime.timedelta(-1, 72000)))

在早期版本中:

from datetime import datetime

date_str = '2009/05/13 19:19:30 -0400'
naive_date_str, _, offset_str = date_str.rpartition(' ')
naive_dt = datetime.strptime(naive_date_str, '%Y/%m/%d %H:%M:%S')
offset = int(offset_str[-4:-2])*60 + int(offset_str[-2:])
if offset_str[0] == "-":
   offset = -offset
dt = naive_dt.replace(tzinfo=FixedOffset(offset))
print(repr(dt))
# -> datetime.datetime(2009, 5, 13, 19, 19, 30, tzinfo=FixedOffset(-240))
print(dt)
# -> 2009-05-13 19:19:30-04:00

其中FixedOffset是一个基于文档中示例代码的类:

from datetime import timedelta, tzinfo

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)

1
这在我的情况下(Python 2.7)会导致一个“ValueError:'z'是格式'%Y-%m-%d %M:%H:%S.%f %z'中的错误指令”。 - Jonathan H
@Sheljohn 这不应该在 Python 2.7 上工作。请查看答案的顶部。 - jfs
顺便说一句,这在 Python 2.7 文档中根本没有提到,真是奇怪:https://docs.python.org/2.7/library/datetime.html?highlight=datetime#strftime-strptime-behavior - 62mkv

26

这里是针对Python 2.7及更早版本中“%z”问题的修复方法。

不要再使用以下方式:

datetime.strptime(t,'%Y-%m-%dT%H:%M %z')

使用 timedelta 来考虑时区,如下所示:

from datetime import datetime,timedelta
def dt_parse(t):
    ret = datetime.strptime(t[0:16],'%Y-%m-%dT%H:%M')
    if t[18]=='+':
        ret-=timedelta(hours=int(t[19:22]),minutes=int(t[23:]))
    elif t[18]=='-':
        ret+=timedelta(hours=int(t[19:22]),minutes=int(t[23:]))
    return ret

请注意,日期将被转换为 GMT,这将允许进行日期算术运算而不必担心时区问题。

我喜欢这个,不过你需要把“seconds=”改成“minutes=”。 - Dave
1
只是提醒一下,如果您想将时区转换为字符串,并将日期时间转换为UTC,则应使用此处列出的相反逻辑。如果时区有一个 +,则减去timedelta,反之亦然。 - Sector95
转换为UTC的过程有误,如果存在 + 字符,则应该减去时间差,反之亦然。我已经编辑并纠正了代码。 - tomtastico

7
使用dateutil的问题在于你不能使用相同的格式字符串进行序列化和反序列化,因为dateutil的格式选项有限(仅限dayfirst和yearfirst)。在我的应用程序中,我将格式字符串存储在.INI文件中,每个部署都可以有自己的格式。因此,我真的不喜欢dateutil的方法。以下是使用pytz的替代方法:
from datetime import datetime, timedelta

from pytz import timezone, utc
from pytz.tzinfo import StaticTzInfo

class OffsetTime(StaticTzInfo):
    def __init__(self, offset):
        """A dumb timezone based on offset such as +0530, -0600, etc.
        """
        hours = int(offset[:3])
        minutes = int(offset[0] + offset[3:])
        self._utcoffset = timedelta(hours=hours, minutes=minutes)

def load_datetime(value, format):
    if format.endswith('%z'):
        format = format[:-2]
        offset = value[-5:]
        value = value[:-5]
        return OffsetTime(offset).localize(datetime.strptime(value, format))

    return datetime.strptime(value, format)

def dump_datetime(value, format):
    return value.strftime(format)

value = '2009/05/13 19:19:30 -0400'
format = '%Y/%m/%d %H:%M:%S %z'

assert dump_datetime(load_datetime(value, format), format) == value
assert datetime(2009, 5, 13, 23, 19, 30, tzinfo=utc) \
    .astimezone(timezone('US/Eastern')) == load_datetime(value, format)

5

对于旧版的Python,您可以通过以下方式将timedelta乘以1 /-1,具体取决于+/-符号:

datetime.strptime(s[:19], '%Y-%m-%dT%H:%M:%S') + timedelta(hours=int(s[20:22]), minutes=int(s[23:])) * (-1 if s[19] == '+' else 1)

-10

如果你使用的是Linux系统,那么你可以使用外部的date命令来执行dwim操作:

import commands, datetime

def parsedate(text):
  output=commands.getoutput('date -d "%s" +%%s' % text )
  try:
      stamp=eval(output)
  except:
      print output
      raise
  return datetime.datetime.frometimestamp(stamp)

这当然比dateutil不太便携,但稍微更灵活一些,因为date也会接受像“昨天”或“去年”之类的输入 :-)


3
我认为调用外部程序并不好。接下来的一个弱点是:eval()函数——如果你知道一个 Web 服务器会执行这段代码,你可以在服务器上进行任意代码执行! - guettli
5
一切都取决于上下文:如果我们只需要编写并丢弃脚本,那么这些弱点就是不相关的 :-) - Gyom
11
因为以下原因我要给这个点赞数点踩:1)它为了一些微不足道的事情而进行系统调用,2)它直接将字符串注入到 shell 调用中,3)它调用了 eval(),4)它具有异常捕获所有。基本上,这是一个如何做事情的反例。 - benjaoming
在这种情况下,虽然 eval 是有害的,不应该使用。但是,从一个时区感知的日期字符串获取 Unix 时间戳的最简单和最实用的方法似乎是进行外部调用,其中时区不是数字偏移量。 - Leliel
1
再次强调,“eval是邪恶的”这句话实际上取决于你的情境(但OP并没有说明他的情境)。当我为自己编写脚本时,我会大量使用eval,而且效果很棒。Python是一个极好的胶水脚本语言!当然你可以像其他一些答案中那样提供繁琐、过度设计的通用解决方案,并声称这是唯一正确的方法,就像Java一样。但对于许多用例来说,快速而简单的解决方案同样适用。 - Gyom

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