Jak analizować daty z ciągiem stref czasowych -0400 w Pythonie?

81

Mam ciąg daty w postaci „2009/05/13 19:19:30 -0400”. Wygląda na to, że poprzednie wersje Pythona mogły obsługiwać znacznik formatu% z w strptime dla końcowej specyfikacji strefy czasowej, ale 2.6.x najwyraźniej to usunęło.

Jaki jest właściwy sposób parsowania tego ciągu w obiekt datetime?

pola
źródło

Odpowiedzi:

117

Możesz użyć funkcji parsowania z dateutil:

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

W ten sposób otrzymujesz obiekt daty i godziny, którego możesz następnie użyć.

Jak odpowiedziałem , dateutil2.0 jest napisane dla Pythona 3.0 i nie działa z Pythonem 2.x. W przypadku Pythona 2.x należy użyć dateutil1.5.

txwikinger
źródło
13
To działa dobrze dla mnie ( dateutil2.1) z Pythonem 2.7.2; Python 3 nie jest wymagany. Zwróć uwagę, że jeśli instalujesz z pip, nazwa pakietu to python-dateutil.
BigglesZX
47

%z jest obsługiwany w Pythonie 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)))

We wcześniejszych wersjach:

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

gdzie FixedOffsetto klasa oparta na przykładzie kodu z dokumentacji :

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)
jfs
źródło
1
To powoduje ValueError: 'z' is a bad directive in format '%Y-%m-%d %M:%H:%S.%f %z'w moim przypadku (Python 2.7).
Jonathan H
@Sheljohn to nie powinno działać w Pythonie 2.7 Spójrz na samą górę odpowiedzi.
jfs
dziwne, nawiasem mówiąc, NIE jest to NIGDY wspomniane w dokumentach Pythona 2.7 : docs.python.org/2.7/library/ ...
62mkv
22

Oto rozwiązanie "%z"problemu dotyczącego języka Python 2.7 i wcześniejszych

Zamiast używać:

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

Użyj, timedeltaaby uwzględnić strefę czasową, na przykład:

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

Zauważ, że daty zostaną przekonwertowane na GMT, co pozwoliłoby na arytmetykę dat bez martwienia się o strefy czasowe.

Uri Goren
źródło
Podoba mi się to, chociaż musisz zmienić „sekundy =” na „minuty =”.
Dave,
1
Uwaga: jeśli chcesz wziąć strefę czasową w ciągu i przekonwertować datę i godzinę na UTC, użyjesz odwrotnej logiki wymienionej tutaj. Jeśli strefa czasowa ma znak +, odejmujesz deltę czasu i odwrotnie.
Sector95
Transformacja do UTC była nieprawidłowa, jeśli istnieje +znak, należy odjąć timedeltę i odwrotnie. Edytowałem i poprawiłem kod.
tomtastico
7

Problem z używaniem dateutil polega na tym, że nie można mieć tego samego ciągu formatu zarówno dla serializacji, jak i deserializacji, ponieważ dateutil ma ograniczone opcje formatowania (tylko dayfirsti yearfirst).

W mojej aplikacji przechowuję ciąg formatu w pliku .INI, a każde wdrożenie może mieć własny format. Dlatego naprawdę nie podoba mi się podejście dateutil.

Oto alternatywna metoda, która zamiast tego używa 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)
sayap
źródło
2

Jedna linijka dla starych Pythonów. Możesz pomnożyć timedelta przez 1 / -1 w zależności od znaku +/-, jak w:

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)
Eric Sellin
źródło
-10

Jeśli korzystasz z systemu Linux, możesz użyć zewnętrznego datepolecenia, aby zmniejszyć:

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)

Jest to oczywiście mniej przenośne niż dateutil, ale nieco bardziej elastyczne, ponieważ date akceptuje również dane wejściowe, takie jak „wczoraj” lub „zeszły rok” :-)

Gyom
źródło
3
Nie sądzę, aby w tym celu dzwonić do zewnętrznego programu. I kolejny słaby punkt: eval (): Jeśli teraz serwer WWW wykonuje ten kod, możesz wykonać dowolny kod na serwerze!
guettli
5
Wszystko zależy od kontekstu: jeśli to, czego szukamy, to tylko skrypt do pisania i wyrzucania, to te słabości są po prostu nieistotne :-)
Gyom
10
Głosowanie w dół, ponieważ: 1) powoduje wywołanie systemowe czegoś trywialnego, 2) wstrzykuje ciągi znaków bezpośrednio do wywołania powłoki, 3) wywołuje eval () i 4) ma wyjątek przechwytujący wszystko. Zasadniczo jest to przykład tego, jak nie robić rzeczy.
benjaoming
W tym przypadku, chociaż eval jest zły i nie powinien być używany. połączenie zewnętrzne wydaje się być najłatwiejszym i najbardziej praktycznym sposobem na uzyskanie uniksowego znacznika czasu z datestingu uwzględniającego strefę czasową, gdzie strefa czasowa nie jest przesunięciem liczbowym.
Leliel
1
Cóż, znowu, to motto „eval is evil” naprawdę zależy od twojego kontekstu (który nie został określony przez OP). Kiedy piszę skrypty na własny użytek, swobodnie używam eval i to jest niesamowite. Python to świetny język dla skryptów kleju! Oczywiście możesz wdrożyć zawiłe, nadmiernie zaawansowane rozwiązania, jak w niektórych odpowiedziach powyżej, a następnie twierdzić, że jest to jedyny właściwy sposób, ala Java. Ale dla wielu zastosowań szybkie i brudne rozwiązanie jest równie dobre.
Gyom