【问题标题】:Parsing date with timezone from an email?从电子邮件中解析带有时区的日期?
【发布时间】:2010-12-19 22:24:22
【问题描述】:

我正在尝试从电子邮件中检索日期。一开始很简单:

message = email.parser.Parser().parse(file)
date = message['Date']
print date

我收到:

'Mon, 16 Nov 2009 13:32:02 +0100'

但我需要一个不错的日期时间对象,所以我使用:

datetime.strptime('Mon, 16 Nov 2009 13:32:02 +0100', '%a, %d %b %Y %H:%M:%S %Z')

引发ValueError, since %Z isn't format for +0100。但是我在文档中找不到正确的时区格式,只有这个%Z 用于时区。有人可以帮我吗?

【问题讨论】:

    标签: python datetime timezone format rfc5322


    【解决方案1】:

    email.utils 有一个用于 RFC 2822 格式的 parsedate() 函数,据我所知,它并没有被弃用。

    >>> import email.utils
    >>> import time
    >>> import datetime
    >>> email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0100')
    (2009, 11, 16, 13, 32, 2, 0, 1, -1)
    >>> time.mktime((2009, 11, 16, 13, 32, 2, 0, 1, -1))
    1258378322.0
    >>> datetime.datetime.fromtimestamp(1258378322.0)
    datetime.datetime(2009, 11, 16, 13, 32, 2)
    

    但是请注意,parsedate 方法不考虑时区,time.mktime 始终需要一个本地时间元组,如 here 所述。

    >>> (time.mktime(email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0900')) ==
    ... time.mktime(email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0100'))
    True
    

    所以你仍然需要解析出时区并考虑本地时差:

    >>> REMOTE_TIME_ZONE_OFFSET = +9 * 60 * 60
    >>> (time.mktime(email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0900')) +
    ... time.timezone - REMOTE_TIME_ZONE_OFFSET)
    1258410122.0
    

    【讨论】:

    • 是的,这些功能似乎已移至实用程序,并且可以使用电子邮件。谢谢。
    • 这不会产生准确的值。 time.mktime 假设一个本地时间元组,parsedate 函数不考虑时区:time.mktime(email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0900')) == time.mktime(email.utils.parsedate('Mon, 16 Nov 2009 13:32:02 +0100')) 返回True。标记@gruszczy,以防他依赖这种方法。
    • mktime + timezone 可能会为过去的日期或时区有 DST 转换产生错误的值:time.timezone != time.altzone。请改用tt = parsedate_tz(date_str); timestamp = calendar.timegm(tt) - tt[9]
    • 在更新的python版本中你也可以使用email.utils.parsedate_to_datetime
    【解决方案2】:

    使用email.utils.parsedate_tz(date):

    msg=email.message_from_file(open(file_name))
    date=None
    date_str=msg.get('date')
    if date_str:
        date_tuple=email.utils.parsedate_tz(date_str)
        if date_tuple:
            date=datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple))
    if date:
        ... # valid date found
    

    【讨论】:

    • mktime_tz 可能会在 2.7.4 之前的 Python 上失败,如果本地时区在 date_tuple 处具有不同的 UTC 偏移量。 Use calendar.timegm() directly in this case.
    • 这会在 UTC 中返回一个幼稚的 datetime。为了让它知道,您可以提供一个时区作为fromtimestamp 的第二个参数。在 python 3 中,这很简单:datetime.timezone.utc。在 python 2.7 中,您需要 implement a UTC tzinfo class 并提供它。
    • 在 python 3.7 中,parsedate_tz 没有计算日期时间 '2019-03-14 20:43:56 +0300' 中的 tz shift,只是返回了一个幼稚的 '2019-03-14 20:43:56' .尽管来自@jfs 答案的 email.utils.parsedate_to_datetime 解决了问题并返回了 tz-aware 对象。
    【解决方案3】:

    对于 python 3.3+,你可以使用 parsedate_to_datetime 函数:

    >>> from email.utils import parsedate_to_datetime
    >>> parsedate_to_datetime('Mon, 16 Nov 2009 13:32:02 +0100')
    ...
    datetime.datetime(2009, 11, 16, 13, 32, 2, tzinfo=datetime.timezone(datetime.timedelta(0, 3600)))
    

    官方文档:

    format_datetime() 的倒数。执行相同的功能 parsedate(),但成功时返回一个日期时间。如果输入日期有 -0000 的时区,日期时间将是一个天真的日期时间,如果 date 符合 RFC,它将代表 UTC 时间,但 没有指示消息的实际源时区 日期来自。如果输入日期有任何其他有效时区 偏移量,日期时间将是具有相应的感知日期时间 时区 tzinfo。 3.3 版中的新功能。

    【讨论】:

      【解决方案4】:

      在 Python 3.3+ 中,email 消息可以为您解析标头:

      import email
      import email.policy
      
      headers = email.message_from_file(file, policy=email.policy.default)
      print(headers.get('date').datetime)
      # -> 2009-11-16 13:32:02+01:00
      

      从 Python 3.2+ 开始,如果您将 %Z 替换为 %z,它就可以工作:

      >>> from datetime import datetime
      >>> datetime.strptime("Mon, 16 Nov 2009 13:32:02 +0100", 
      ...                   "%a, %d %b %Y %H:%M:%S %z")
      datetime.datetime(2009, 11, 16, 13, 32, 2,
                        tzinfo=datetime.timezone(datetime.timedelta(0, 3600)))
      

      或使用email 包(Python 3.3+):

      >>> from email.utils import parsedate_to_datetime
      >>> parsedate_to_datetime("Mon, 16 Nov 2009 13:32:02 +0100")
      datetime.datetime(2009, 11, 16, 13, 32, 2,
                        tzinfo=datetime.timezone(datetime.timedelta(0, 3600)))
      

      如果将 UTC 偏移量指定为 -0000,则它返回一个以 UTC 表示时间的原始日期时间对象,否则它返回一个具有相应 tzinfo 设置的感知日期时间对象。

      在较早的 Python 版本(2.6+)上解析 rfc 5322 date-time string

      from calendar import timegm
      from datetime import datetime, timedelta, tzinfo
      from email.utils import parsedate_tz
      
      ZERO = timedelta(0)
      time_string = 'Mon, 16 Nov 2009 13:32:02 +0100'
      tt = parsedate_tz(time_string)
      #NOTE: mktime_tz is broken on Python < 2.7.4,
      #  see https://bugs.python.org/issue21267
      timestamp = timegm(tt) - tt[9] # local time - utc offset == utc time
      naive_utc_dt = datetime(1970, 1, 1) + timedelta(seconds=timestamp)
      aware_utc_dt = naive_utc_dt.replace(tzinfo=FixedOffset(ZERO, 'UTC'))
      aware_dt = aware_utc_dt.astimezone(FixedOffset(timedelta(seconds=tt[9])))
      print(aware_utc_dt)
      print(aware_dt)
      # -> 2009-11-16 12:32:02+00:00
      # -> 2009-11-16 13:32:02+01:00
      

      FixedOffset is based on tzinfo subclass from the datetime documentation:

      class FixedOffset(tzinfo):
          """Fixed UTC offset: `time = utc_time + utc_offset`."""
          def __init__(self, offset, name=None):
              self.__offset = offset
              if name is None:
                  seconds = abs(offset).seconds
                  assert abs(offset).days == 0
                  hours, seconds = divmod(seconds, 3600)
                  if offset < ZERO:
                      hours = -hours
                  minutes, seconds = divmod(seconds, 60)
                  assert seconds == 0
                  #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>GMT%+d' % (hours, minutes, -hours)
              else:
                  self.__name = name
          def utcoffset(self, dt=None):
              return self.__offset
          def tzname(self, dt=None):
              return self.__name
          def dst(self, dt=None):
              return ZERO
          def __repr__(self):
              return 'FixedOffset(%r, %r)' % (self.utcoffset(), self.tzname())
      

      【讨论】:

        【解决方案5】:

        你试过了吗

        rfc822.parsedate_tz(date) # ?
        

        更多关于 RFC822,http://docs.python.org/library/rfc822.html

        不过,它已被弃用(parsedate_tz 现在位于 email.utils.parsedate_tz)。

        但也许这些答案会有所帮助:

        【讨论】:

        • 是的,我见过它,但它已被弃用。
        • 这个函数现在被称为 email.utils.parsedate_tz(), FWIW。
        【解决方案6】:
        # Parses Nginx' format of "01/Jan/1999:13:59:59 +0400"
        # Unfortunately, strptime doesn't support %z for the UTC offset (despite what
        # the docs actually say), hence the need # for this function.
        def parseDate(dateStr):
            date = datetime.datetime.strptime(dateStr[:-6], "%d/%b/%Y:%H:%M:%S")
            offsetDir = dateStr[-5]
            offsetHours = int(dateStr[-4:-2])
            offsetMins = int(dateStr[-2:])
            if offsetDir == "-":
                offsetHours = -offsetHours
                offsetMins = -offsetMins
            return date + datetime.timedelta(hours=offsetHours, minutes=offsetMins)
        

        【讨论】:

          【解决方案7】:

          对于那些想要获得正确当地时间的人,这是我所做的:

          from datetime import datetime
          from email.utils import parsedate_to_datetime
          
          mail_time_str = 'Mon, 16 Nov 2009 13:32:02 +0100'
          
          local_time_str = datetime.fromtimestamp(parsedate_to_datetime(mail_time_str).timestamp()).strftime('%Y-%m-%d %H:%M:%S')
          
          print(local_time_str)
          

          【讨论】:

            【解决方案8】:

            ValueError: 'z' is a bad directive in format...

            (注意:在我的情况下,我必须坚持使用 python 2.7)

            我在从 git log --date=iso8601 的输出解析提交日期时遇到了类似的问题,这实际上不是 ISO8601 格式(因此在更高版本中添加了 --date=iso8601-strict)。

            由于我使用的是django,我可以利用那里的实用程序。

            https://github.com/django/django/blob/master/django/utils/dateparse.py

            >>> from django.utils.dateparse import parse_datetime
            >>> parse_datetime('2013-07-23T15:10:59.342107+01:00')
            datetime.datetime(2013, 7, 23, 15, 10, 59, 342107, tzinfo=+0100)
            

            您可以使用自己的正则表达式来代替strptime

            【讨论】:

            • 它没有回答这个问题。您使用 不同 时间格式。注意:问题中的时间格式在 rfc 5322(及其前身)中定义——它可以在 Python 2.7 上使用email.utils.parsedate_tz 进行解析。您的格式看起来像 rfc 3339。两者都可以在 Python 2 上使用 dateutil.parser.parse() 解析。请参阅 Convert timestamps with offset to datetime obj using strptime
            • @J.F.Sebastian,如果您没有删除我对其中一个重复问题的回答,我就不会在这里发布我的回答。我的问题是strptime does not handle %z format,我相信这是同样的问题。
            • 不能自己删除别人的回答。你能链接到相应的问题吗?
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-01-12
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多