从电子邮件中解析带时区的日期?

41

我正在尝试从一封电子邮件中检索日期。首先很容易:

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,因为 %Z 不是 +0100 的格式。但是我在文档中找不到时区的正确格式,只有这个 %Z。有人能帮我吗?

8个回答

42

email.utils提供了一个parsedate()函数,用于解析RFC 2822格式的日期,据我所知该函数并未被弃用。

>>> 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始终期望一个本地时间元组。

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

是的,这些函数似乎已经移到了 utils 中,使用电子邮件是可以的。谢谢。 - gruszczy
这样做得不到准确的值。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,以防他依赖此方法。 - Eric Pruitt
3
mktime+时区可能会对过去的日期或具有夏令时转换的时区产生错误的值:time.timezone != time.altzone。使用tt = parsedate_tz(date_str); timestamp = calendar.timegm(tt) - tt[9]代替。 - jfs
4
在较新版本的Python中,您还可以使用email.utils.parsedate_to_datetime - mgilbert

37

使用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

如果本地时区在date_tuple中具有不同的UTC偏移,则Python 2.7.4之前的mktime_tz可能会失败。在这种情况下,请直接使用calendar.timegm()。参见链接:https://dev59.com/FnI-5IYBdhLWcg3wkJMA#eKifEYcBWogLw_1bm5Zs - jfs
这将返回一个UTC的naive datetime。要使其aware,您可以提供一个时区作为fromtimestamp的第二个参数。在Python 3中,这很容易:datetime.timezone.utc。在Python 2.7中,您需要实现一个UTC tzinfo并提供它。 - jtbr
1
在Python 3.7中,parsedate_tz没有在日期时间“2019-03-14 20:43:56 +0300”中计算时区偏移量,只返回了一个naive的“2019-03-14 20:43:56”。尽管@jfs答案中的email.utils.parsedate_to_datetime解决了这个问题,并返回了tz-aware对象。 - klapshin

17

对于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()相同的功能,但是在成功时返回一个datetime对象。如果输入日期的时区为-0000,则datetime将是一个naive datetime对象;如果日期符合RFC,则datetime将表示UTC时间,但不会显示消息来源时区的实际指示。如果输入日期具有任何其他有效的时区偏移量,则datetime将是具有相应时区tzinfo的aware datetime对象。从版本3.3开始支持。


11

在 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时间的原始datetime对象;否则,返回具有相应tzinfo设置的知道时区信息的datetime对象。 要在早期Python版本(2.6+)中解析RFC 5322日期时间字符串
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基于datetime文档中的tzinfo子类:

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

2

是的,我看到了,但它已经过时了。 - gruszczy
这个函数现在被称为email.utils.parsedate_tz(),顺便说一下。 - SamB

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

0

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

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)

-1

ValueError: 'z'在格式中是一个错误的指令...

(注意:在我的情况下,我必须坚持使用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


1
请注意,问题中定义的时间格式是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 - jfs
@J.F.Sebastian,如果您没有在其中一个重复的问题上删除我的答案,我就不会在这里发布我的答案。我的问题是“strptime无法处理%z格式”,我认为这是同样的问题。 - dnozay
我无法自己删除别人的答案。你能链接到相应的问题吗? - jfs

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