¿Cómo puedo analizar una fecha con formato ISO 8601?

Tengo que analizar las cadenas RFC 3339 como "2008-09-03T20:56:35.450686Z" en el tipo de datetime y datetime de Python.

He encontrado strptime en la biblioteca estándar de Python, pero no es muy conveniente.

¿Cuál es la mejor manera de hacer esto?

El paquete python-dateutil puede analizar no solo las cadenas de fecha y hora RFC 3339 como la de la pregunta, sino también otras cadenas de fecha y hora ISO 8601 que no cumplen con RFC 3339 (como las que no tienen una compensación UTC o las que representan sólo una fecha).

 >>> import dateutil.parser >>> dateutil.parser.parse('2008-09-03T20:56:35.450686Z') # RFC 3339 format datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc()) >>> dateutil.parser.parse('2008-09-03T20:56:35.450686') # ISO 8601 extended format datetime.datetime(2008, 9, 3, 20, 56, 35, 450686) >>> dateutil.parser.parse('20080903T205635.450686') # ISO 8601 basic format datetime.datetime(2008, 9, 3, 20, 56, 35, 450686) >>> dateutil.parser.parse('20080903') # ISO 8601 basic format, date only datetime.datetime(2008, 9, 3, 0, 0) 

Tenga en cuenta que el dateutil.parser es intencionalmente hacky: intenta adivinar el formato y hace suposiciones inevitables (solo personalizables a mano) en casos ambiguos. Solo Úselo si necesita analizar la entrada de formato desconocido y está bien tolerar errores de lectura ocasionales. (gracias ivan_pozdeev )

El nombre de Pypi es python-dateutil , no dateutil (gracias code3monk3y ):

 pip install python-dateutil 

Si está utilizando Python 3.7, consulte esta respuesta sobre datetime.datetime.fromisoformat .

Tenga en cuenta que en Python 2.6+ y Py3K, el carácter% f captura microsegundos.

 >>> datetime.datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%fZ") 

Ver problema aquí

Varias respuestas aquí sugieren el uso de datetime.datetime.strptime para analizar los tiempos de las fechas RFC 3339 o ISO 8601 con las zonas horarias, como la que se muestra en la pregunta:

 2008-09-03T20:56:35.450686Z 

Esta es una mala idea.

Suponiendo que desea admitir el formato RFC 3339 completo, incluido el soporte para compensaciones UTC distintas de cero, entonces el código que sugieren estas respuestas no funciona. De hecho, no puede funcionar, porque es imposible analizar la syntax de RFC 3339 usando strptime . Las cadenas de formato utilizadas por el módulo datetime de Python son incapaces de describir la syntax de RFC 3339.

El problema son las compensaciones UTC. El formato de fecha / hora de Internet del RFC 3339 requiere que cada fecha y hora incluya un desplazamiento UTC, y que esas compensaciones puedan ser Z (abreviatura de “Zulu time”) o en formato +HH:MM o -HH:MM , como +05:00 o -10:30 .

En consecuencia, estos son todos los tiempos de referencia RFC 3339 válidos:

  • 2008-09-03T20:56:35.450686Z
  • 2008-09-03T20:56:35.450686+05:00
  • 2008-09-03T20:56:35.450686-10:30

Por desgracia, las cadenas de formato utilizadas por strptime y strftime no tienen ninguna directiva que corresponda a las compensaciones UTC en formato RFC 3339. Puede encontrar una lista completa de las directivas que admiten en https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior , y la única directiva de compensación UTC incluida en la lista es %z

% z

Desplazamiento UTC en la forma + HHMM o -HHMM (cadena vacía si el objeto es ingenuo).

Ejemplo: (vacío), +0000, -0400, +1030

Esto no coincide con el formato de un desplazamiento RFC 3339, y de hecho, si intentamos usar %z en la cadena de formato y analizamos una fecha RFC 3339, fallaremos:

 >>> from datetime import datetime >>> datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%f%z") Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime tt, fraction = _strptime(data_string, format) File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime (data_string, format)) ValueError: time data '2008-09-03T20:56:35.450686Z' does not match format '%Y-%m-%dT%H:%M:%S.%f%z' >>> datetime.strptime("2008-09-03T20:56:35.450686+05:00", "%Y-%m-%dT%H:%M:%S.%f%z") Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime tt, fraction = _strptime(data_string, format) File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime (data_string, format)) ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%f%z' 

(En realidad, lo anterior es justo lo que verá en Python 3. En Python 2 fallaremos por una razón aún más simple, que es que strptime no implementa la directiva %z en Python 2 ).

