web-dev-qa-db-fra.com

Comment analyser une date au format ISO 8601?

J'ai besoin d'analyser les chaînes RFC 3339 telles que "2008-09-03T20:56:35.450686Z" dans le type datetime de Python.

J'ai trouvé strptime dans la bibliothèque standard Python, mais ce n'est pas très pratique.

Quelle est la meilleure façon de procéder?

549
Alexander Artemenko

Le package python-dateutil peut analyser non seulement les chaînes datetime RFC 3339 telles que celle de la question, mais également les autres ISO 8601 Les chaînes de date et d'heure qui ne sont pas conformes à la RFC 3339 (telles que celles sans décalage UTC ou celles ne représentant qu'une date).

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

Soyez averti que le _dateutil.parser_ est intentionnellement hacky: il essaie de deviner le format et fait des suppositions inévitables (personnalisables à la main uniquement) dans des cas ambigus. Donc, utilisez-le UNIQUEMENT si vous devez analyser une entrée de format inconnu et tolérer des erreurs de lecture occasionnelles. (merci ivan_pozdeev )

Le nom Pypi est python-dateutil , pas dateutil (merci code3monk3y ):

_pip install python-dateutil
_

Si vous utilisez Python 3.7, regardez cette réponse à propos de _datetime.datetime.fromisoformat_.

394
Flimm

Remarque dans Python 2.6+ et Py3K, le caractère% f intercepte des microsecondes.

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

Voir le numéro ici

152
sethbc

Plusieursréponsesicisuggérer utiliser datetime.datetime.strptime pour analyser la RFC 3339 ou Les dates/heures ISO 8601 avec les fuseaux horaires, comme celle présentée dans la question:

_2008-09-03T20:56:35.450686Z
_

C'est une mauvaise idée.

En supposant que vous souhaitiez prendre en charge le format complet RFC 3339, y compris la prise en charge des décalages UTC autres que zéro, le code suggéré par ces réponses ne fonctionne pas. En effet, il ne peut pas fonctionner, car l'analyse de la syntaxe RFC 3339 à l'aide de strptime est impossible. Les chaînes de format utilisées par le module datetime de Python sont incapables de décrire la syntaxe RFC 3339.

Le problème vient des décalages UTC. Le format Internet Date/Heure RFC 3339 requiert que chaque date-heure comprenne un décalage UTC, et que ces décalages peuvent être soit Z (abréviation de "temps zoulou") ou en _+HH:MM_ ou _-HH:MM_ format, comme _+05:00_ ou _-10:30_.

Par conséquent, il s’agit de dates RFC 3339 valides:

  • _2008-09-03T20:56:35.450686Z_
  • _2008-09-03T20:56:35.450686+05:00_
  • _2008-09-03T20:56:35.450686-10:30_

Hélas, les chaînes de format utilisées par strptime et strftime n'ont pas de directive correspondant aux décalages UTC au format RFC 3339. Une liste complète des directives prises en charge est disponible à l’adresse suivante: https://docs.python.org/3/library/datetime.html#strftime-and-strptime-behavior , et le seul décalage UTC La directive incluse dans la liste est _%z_:

% z

