Даты и время, вероятно, звучат как простая концепция, пока вам не придется иметь дело с пользователями и данными со всего мира.

Тогда вы понимаете, что это действительно сложно!

TL; DR: шпаргалка

1. Анализировать строки даты

import datetime
import pytz
import dateutil.parser
format = '%Y-%m-%dT%H:%M:%S%z'
datestring = '2016-09-20T16:43:45-07:00'
d = dateutil.parser.parse(datestring)   # python 2.7
d = datetime.datetime.strptime(datestring, format) #3.2+

2. Строка даты ISO-8601 для объекта datetime UTC

import datetime
import pytz
import dateutil.parser
format = '%Y-%m-%dT%H:%M:%S%z'
datestring = '2016-09-20T16:43:45-07:00'
d = dateutil.parser.parse(datestring) # python 2.7
d = d.replace(tzinfo=utc) - d.utcoffset()
>>> datetime.datetime(2016, 9, 20, 23, 43, 45, tzinfo=<UTC>)

3. Отметка времени в формате UTC (сейчас)

import datetime
import pytz
# define epoch, the beginning of times in the UTC timestamp world
epoch = datetime.datetime(1970,1,1,0,0,0)
now = datetime.datetime.utcnow()
timestamp = (now - epoch).total_seconds()
>>> 1505329554.617216
# subtracting datetime objects result in a datetime.timedelta object which can be expressed in seconds.
# this works because both datetime are implicitly in UTC time

4. Отметка времени в формате UTC из простой строки даты.

import datetime
import pytz
# naive datetime 
d = datetime.datetime.strptime('01/12/2011', '%d/%m/%Y')
>>> datetime.datetime(2011, 12, 1, 0, 0)
# add proper timezone for the date
pst = pytz.timezone('America/Los_Angeles')
d = pst.localize(d)
>>> datetime.datetime(2011, 12, 1, 0, 0, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
# convert to UTC timezone 
utc = pytz.UTC 
d = d.astimezone(utc)
>>> datetime.datetime(2011, 12, 1, 8, 0, tzinfo=<UTC>)
# epoch is the beginning of time in the UTC timestamp world
epoch = datetime.datetime(1970,1,1,0,0,0,tzinfo=pytz.UTC)
>>> datetime.datetime(1970, 1, 1, 0, 0, tzinfo=<UTC>)
# get the total second difference
ts = (d - epoch).total_seconds() 
>>> 1322726400.0

5. Добавьте часовой пояс к наивному datetime

import datetime
import pytz
# naive datetime
d = datetime.datetime.strptime('01/12/2011 16:43:45', '%d/%m/%Y %H:%M:%S')
>>> datetime.datetime(2011, 12, 1, 16, 43, 45)
# add proper timezone
pst = pytz.timezone('America/Los_Angeles')
d = pst.localize(d)
>>> datetime.datetime(2011, 12, 1, 16, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)

НЕ используйте datetime.replace для установки часового пояса:

import datetime
import pytz
### DON'T DO THIS, THIS CODE IS WRONG!!!
d = datetime.datetime.utcfromtimestamp(1505325217)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37)
pst = pytz.timezone('America/Los_Angeles')
d = d.replace(tzinfo=pst)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37, tzinfo=<DstTzInfo 'America/Los_Angeles' LMT-1 day, 16:07:00 STD>)

В приведенном выше коде есть две ошибки:

  • datetime.replace заменяет tzinfo: он вообще не преобразует время. (utcfromtimestamp дает datetime в UTC, поэтому изменение tzinfo изменяет представленное время)
  • pytz плохо работает с tzinfo. Замена tzinfo на любое значение, кроме UTC, может привести к непредвиденным последствиям. В приведенном выше примере обратите внимание, как параметр часового пояса PST фактически становится временем LMT (местное среднее время), которое смещено на 16:07:00 от UTC, НЕ 16:00:00.

В итоге получается смещение 16h07m: плохо!

6. datetime в строку ISO 8601

import datetime
import pytz
# naive datetime
d = datetime.datetime.strptime('01/12/2011 16:43:45', '%d/%m/%Y %H:%M:%S')
>>> datetime.datetime(2011, 12, 1, 16, 43, 45)
# add proper timezone
pst = pytz.timezone('America/Los_Angeles')
d = pst.localize(d)
>>> datetime.datetime(2011, 12, 1, 16, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
d.isoformat()
>>> "2011-12-01T16:43:45-08:00"

7. дата и время из метки времени

import datetime
import pytz
d = datetime.datetime.utcfromtimestamp(1505325217)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37)
# add timezone info
d = pytz.UTC.localize(d)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37, tzinfo=<UTC>)

