Как мне разобрать дату в формате ISO 8601?
Мне нужно разобрать строки RFC 3339 как "2008-09-03T20:56:35.450686Z"
в питоне datetime
тип.
я нашел strptime
в стандартной библиотеке Python, но это не очень удобно.
Каков наилучший способ сделать это?
29 ответов
Пакет python-dateutil может анализировать не только строки даты и времени RFC 3339, как в вопросе, но и другие строки даты и времени ISO 8601, которые не соответствуют RFC 3339 (например, те, которые не имеют смещения UTC, или те, которые представляют только свидание).
>>> 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)
Имейте в виду, что dateutil.parser
намеренно взломан: он пытается угадать формат и делает неизбежные предположения (настраиваемые только вручную) в неоднозначных случаях. Так что используйте его ТОЛЬКО, если вам нужно разобрать ввод неизвестного формата, и вы можете терпеть случайные неправильные чтения. (спасибо ivan_pozdeev)
Имя Pypi python-dateutil
не dateutil
(спасибо cod3monk3y):
pip install python-dateutil
Если вы используете Python 3.7, взгляните на этот ответ о datetime.datetime.fromisoformat
,
Новое в Python 3.7+
datetime
стандартная библиотека ввела функцию инвертирования datetime.isoformat()
,
classmethod
datetime.fromisoformat(date_string)
:Вернуть
datetime
соответствуетdate_string
в одном из форматов, испускаемыхdate.isoformat()
а такжеdatetime.isoformat()
,В частности, эта функция поддерживает строки в формате (ах):
YYYY-MM-DD[*HH[:MM[:SS[.mmm[mmm]]]][+HH:MM[:SS[.ffffff]]]]
где
*
может соответствовать любому отдельному символу.Внимание: это не поддерживает разбор произвольных строк ISO 8601 - оно предназначено только как обратная операция
datetime.isoformat()
,
Пример использования:
from datetime import datetime
date = datetime.fromisoformat('2017-01-01T12:30:59.000000')
Обратите внимание, что в Python 2.6+ и Py3K символ%f перехватывает микросекунды.
>>> datetime.datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%fZ")
Смотрите проблему здесь
Несколько ответов здесь предлагают использовать datetime.datetime.strptime
для анализа времени и даты RFC 3339 или ISO 8601 с часовыми поясами, как показано в вопросе:
2008-09-03T20:56:35.450686Z
Это плохая идея.
Предполагая, что вы хотите поддерживать полный формат RFC 3339, включая поддержку смещений UTC, отличных от нуля, код, предлагаемый этими ответами, не работает. Действительно, этоне может работать, потому что синтаксический анализ RFC 3339 с использованием strptime
невозможно. Строки формата, используемые модулем datetime в Python, не могут описать синтаксис RFC 3339.
Проблема в смещениях UTC. Интернет-формат даты / времени RFC 3339 требует, чтобы каждая дата-время включала смещение UTC, и эти смещения могут быть либо Z
(сокращение от "время Зулу") или в+HH:MM
или же -HH:MM
формат, как +05:00
или же -10:30
,
Следовательно, все они являются действительными датами времени RFC 3339:
2008-09-03T20:56:35.450686Z
2008-09-03T20:56:35.450686+05:00
2008-09-03T20:56:35.450686-10:30
Увы, строки формата, используемыеstrptime
а такжеstrftime
не имеют директивы, которая соответствует смещению UTC в формате RFC 3339. Полный список директив, которые они поддерживают, можно найти по адресу https://docs.python.org/3/library/datetime.html, и единственная директива смещения UTC, включенная в список, - %z
:
% г
Смещение UTC в форме + ЧЧММ или -ЧЧММ (пустая строка, если объект наивный).
Пример: (пусто), +0000, -0400, +1030
Это не соответствует формату смещения RFC 3339, и действительно, если мы попытаемся использовать%z
в строке формата и разборе даты RFC 3339 у нас не получится:
>>> 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'
(На самом деле, вышеописанное - это именно то, что вы увидите в Python 3. В Python 2 мы потерпим неудачу по еще более простой причине:strptime
не реализует%z
директива вообще в Python 2.)
Несколько ответов здесь, которые рекомендуют strptime
все работают вокруг этого, включая буквальное Z
в их строке формата, которая соответствует Z
из строки datetime примера спрашивающего (и отбрасывает ее, производя datetime
объект без часового пояса):
>>> datetime.strptime("2008-09-03T20:56:35.450686Z", "%Y-%m-%dT%H:%M:%S.%fZ")
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686)
Так как при этом отбрасывается информация о часовом поясе, которая была включена в исходную строку даты и времени, сомнительно, должны ли мы рассматривать даже этот результат как правильный. Но что еще более важно, поскольку этот подход предусматривает жесткое кодирование определенного смещения UTC в строку формата, он будет подавлен в тот момент, когда попытается проанализировать дату / время RFC 3339 с другим смещением UTC:
>>> 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'
Если вы не уверены, что вам нужно только поддерживать время RFC 3339 по времени Зулу, а не время с другими смещениями часового пояса, не используйте strptime
, Вместо этого используйте один из многих других подходов, описанных в ответах.
Попробуйте модуль iso8601; это делает именно это.
Есть несколько других опций, упомянутых на странице WorkingWithTime вики-сайта python.org.
Простой вариант из одного из комментариев: заменить 'Z'
с участием '+00:00'
- и используйте Python3.7+ fromisoformat
.
from datetime import datetime
s = "2008-09-03T20:56:35.450686Z"
datetime.fromisoformat(s.replace('Z', '+00:00'))
# datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=datetime.timezone.utc)
Несмотря на то что strptime
может разобрать 'Z'
персонаж, fromisoformat
быстрее на ~ x40 (см. также: Более быстрое время strptime):
%timeit datetime.fromisoformat(s.replace('Z', '+00:00'))
387 ns ± 20.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit datetime.strptime(s, '%Y-%m-%dT%H:%M:%S.%f%z')
15.3 µs ± 540 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Начиная с Python 3.7, strptime поддерживает разделители двоеточий в смещениях UTC ( источник). Таким образом, вы можете использовать:
import datetime
datetime.datetime.strptime('2018-01-31T09:24:31.488670+00:00', '%Y-%m-%dT%H:%M:%S.%f%z')
Какую именно ошибку вы получаете? Это похоже на следующее?
>>> 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
Если да, вы можете разделить вашу входную строку на ".", А затем добавить микросекунды к полученному вами времени.
Попробуй это:
>>> 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)
импорт ре, дата и время s="2008-09-03T20:56:35.450686Z" d=datetime.datetime(*map(int, re.split('[^\d]', s)[:-1]))
В наши дни Arrow также можно использовать как стороннее решение:
>>> import arrow
>>> date = arrow.get("2008-09-03T20:56:35.450686Z")
>>> date.datetime
datetime.datetime(2008, 9, 3, 20, 56, 35, 450686, tzinfo=tzutc())
Это намного проще, чем вы все это делаете.
Если вы хотите получить секунды с начала эпохи, вы можете использовать python-dateutil, чтобы преобразовать его в объект datetime, а затем преобразовать его в секунды, используя метод strftime. Вот так:
>>> import dateutil.parser as dp
>>> t = '1984-06-02T19:05:00.000Z'
>>> parsed_t = dp.parse(t)
>>> t_in_seconds = parsed_t.strftime('%s')
>>> t_in_seconds
'455051100'
Примечание: это преобразует данные datetime
в эпоху времени. Но вы можете использовать strftime()
функция для преобразования этого datetime
в любой формат. parsed_t
объект здесь имеет тип datetime
с этой точки зрения.
Я обнаружил, что ciso8601 - это самый быстрый способ анализа временных меток ISO 8601. Как следует из названия, он реализован на C.
import ciso8601
ciso8601.parse_datetime('2014-01-09T21:48:00.921000+05:30')
GitHub Repo README демонстрирует ускорение в 10 раз по сравнению со всеми остальными библиотеками, перечисленными в других ответах.
Мой личный проект включал много разбора ISO 8601. Было приятно иметь возможность просто переключать вызов и идти в 10 раз быстрее.:)
Изменить: с тех пор я стал сопровождающим ciso8601. Теперь быстрее, чем когда-либо!
Если вы работаете с Django, он предоставляет модуль dateparse, который принимает множество форматов, похожих на формат ISO, включая часовой пояс.
Если вы не используете Django и не хотите использовать одну из других библиотек, упомянутых здесь, вы, вероятно, можете адаптировать исходный код Django для dateparse для вашего проекта.
Если вы не хотите использовать dateutil, вы можете попробовать эту функцию:
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)
Тестовое задание:
from_utc("2007-03-04T21:08:12.123Z")
Результат:
datetime.datetime(2007, 3, 4, 21, 8, 12, 123000)
Это работает для stdlib на Python 3.2 и более поздних версиях (при условии, что все метки времени указаны в формате UTC):
from datetime import datetime, timezone, timedelta
datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
tzinfo=timezone(timedelta(0)))
Например,
>>> datetime.utcnow().replace(tzinfo=timezone(timedelta(0)))
... datetime.datetime(2015, 3, 11, 6, 2, 47, 879129, tzinfo=datetime.timezone.utc)
Я написал парсер для стандарта ISO 8601 и разместил его на GitHub: https://github.com/boxed/iso8601. Эта реализация поддерживает все в спецификации, кроме длительностей, интервалов, периодических интервалов и дат вне поддерживаемого диапазона дат модуля Python datetime.
Тесты включены!:П
Другой способ заключается в использовании специализированного парсер для ISO-8601 является использование isoparse функции dateutil парсер:
from dateutil import parser
date = parser.isoparse("2008-09-03T20:56:35.450686+01:00")
print(date)
Выход:
2008-09-03 20:56:35.450686+01:00
Эта функция также упоминается в документации к стандартной функции Python datetime.fromisoformat:
Более полнофункциональный анализатор ISO 8601, dateutil.parser.isoparse, доступен в стороннем пакете dateutil.
Функция parse_datetime() в Django поддерживает даты со смещением UTC:
parse_datetime('2016-08-09T15:12:03.65478Z') =
datetime.datetime(2016, 8, 9, 15, 12, 3, 654780, tzinfo=<UTC>)
Таким образом, его можно использовать для разбора дат ISO 8601 в полях всего проекта:
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')
Один простой способ преобразовать строку даты, подобную ISO 8601, в метку времени UNIX или datetime.datetime
Объект во всех поддерживаемых версиях Python без установки сторонних модулей заключается в использовании парсера дат 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)
Выход:
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
Если все равно используется, могу порекомендовать Timestamp
из
pandas
. Там вы можете
ts_1 = pd.Timestamp('2020-02-18T04:27:58.000Z')
ts_2 = pd.Timestamp('2020-02-18T04:27:58.000')
Рэнт: Просто невероятно, что нам все еще нужно беспокоиться о таких вещах, как синтаксический анализ строки даты в 2021 году.
Поскольку ISO 8601 допускает множество вариантов необязательных двоеточий и тире, в основном CCYY-MM-DDThh:mm:ss[Z|(+|-)hh:mm]
, Если вы хотите использовать strptime, вы должны сначала удалить эти варианты.
Цель состоит в том, чтобы сгенерировать объект utc datetime.
Если вы просто хотите базовый случай, который работает для UTC с суффиксом Z, как
2016-06-29T19:36:29.3453Z
:datetime.datetime.strptime(timestamp.translate(None, ':-'), "%Y%m%dT%H%M%S.%fZ")
Если вы хотите обрабатывать смещения часового пояса, такие как
2016-06-29T19:36:29.3453-0400
или же 2008-09-03T20:56:35.450686+05:00
используйте следующее. Они преобразуют все варианты во что-то без разделителей переменных, таких как 20080903T205635.450686+0500
делая его более последовательным / легче разобрать.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" )
Если ваша система не поддерживает
%z
директива strptime (вы видите что-то вроде ValueError: 'z' is a bad directive in format '%Y%m%dT%H%M%S.%f%z'
) тогда нужно вручную сместить время от Z
(УНИВЕРСАЛЬНОЕ ГЛОБАЛЬНОЕ ВРЕМЯ). Заметка %z
может не работать в вашей системе в версиях Python < 3, так как это зависит от поддержки библиотеки c, которая варьируется в зависимости от типа сборки системы /python (например, Jython, Cython и т. д.).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
В настоящее время существует Maya: Datetimes for Humans ™, от автора популярного пакета 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.X, попробуйте:
calendar.timegm(time.strptime(date.split(".")[0]+"UTC", "%Y-%m-%dT%H:%M:%S%Z"))
calendar.timegm - это отсутствующая версия gm time.mktime.
datetime.fromisoformat()
улучшен в Python 3.11 для анализа большинства форматов ISO 8601
datetime.fromisoformat() теперь можно использовать для анализа большинства форматов ISO 8601, за исключением только тех, которые поддерживают дробные часы и минуты. Ранее этот метод поддерживал только форматы, которые могли быть созданы с помощью datetime.isoformat().
>>> from datetime import datetime
>>> datetime.fromisoformat('2011-11-04T00:05:23Z')
datetime.datetime(2011, 11, 4, 0, 5, 23, tzinfo=datetime.timezone.utc)
>>> datetime.fromisoformat('20111104T000523')
datetime.datetime(2011, 11, 4, 0, 5, 23)
>>> datetime.fromisoformat('2011-W01-2T00:05:23.283')
datetime.datetime(2011, 1, 4, 0, 5, 23, 283000)
Python-dateutil будет генерировать исключение при разборе неверных строк даты, поэтому вы можете захотеть перехватить исключение.
from dateutil import parser
ds = '2012-60-31'
try:
dt = parser.parse(ds)
except ValueError, e:
print '"%s" is an invalid date' % ds
Благодаря великолепному ответу Марка Эмери я разработал функцию учета всех возможных форматов ISO даты и времени:
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))
Первоначально я пробовал:
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)
Но это не сработало в отрицательных часовых поясах. Однако в 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)
Некоторые тесты отмечают, что выход отличается только точностью до микросекунд. На моей машине 6 цифр точности, но 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)
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)
Обратите внимание, что мы должны посмотреть, не заканчивается ли строка Z
мы могли бы разобрать с помощью %z
,