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?
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
_.
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
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.
La bibliothèque standard datetime
a introduit une fonction permettant d'inverser datetime.isoformat()
.
classmethod
datetime.fromisoformat(date_string)
:Retourne un
datetime
correspondant à undate_string
dans l'un des formats émis pardate.isoformat()
etdatetime.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]]]]
où
*
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')
Essayez le module iso8601 ; il fait exactement cela.
Plusieurs autres options sont mentionnées sur la page WorkingWithTime du wiki de python.org.
import re, datetime s = "2008-09-03T20: 56: 35.450686Z" d = datetime.datetime (* map (int, re.split ('[ ^\d] ', s) [: - 1]))
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)
À 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')
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())
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)
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())
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.
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!
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)
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
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
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')
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)
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.
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>)
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
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))
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
.
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)