Las respuestas múltiples aquí que recomiendan que strptime todo strptime esto al incluir una Z literal en su cadena de formato, que coincide con la Z de la cadena datetime de ejemplo de la pregunta (y la descarta, produciendo un objeto datetime sin una zona horaria):

 >>> datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%fZ") datetime.datetime(2008, 9, 3, 20, 56, 35, 450686) 

Dado que esto descarta la información de la zona horaria que se incluyó en la cadena de fecha y hora original, es cuestionable si debemos considerar incluso este resultado como correcto. Pero lo que es más importante, debido a que este enfoque involucra la encoding rígida de un determinado UTC en la cadena de formato , se ahogará en el momento en que intente analizar cualquier fecha y hora RFC 3339 con un desplazamiento UTC diferente:

 >>> datetime.strptime("2008-09-03T20:56:35.450686+05:00", "%Y-%m-%dT%H:%M:%S.%fZ") Traceback (most recent call last): File "", line 1, in File "/usr/lib/python3.4/_strptime.py", line 500, in _strptime_datetime tt, fraction = _strptime(data_string, format) File "/usr/lib/python3.4/_strptime.py", line 337, in _strptime (data_string, format)) ValueError: time data '2008-09-03T20:56:35.450686+05:00' does not match format '%Y-%m-%dT%H:%M:%S.%fZ' 

A menos que esté seguro de que solo necesita admitir tiempos de datos de RFC 3339 en tiempo Zulú, y no con otros desplazamientos de zona horaria, no use tiempo de strptime . Utilice uno de los muchos otros enfoques descritos en las respuestas aquí en su lugar.

Nuevo en Python 3.7+


La biblioteca estándar de datetime introdujo una función para invertir datetime.isoformat() .

classmethod datetime.fromisoformat(date_string) :

Devuelva un datetime correspondiente a un date_string en uno de los formatos emitidos por date.isoformat() y datetime.isoformat() .

Específicamente, esta función admite cadenas en el (los) formato (s):

YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]][+HH:MM[:SS[.ffffff]]]]

donde * puede coincidir con cualquier carácter individual.

Precaución : Esto no es compatible con el análisis de cadenas ISO 8601 arbitrarias; solo está pensado como la operación inversa de datetime.isoformat() .

Ejemplo de uso:

 from datetime import datetime date = datetime.fromisoformat('2017-01-01T12:30:59.000000') 

Prueba el módulo iso8601 ; hace exactamente esto.

Hay varias otras opciones mencionadas en la página de WorkingWithTime en la wiki de python.org.

 importar re, fecha y hora
 s = "2008-09-03T20: 56: 35.450686Z"
 d = datetime.datetime (* map (int, re.split ('[^ \ d]', s) [: - 1]))

¿Cuál es el error exacto que recibe? ¿Es como el siguiente?

 >>> datetime.datetime.strptime("2008-08-12T12:20:30.656234Z", "%Y-%m-%dT%H:%M:%SZ") ValueError: time data did not match format: data=2008-08-12T12:20:30.656234Z fmt=%Y-%m-%dT%H:%M:%SZ 

Si es así, puede dividir la cadena de entrada en “.” Y luego agregar los microsegundos a la fecha y hora que obtuvo.

Prueba esto:

 >>> def gt(dt_str): dt, _, us= dt_str.partition(".") dt= datetime.datetime.strptime(dt, "%Y-%m-%dT%H:%M:%S") us= int(us.rstrip("Z"), 10) return dt + datetime.timedelta(microseconds=us) >>> gt("2008-08-12T12:20:30.656234Z") datetime.datetime(2008, 8, 12, 12, 20, 30, 656234) 

A partir de Python 3.7, strptime admite delimitadores de dos puntos en las compensaciones UTC ( fuente ). Entonces puedes usar:

 import datetime datetime.datetime.strptime('2018-01-31T09:24:31.488670+00:00', '%Y-%m-%dT%H:%M:%S.%f%z') 

En estos días, Arrow también se puede utilizar como una solución de terceros:

 >>> import arrow >>> date = arrow.get("2008-09-03T20:56:35.450686Z") >>> date.datetime datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc()) 

Si no quieres usar dateutil, puedes probar esta función:

 def from_utc(utcTime,fmt="%Y-%m-%dT%H:%M:%S.%fZ"): """ Convert UTC time string to time.struct_time """ # change datetime.datetime to time, return time.struct_time type return datetime.datetime.strptime(utcTime, fmt) 

Prueba:

 from_utc("2007-03-04T21:08:12.123Z") 

Resultado:

 datetime.datetime(2007, 3, 4, 21, 8, 12, 123000) 