Décalage UTC sous la forme + HHMM ou -HHMM (chaîne vide si l'objet est naïf).

Exemple: (vide), +0000, -0400, +1030

Cela ne correspond pas au format d'un offset RFC 3339, et si nous essayons d'utiliser _%z_ dans la chaîne de format et d'analyser une date RFC 3339, nous échouerons:

_>>> 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 fait, ce qui précède correspond exactement à ce que vous verrez dans Python 3. Dans Python 2, nous échouerons pour une raison encore plus simple, à savoir que strptime n'implémente pas la directive _%z_ du tout dans Python 2 .)

Les réponses multiples recommandées ici strptime contournent toutes ce problème en incluant un littéral Z dans leur chaîne de format, qui correspond à la Z de l'exemple de chaîne datetime (et la supprime, produisant un objet datetime sans fuseau horaire:

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

Dans la mesure où cela supprime les informations de fuseau horaire incluses dans la chaîne datetime originale, il est difficile de savoir si nous devrions considérer ce résultat comme correct. Mais plus important encore, comme cette approche implique de coder en dur un décalage UTC particulier dans la chaîne de formatage , elle s’étouffera au moment où elle essaiera d’analyser une RFC 3339. date-heure avec un décalage UTC différent:

_>>> 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'_

Sauf si vous êtes certain que vous n’avez besoin que de prendre en charge les dates de la RFC 3339 à l’heure zoulou, et non celles avec un décalage de fuseau horaire différent, n’utilisez pas strptime. Utilisez plutôt l’une des nombreuses autres approches décrites dans les réponses.

142
Mark Amery

Nouveau dans Python 3.7+


La bibliothèque standard datetime a introduit une fonction permettant d'inverser datetime.isoformat().

classmethoddatetime.fromisoformat(date_string) :

Retourne un datetime correspondant à un date_string dans l'un des formats émis par date.isoformat() et datetime.isoformat().

Plus précisément, cette fonction prend en charge les chaînes dans le ou les formats suivants:

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

* peut correspondre à n'importe quel caractère.

Attention : ceci ne prend pas en charge l'analyse de chaînes ISO 8601 arbitraires. Il s'agit uniquement de l'opération inverse de datetime.isoformat().

Exemple d'utilisation:

from datetime import datetime

date = datetime.fromisoformat('2017-01-01T12:30:59.000000')
108
abccd

Essayez le module iso8601 ; il fait exactement cela.

Plusieurs autres options sont mentionnées sur la page WorkingWithTime du wiki de python.org.

72
Nicholas Riley
 import re, datetime 
 s = "2008-09-03T20: 56: 35.450686Z" 
 d = datetime.datetime (* map (int, re.split ('[ ^\d] ', s) [: - 1])) 
34
Ted

Quelle est l'erreur exacte que vous obtenez? Est-ce comme ce qui suit?

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

Si c'est le cas, vous pouvez fractionner votre chaîne d'entrée en ".", Puis ajouter les microsecondes à la date et à l'heure que vous avez obtenues.

Essaye ça:

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

À partir de Python 3.7, strptime prend en charge les délimiteurs de deux points dans les décalages UTC ( source ). Vous pouvez alors utiliser:

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

De nos jours, Arrow peut également être utilisé comme solution tierce:

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

Si vous ne voulez pas utiliser dateutil, vous pouvez essayer cette fonction:

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)

Tester:

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

Résultat:

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

Utilisez simplement le module 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())

Documentation

13
Blairg23

Si vous travaillez avec Django, il fournit le module dateparse qui accepte un grand nombre de formats similaires au format ISO, y compris le fuseau horaire.

Si vous n'utilisez pas Django et que vous ne souhaitez pas utiliser l'une des autres bibliothèques mentionnées ici, vous pourrez probablement adapter le code source Django de dateparse = à votre projet.

11
Don Kirkby

J'ai trouvé que ciso8601 était le moyen le plus rapide d'analyser les horodatages ISO 8601. Comme son nom l'indique, il est implémenté en C.

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

Le GitHub Repo README montre leur vitesse d'accélération> 10x par rapport à toutes les autres bibliothèques répertoriées dans les autres réponses.

Mon projet personnel impliquait beaucoup d'analyses ISO 8601. C'était bien de pouvoir passer l'appel et d'aller 10 fois plus vite. :)

Edit: Je suis depuis devenu un mainteneur de ciso8601. C'est maintenant plus rapide que jamais!

9
movermeyer

Je suis l'auteur d'uto iso8601. On peut le trouver sur GitHub ou sur PyPI . Voici comment vous pouvez analyser votre exemple:

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

Un moyen simple de convertir une chaîne de date de type ISO 8601 en un horodatage UNIX ou en un objet datetime.datetime dans toutes les versions prises en charge Python sans installer de modules tiers consiste à utiliser le analyseur de date). 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)

