¿Cómo analizar las fechas con la cadena de zona horaria -0400 en Python?

81

Tengo una cadena de fecha con el formato '2009/05/13 19:19:30 -0400'. Parece que las versiones anteriores de Python pueden haber admitido una etiqueta de formato% z en strptime para la especificación de la zona horaria final, pero 2.6.x parece haber eliminado eso.

¿Cuál es la forma correcta de analizar esta cadena en un objeto de fecha y hora?

campos
fuente

Respuestas:

117

Puede usar la función de análisis de 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))

De esta manera, obtiene un objeto de fecha y hora que luego puede usar.

Como se respondió , dateutil2.0 está escrito para Python 3.0 y no funciona con Python 2.x. Para Python 2.x, es necesario usar dateutil1.5.

txwikinger
fuente
13
Esto funciona bien para mí ( dateutil2.1) con Python 2.7.2; No se requiere Python 3. Tenga en cuenta que si está instalando desde pip, el nombre del paquete es python-dateutil.
BigglesZX
47

%z es compatible con 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)))

En versiones anteriores:

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

donde FixedOffsetes una clase basada en el ejemplo de código de los documentos :

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
fuente
1
Esto causa un ValueError: 'z' is a bad directive in format '%Y-%m-%d %M:%H:%S.%f %z'en mi caso (Python 2.7).
Jonathan H
@Sheljohn se supone que no funciona en Python 2.7 Mire la parte superior de la respuesta.
jfs
extraño, por cierto, que esto NO se menciona en absoluto en los documentos de Python 2.7 : docs.python.org/2.7/library/…
62mkv
22

Aquí hay una solución del "%z"problema para Python 2.7 y versiones anteriores

En lugar de usar:

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

Utilice timedeltapara dar cuenta de la zona horaria, así:

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

Tenga en cuenta que las fechas se convertirían a GMT, lo que permitiría realizar operaciones aritméticas de fechas sin preocuparse por las zonas horarias.

Uri Goren
fuente
Me gusta esto, aunque necesitas cambiar 'seconds =' a 'minutes ='.
Dave
1
Solo como nota, si quisiera tomar una zona horaria en una cadena y convertir la fecha y hora a UTC, usaría la lógica opuesta que se enumera aquí. Si la zona horaria tiene un +, resta el timedelta y viceversa.
Sector95
La transformación a UTC estaba mal, si hay un +personaje de la timedelta debe restarse , y viceversa. He editado y corregido el código.
tomtastico
7

El problema con el uso de dateutil es que no puede tener la misma cadena de formato tanto para la serialización como para la deserialización, ya que dateutil tiene opciones de formato limitadas (solo dayfirsty yearfirst).

En mi aplicación, almaceno la cadena de formato en un archivo .INI y cada implementación puede tener su propio formato. Por lo tanto, realmente no me gusta el enfoque de dateutil.

Aquí hay un método alternativo que usa pytz en su lugar:

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
fuente
2

Un trazador de líneas para pitones viejos que hay. Puede multiplicar un timedelta por 1 / -1 dependiendo del signo +/-, como en:

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
fuente
-10

Si está en Linux, puede usar el datecomando externo para 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)

Esto, por supuesto, es menos portátil que dateutil, pero un poco más flexible, porque datetambién aceptará entradas como "ayer" o "el año pasado" :-)

Gyom
fuente
3
No creo que sea bueno llamar a un programa externo para esto. Y el siguiente punto débil: eval (): si ahora que un servidor web ejecuta este código, ¡podría ejecutar código arbitrario en el servidor!
guettli
5
Todo depende del contexto: si lo que buscamos es solo un script para escribir y desechar, entonces estas debilidades son simplemente irrelevantes :-)
Gyom
10
Votar en contra de esto porque: 1) Hace una llamada al sistema para algo trivial, 2) Inyecta cadenas directamente en una llamada de shell, 3) Llama a eval () y 4) Tiene una excepción para todo. Básicamente, este es un ejemplo de cómo no hacer las cosas.
benjaoming
En este caso, aunque eval es malo y no debería usarse. una llamada externa parece ser la forma más fácil y práctica de obtener una marca de tiempo de Unix de una cadena de fechas consciente de la zona horaria, donde la zona horaria no es un desplazamiento numérico.
Leliel
1
Bueno, de nuevo, este lema "eval is evil" realmente depende de su contexto (que no fue declarado por el OP). Cuando escribo scripts para mi propio uso, uso eval generosamente y es increíble. ¡Python es un gran lenguaje para pegar scripts! Por supuesto, puede implementar soluciones complicadas sobre ingeniería de casos generales como en algunas respuestas anteriores, y luego afirmar que es la única forma correcta de hacerlo, en Java. Pero para muchos casos de uso, una solución rápida y sucia es igual de buena.
Gyom