8. преобразовать из UTC в другой часовой пояс

import datetime
import pytz
d = datetime.datetime.utcfromtimestamp(1505325217)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37)
# add timezone info
d = pytz.UTC.localize(d)
>>> datetime.datetime(2017, 9, 13, 17, 53, 37, tzinfo=<UTC>)
pst = pytz.timezone('America/Los_Angeles')
d.astimezone(pst)
>>> datetime.datetime(2017, 9, 13, 10, 53, 37, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)

9. Сложение и вычитание времени с помощью timedelta

Летнее время было 6 ноября 2016 г .:

import datetime
import pytz
d = datetime.datetime(2016, 11, 5, 16, 43, 45) # naive datetime
utc = pytz.UTC
pst = pytz.timezone('America/Los_Angeles')
d = utc.localize(d) # UTC timezone aware
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>)
# add 1 day to UTC date
d = d + datetime.timedelta(days=1)
>>> datetime.datetime(2016, 11, 6, 16, 43, 45, tzinfo=<UTC>)
# now convert to local timezone
d = d.astimezone(pst)
>>> datetime.datetime(2016, 11, 6, 8, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
# daylight saving was respected

НЕ используйте timedelta ни с чем, кроме времени UTC:

import datetime
import pytz
### DON'T USE THIS CODE, THIS CODE IS WRONG !!!
d = datetime.datetime(2016, 11, 5, 16, 43, 45) # naive datetime
utc = pytz.UTC
pst = pytz.timezone('America/Los_Angeles')
d = utc.localize(d) # UTC timezone aware
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>)
# convert d to 'America/Los_Angeles' timezone
d = d.astimezone(pst) 
>>> datetime.datetime(2016, 11, 5, 9, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
# add 1 day to PDT date: DON'T DO THAT
d = d + datetime.timedelta(days=1)
>>> datetime.datetime(2016, 11, 6, 9, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
# daylight saving was not respected, still PDT time, not PST as it should be

Итак, теперь длинная версия:

1. Анализ строк даты и времени

Во-первых, существует так много форматов даты и времени, находитесь ли вы в Северной Америке или Европе, говорите ли вы в 12-часовом или 24-часовом или даже военном времени:

  • «Среда, 21 сентября, 18:30»
  • “09/21/2016 06:30PM”
  • “2016/09/21 18:30”
  • “21/09/2016 18:30”

Разбирать дату и время может быть немного неудобно, но это не самое сложное:

import datetime
format = '%Y-%m-%d %H:%M:%S'
datestring = '2016-09-20 16:43:45'
d = datetime.datetime.strptime(datestring, format)
# Note ISO string format is special. More about that later

То, что мы только что получили, называется наивным датой и временем.

Вы спросите, почему наивный? Потому что это субъективно.

2. Время относительно

Если сейчас в Лондоне 16.43, то в Сан-Франциско через 8 часов будет только 16.43 (16:43). Одна и та же строка даты может представлять разные моменты времени в разных местах.

При наивном datetime нет четкого способа узнать, в какой фактический момент времени произошло событие, и, следовательно, нет четкого способа упорядочить события.

Приходит время в формате UTC

3. Временные метки UTC.

Всемирное координированное время (дословный перевод французского TUC (Temps Universal Coordiné)

Временная метка UTC - это количество секунд с эпохи, то есть 1 января 1970 года в 00:00 по Гринвичу (среднее время по Гринвичу).

эпоха определяется в определенном месте в мире (часовой пояс по Гринвичу), что позволяет представить фактический момент времени.

Примечание: вы можете услышать о времени по Гринвичу, UTC или даже о времени Зулу. Все они представляют время на Гринвичском меридиане, который проходит с севера на юг и пересекает Гринвич в Великобритании.

Отметки времени в формате UTC используются для упорядочивания событий (данных журнала) и расчета разницы во времени с точки зрения прошедшего времени.

Наивный подход к получению метки времени в python может выглядеть примерно так:

# define epoch, the beginning of times in the UTC timestamp world
epoch = datetime.datetime(1970,1,1,0,0,0)
now = datetime.datetime.utcnow()
timestamp = (now - epoch).total_seconds()
# subtracting datetime objects result in a datetime.timedelta object which can be expressed in seconds.

Это работает, потому что метод utcnow по умолчанию возвращает наивный datetime объект во времени в формате UTC, но не делает этого с датами с учетом часового пояса не в формате UTC.

Мы немного увидим, как преобразование заданной даты в метку времени в формате UTC требует немного больше усилий.

Хотя метки времени в формате UTC - отличный способ стандартизировать представление момента времени, одна и та же метка времени в формате UTC преобразуется в разные строки даты в разных местах мира. В конце концов, он определяется по Гринвичу.

Используя временную метку, мы потеряли понятие местоположения и, следовательно, местного времени.

Так одно или другое?

4. Смещение времени, ISO-8601 и местное время

Если мы хотим знать о времени суток события, отправленного пользователем, или нам просто нужно представить время события в местном времени, как в случае большинства пользователей -в приложении, вам нужно будет преобразовать время сервера (которое на самом деле всегда должно быть в формате UTC) в местное время.

Смещение времени - это смещение часового пояса, которое необходимо добавить ко времени UTC для представления местного времени.

ISO-8601 - это стандарт для представления строк даты, включая временные смещения.

Он действительно может представлять строку даты во многих различных форматах, но наиболее часто используемый формат в вычислениях:

2017–09–13T20:58:41+07:00

YYYY-MM-DDTHH:mm:ss+/-HH:mm

В формате синтаксического анализа Python, который становится

'%Y-%m-%dT%H:%M:%S%z'

для синтаксического анализа строки ISO-8601 в python ›3.2 вы можете использовать ту же функцию, что и в 1., но если вы используете python 2.7, вам понадобится dateutil.parser:

import datetime
import pytz
import dateutil.parser
format = '%Y-%m-%dT%H:%M:%S%z'
datestring = '2016-09-20T16:43:45-07:00'
d = dateutil.parser.parse(datestring)   # python 2.7
d = datetime.datetime.strptime(datestring, format) #3.2+

Теперь очень важно понять:

Строка даты ISO-8601 содержит:

  • местное время (часть YYYY-MM-DDTHH:mm:ss)
  • средство для преобразования в UTC (смещение времени, т.е. часть +/-HH:mm),

Однако смещение времени действительно только для этой местной даты / времени.

Да, временные сдвиги не высечены на камне: они фактически меняются из-за перехода на летнее время.

2017–07–13T20:58:41+07:00 или 2017–12–13T20:58:41+08:00 - время для одного и того же места в тихоокеанском часовом поясе, но одно - летом (когда наблюдается переход на летнее время, летнее время), а другое - зимой, когда это не так.

Это означает, что вы не можете использовать только временное смещение для определения местоположения или вычисления временных дельт.

Строку ISO-8601 можно безопасно использовать для перевода местного времени между часовыми поясами, но не следует использовать как есть для манипуляций со временем.

5. Часовые пояса

Компенсации по времени недостаточно. Нам нужна информация о часовом поясе.

Что такое часовой пояс?

Это немного сбивает с толку, потому что я говорил о времени в формате UTC, но UTC не является часовым поясом. Вы, вероятно, слышали о EST, CST, MST или PST, если вы живете в Северной Америке, и о GMT и CET, если вы находитесь в Западной Европе, и вам сказали, что это часовые пояса, но на самом деле это не часовые пояса, они называют представляющий смещение по времени.

Часовой пояс обычно определяется континентом и местоположением, например America/Los_Angeles, Europe/Paris или Africa/Tunis.

Обратите внимание, что в названии нет понятия перехода на летнее время.

  • PST: Pacific Standard Time (GMT-08:00) и PDT: Pacific Daylight Time (GMT-07:00) - это временное смещение, применяемое в America/Los_Angeles часовом поясе в зависимости от времени года.
  • CET: Central European Time (GMT+01:00) и CEST: Central European Summer Time (GMT+02:00) - оба смещения времени, применяемые в Europe/Paris часовом поясе.
  • CET: Central European Time (GMT+01:00) также применяется в Africa/Tunis часовом поясе, но переход на летнее время не выполняется, поэтому CET время используется круглый год.

чтобы иметь дело с часовым поясом, pytz - ваш друг ... в большинстве случаев.

Мы еще не упоминали об этом, но когда мы ранее анализировали ISO-8601, вы могли заметить, что объект datetime выглядит так:

datetime.datetime(2016, 9, 20, 16, 43, 45, tzinfo=tzoffset(None, -25200))

Есть смещение по времени, но нет информации о часовом поясе.

Все, что мы действительно можем с этим сделать, - это преобразовать обратно в формат UTC.

import datetime
import pytz
import dateutil.parser
format = '%Y-%m-%dT%H:%M:%S%z'
datestring = '2016-09-20T16:43:45-07:00'
d = dateutil.parser.parse(datestring)
d = d.replace(tzinfo=pytz.UTC) - d.utcoffset()
>>> datetime.datetime(2016, 9, 20, 23, 43, 45, tzinfo=<UTC>)
# now a timezone aware UTC datetime.

Подождите, мы снова потеряли часовой пояс.

Нет, мы этого не сделали. Для начала у нас никогда не было часового пояса. У нас был зачет. Как видно выше, часовой пояс может иметь несколько смещений, поэтому нет простого способа получить имя часового пояса из смещения.

Имя часового пояса - это то, что вы можете легко получить на клиенте (браузере, в Javascript) и которое вы должны отправить на свой сервер вместе со строкой ISO-8601, если она вам нужна на сервере.

Если вы знаете часовой пояс, в который хотите преобразовать, вы можете:

import datetime
import pytz
import dateutil.parser
format = '%Y-%m-%dT%H:%M:%S%z'
datestring = '2016-09-20T16:43:45-07:00'
d = dateutil.parser.parse(datestring)
# UTC datetime
d = d.replace(tzinfo=pytz.UTC) - d.utcoffset()
pst = pytz.timezone('America/Los_Angeles')
d.astimezone(pst)
>>> datetime.datetime(2016, 9, 20, 16, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)

но не волнуйтесь:

Вы не должны выполнять никаких операций с датой и временем в локализованном времени. Всегда используйте UTC.

Посмотрите на этот пример, где мы находимся за день до перехода на летнее время и хотим добавить один день к объекту datetime:

import datetime
import pytz
### DON'T USE THIS CODE, THIS CODE IS WRONG !!!
d = datetime.datetime(2016, 11, 5, 16, 43, 45) # naive datetime
utc = pytz.UTC
pst = pytz.timezone('America/Los_Angeles')
d = utc.localize(d) # UTC timezone aware
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>)
# convert d to 'America/Los_Angeles' timezone
d = d.astimezone(pst) 
>>> datetime.datetime(2016, 11, 5, 9, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
# add 1 day to PDT date: DON'T DO THAT
d = d + datetime.timedelta(days=1)
>>> datetime.datetime(2016, 11, 6, 9, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PDT-1 day, 17:00:00 DST>)
# daylight saving was not respected, still PDT time, not PST as it should be

вместо этого, используя дату и время в формате UTC для всех операций и конвертируя только в местное время в конце:

import datetime
import pytz
d = datetime.datetime(2016, 11, 5, 16, 43, 45) # naive datetime
utc = pytz.UTC
pst = pytz.timezone('America/Los_Angeles')
d = utc.localize(d) # UTC timezone aware
>>> datetime.datetime(2016, 11, 5, 16, 43, 45, tzinfo=<UTC>)
# add 1 day to UTC date
d = d + datetime.timedelta(days=1)
>>> datetime.datetime(2016, 11, 6, 16, 43, 45, tzinfo=<UTC>)
# now convert to local timezone
d = d.astimezone(pst)
>>> datetime.datetime(2016, 11, 6, 8, 43, 45, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
# daylight saving was respected

7. Преобразование простой даты в метку времени.

Иногда вам нужно иметь дело с наивными датами, что означает, что вам нужно знать часовой пояс, чтобы делать с ними что-нибудь полезное, например преобразовывать в метку времени.

Преобразование даты в метку времени в формате UTC, как упоминалось выше, требует немного больше усилий, чтобы убедиться, что дата находится в формате UTC, чтобы начать с:

# naive datetime 
d = datetime.datetime.strptime('01/12/2011', '%d/%m/%Y')
>>> datetime.datetime(2011, 12, 1, 0, 0)
# add proper timezone for this naive date
pst = pytz.timezone('America/Los_Angeles')
d = pst.localize(d)
>>> datetime.datetime(2011, 12, 1, 0, 0, tzinfo=<DstTzInfo 'America/Los_Angeles' PST-1 day, 16:00:00 STD>)
# convert to UTC timezone 
utc = pytz.UTC 
d = d.astimezone(utc)
>>> datetime.datetime(2011, 12, 1, 8, 0, tzinfo=<UTC>)
# epoch is the beginning of time in the UTC timestamp world
epoch = datetime.datetime(1970,1,1,0,0,0,tzinfo=pytz.UTC)
>>> datetime.datetime(1970, 1, 1, 0, 0, tzinfo=<UTC>)
# get the total second difference
ts = (d - epoch).total_seconds() 
>>> 1322726400.0

Вот так.

Надеюсь, это было полезно.

Если вам нужно запомнить всего несколько вещей, запомните вот что:

  • Всегда используйте datetime часовой пояс в формате UTC на сервере и для любых операций.
  • Используйте строки даты ISO-8601 со смещением для передачи информации о дате между клиентом и сервером.
  • Отправьте название часового пояса от клиента, не пытайтесь угадать.

Удачи!