Si está trabajando con Django, proporciona el módulo dateparse que acepta un montón de formatos similares al formato ISO, incluida la zona horaria.

Si no está usando Django y no quiere usar una de las otras bibliotecas mencionadas aquí, probablemente podría adaptar el código fuente de Django para dateparse a su proyecto.

Solo usa el módulo python-dateutil :

 >>> import dateutil.parser as dp >>> t = '1984-06-02T19:05:00.000Z' >>> parsed_t = dp.parse(t) >>> print(parsed_t) datetime.datetime(1984, 6, 2, 19, 5, tzinfo=tzutc()) 

Documentación

He encontrado que ciso8601 es la forma más rápida de analizar las marcas de tiempo ISO 8601. Como su nombre lo indica, se implementa en C.

 import ciso8601 ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30') 

GitHub Repo README muestra su velocidad de aceleración> 10x en comparación con todas las otras bibliotecas enumeradas en las otras respuestas.

Mi proyecto personal involucró mucho el análisis de ISO 8601. Fue agradable poder simplemente cambiar la llamada e ir 10 veces más rápido. 🙂

Edit: desde entonces me he convertido en un mantenedor de ciso8601. ¡Ahora es más rápido que nunca!

Soy el autor de iso8601 utils. Se puede encontrar en GitHub o en PyPI . Así es como puedes analizar tu ejemplo:

 >>> from iso8601utils import parsers >>> parsers.datetime('2008-09-03T20:56:35.450686Z') datetime.datetime(2008, 9, 3, 20, 56, 35, 450686) 

Una forma sencilla de convertir una cadena de fecha similar a ISO 8601 en un objeto de marca de tiempo UNIX o datetime.datetime en todas las versiones de Python compatibles sin instalar módulos de terceros es usar el analizador de fechas de SQLite .

 #!/usr/bin/env python from __future__ import with_statement, division, print_function import sqlite3 import datetime testtimes = [ "2016-08-25T16:01:26.123456Z", "2016-08-25T16:01:29", ] db = sqlite3.connect(":memory:") c = db.cursor() for timestring in testtimes: c.execute("SELECT strftime('%s', ?)", (timestring,)) converted = c.fetchone()[0] print("%s is %s after epoch" % (timestring, converted)) dt = datetime.datetime.fromtimestamp(int(converted)) print("datetime is %s" % dt) 

Salida:

 2016-08-25T16:01:26.123456Z is 1472140886 after epoch datetime is 2016-08-25 12:01:26 2016-08-25T16:01:29 is 1472140889 after epoch datetime is 2016-08-25 12:01:29 

He codificado un analizador para el estándar ISO 8601 y lo puse en GitHub: https://github.com/boxed/iso8601 . Esta implementación admite todo en la especificación, excepto las duraciones, los intervalos, los intervalos periódicos y las fechas fuera del rango de fechas admitidas del módulo datetime de Python.

Las pruebas están incluidas! :PAG

La función parse_datetime () de Django admite fechas con compensaciones UTC:

 parse_datetime('2016-08-09T15:12:03.65478Z') = datetime.datetime(2016, 8, 9, 15, 12, 3, 654780, tzinfo=) 

Por lo tanto, podría utilizarse para analizar las fechas ISO 8601 en campos dentro del proyecto completo:

 from django.utils import formats from django.forms.fields import DateTimeField from django.utils.dateparse import parse_datetime class DateTimeFieldFixed(DateTimeField): def strptime(self, value, format): if format == 'iso-8601': return parse_datetime(value) return super().strptime(value, format) DateTimeField.strptime = DateTimeFieldFixed.strptime formats.ISO_INPUT_FORMATS['DATETIME_INPUT_FORMATS'].insert(0, 'iso-8601') 

Debido a que ISO 8601 permite que existan muchas variaciones de dos puntos y guiones opcionales, básicamente CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm] . Si desea utilizar strptime, primero debe eliminar esas variaciones.

El objective es generar un objeto utc datetime.


Si solo desea un caso básico que funcione para UTC con el sufijo Z como 2016-06-29T19:36:29.3453Z :

 datetime.datetime.strptime(timestamp.translate(None, ':-'), "%Y%m%dT%H%M%S.%fZ") 

Si desea manejar las compensaciones de zona horaria como 2016-06-29T19:36:29.3453-0400 o 2008-09-03T20:56:35.450686+05:00 use lo siguiente. Estos convertirán todas las variaciones en algo sin delimitadores variables como 20080903T205635.450686+0500 lo que hace que sea más consistente / más fácil de analizar.

 import re # this regex removes all colons and all # dashes EXCEPT for the dash indicating + or - utc offset for the timezone conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp) datetime.datetime.strptime(conformed_timestamp, "%Y%m%dT%H%M%S.%f%z" ) 