Sortie:

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
6
Damian Yerrick

J'ai codé un analyseur syntaxique pour la norme ISO 8601 et je l'ai mis sur GitHub: https://github.com/boxed/iso8601 . Cette implémentation prend en charge tout le contenu de la spécification, à l'exception des durées, des intervalles, des intervalles périodiques et des dates en dehors de la plage de dates prise en charge du module datetime de Python.

Les tests sont inclus! : P

6
boxed

La fonction (parse_datetime () de Django prend en charge les dates avec des décalages UTC:

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

Ainsi, il pourrait être utilisé pour analyser les dates ISO 8601 dans les champs de l'ensemble du projet:

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')
6
Artem Vasilev

Cela fonctionne pour stdlib à partir de Python 3.2 (à supposer que tous les horodatages soient au format UTC):

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

Par exemple,

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

Pour quelque chose qui fonctionne avec la bibliothèque standard 2.X, essayez:

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

calendar.timegm est la version gm manquante de time.mktime.

2
Gordon Wrigley

De nos jours, il y a Maya: Datetimes for Humans ™ , de l'auteur du populaire paquetage Requests: HTTP for 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=<UTC>)
2
jrc

Python-dateutil lève une exception si vous analysez des chaînes de date non valides. Vous voudrez peut-être donc capturer l'exception.

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

Merci à grand réponse de Mark Amery J'ai conçu la fonction pour prendre en compte tous les formats ISO possibles 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 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)
    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))
1
omikron
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)

Notez que nous devrions regarder si la chaîne ne se termine pas par Z, nous pourrions analyser en utilisant %z.

0
Denny Weinberg

Au départ, j'ai essayé avec:

from operator import neg, pos
from time import strptime, mktime
from datetime import datetime, tzinfo, timedelta

class MyUTCOffsetTimezone(tzinfo):
    @staticmethod
    def with_offset(offset_no_signal, signal):  # type: (str, str) -> MyUTCOffsetTimezone
        return MyUTCOffsetTimezone((pos if signal == '+' else neg)(
            (datetime.strptime(offset_no_signal, '%H:%M') - datetime(1900, 1, 1))
          .total_seconds()))

    def __init__(self, offset, name=None):
        self.offset = timedelta(seconds=offset)
        self.name = name or self.__class__.__name__

    def utcoffset(self, dt):
        return self.offset

    def tzname(self, dt):
        return self.name

    def dst(self, dt):
        return timedelta(0)


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        dt, sign, offset = strptime(dt[:-6], fmt), dt[-6], dt[-5:]
        return datetime.fromtimestamp(mktime(dt),
                                      tz=MyUTCOffsetTimezone.with_offset(offset, sign))
    Elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

Mais cela n'a pas fonctionné sur les fuseaux horaires négatifs. Ceci cependant, j’ai bien fonctionné, dans Python 3.7.3:

from datetime import datetime


def to_datetime_tz(dt):  # type: (str) -> datetime
    fmt = '%Y-%m-%dT%H:%M:%S.%f'
    if dt[-6] in frozenset(('+', '-')):
        return datetime.strptime(dt, fmt + '%z')
    Elif dt[-1] == 'Z':
        return datetime.strptime(dt, fmt + 'Z')
    return datetime.strptime(dt, fmt)

Certains tests, notez que la sortie ne diffère que par la précision des microsecondes. Vous avez 6 chiffres de précision sur ma machine, mais YMMV:

for dt_in, dt_out in (
        ('2019-03-11T08:00:00.000Z', '2019-03-11T08:00:00'),
        ('2019-03-11T08:00:00.000+11:00', '2019-03-11T08:00:00+11:00'),
        ('2019-03-11T08:00:00.000-11:00', '2019-03-11T08:00:00-11:00')
    ):
    isoformat = to_datetime_tz(dt_in).isoformat()
    assert isoformat == dt_out, '{} != {}'.format(isoformat, dt_out)
0
A T