Si su sistema no admite la directiva %z strptime (ve algo como ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z' ), entonces necesita para desplazar manualmente el tiempo desde Z (UTC). Es posible que la nota %z no funcione en su sistema en las versiones de python <3, ya que depende del soporte de la biblioteca c, que varía según el tipo de compilación del sistema / python (es decir, Jython, Cython, etc.).

 import re import datetime # this regex removes all colons and all # dashes EXCEPT for the dash indicating + or - utc offset for the timezone conformed_timestamp = re.sub(r"[:]|([-](?!((\d{2}[:]\d{2})|(\d{4}))$))", '', timestamp) # split on the offset to remove it. use a capture group to keep the delimiter split_timestamp = re.split(r"[+|-]",conformed_timestamp) main_timestamp = split_timestamp[0] if len(split_timestamp) == 3: sign = split_timestamp[1] offset = split_timestamp[2] else: sign = None offset = None # generate the datetime object without the offset at UTC time output_datetime = datetime.datetime.strptime(main_timestamp +"Z", "%Y%m%dT%H%M%S.%fZ" ) if offset: # create timedelta based on offset offset_delta = datetime.timedelta(hours=int(sign+offset[:-2]), minutes=int(sign+offset[-2:])) # offset datetime with timedelta output_datetime = output_datetime + offset_delta 

Esto funciona para stdlib en Python 3.2 en adelante (asumiendo que todas las marcas de tiempo son UTC):

 from datetime import datetime, timezone, timedelta datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").replace( tzinfo=timezone(timedelta(0))) 

Por ejemplo,

 >>> datetime.utcnow().replace(tzinfo=timezone(timedelta(0))) ... datetime.datetime(2015, 3, 11, 6, 2, 47, 879129, tzinfo=datetime.timezone.utc) 

Para algo que funciona con la biblioteca estándar 2.X intente:

 calendar.timegm(time.strptime(date.split(".")[0]+"UTC", "%Y-%m-%dT%H:%M:%S%Z")) 

calendar.timegm es la versión de gm que falta en time.mktime.

Python-dateutil emitirá una excepción si analiza cadenas de fecha no válidas, por lo que es posible que desee capturar la excepción.

 from dateutil import parser ds = '2012-60-31' try: dt = parser.parse(ds) except ValueError, e: print '"%s" is an invalid date' % ds 

Hoy en día, está Maya: Datetimes for Humans ™ , del autor del popular paquete Solicitudes: HTTP para Humans ™:

 >>> import maya >>> str = '2008-09-03T20:56:35.450686Z' >>> maya.MayaDT.from_rfc3339(str).datetime() datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=) 

Gracias a la excelente respuesta de Mark Amery, diseñé una función para dar cuenta de todos los formatos ISO posibles de datetime:

 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 eg, 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) def __getinitargs__(self): return (self.__offset.total_seconds()/60,) def parse_isoformat_datetime(isodatetime): try: return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S.%f') except ValueError: pass try: return datetime.strptime(isodatetime, '%Y-%m-%dT%H:%M:%S') except ValueError: pass pat = r'(.*?[+-]\d{2}):(\d{2})' temp = re.sub(pat, r'\1\2', isodatetime) naive_date_str = temp[:-5] offset_str = temp[-5:] naive_dt = datetime.strptime(naive_date_str, '%Y-%m-%dT%H:%M:%S.%f') offset = int(offset_str[-4:-2])*60 + int(offset_str[-2:]) if offset_str[0] == "-": offset = -offset return naive_dt.replace(tzinfo=FixedOffset(offset)) 
 def parseISO8601DateTime(datetimeStr): import time from datetime import datetime, timedelta def log_date_string(when): gmt = time.gmtime(when) if time.daylight and gmt[8]: tz = time.altzone else: tz = time.timezone if tz > 0: neg = 1 else: neg = 0 tz = -tz h, rem = divmod(tz, 3600) m, rem = divmod(rem, 60) if neg: offset = '-%02d%02d' % (h, m) else: offset = '+%02d%02d' % (h, m) return time.strftime('%d/%b/%Y:%H:%M:%S ', gmt) + offset dt = datetime.strptime(datetimeStr, '%Y-%m-%dT%H:%M:%S.%fZ') timestamp = dt.timestamp() return dt + timedelta(hours=dt.hour-time.gmtime(timestamp).tm_hour) 

Tenga en cuenta que deberíamos ver si la cadena no termina con Z , podríamos analizar usando %z .