«Раз!» — ударили часы на колокольной башне,
Те, что шестьдесят минут назад
Пробили полночь.
Фредерик Б. Нидхэм. The Round of the Clock
Хоть я и снялась для календаря, я никогда не приходила вовремя.
Мэрилин Монро
Программисты прилагают удивительное количество усилий, работая с датами и временем. Поговорим о проблемах, с которыми постоянно приходится сталкиваться, а затем рассмотрим лучшие приемы для их решения.
Даты можно представить по-разному — способов не просто много, а слишком много. Даже англоговорящие люди, использующие римский календарь, имеют множество вариантов для написания простой даты:
• July 21 1987;
• 21 Jul 1987;
• 21/7/1987;
• 7/21/1987.
Наряду с другими проблемами, представление даты может быть двусмысленным. В приведенных примерах довольно легко понять, что 7 означает месяц, а 21 — день месяца, хотя бы потому, что у месяца не может быть номера 21. Но что насчет даты 1/6/2012? Мы говорим о 6 января или о 1 июня?
Название месяца в римском календаре изменяется в зависимости от языка. Даже год и месяц могут иметь разные определения в разных культурах.
Время также способно доставить неприятности, особенно из-за часовых поясов и переходов на летнее время. Если вы взглянете на карту часовых поясов, то окажется, что эти пояса больше соответствуют политическим и историческим границам, а не сменяются каждые 15° (360°/24) долготы. Кроме того, разные страны переходят на летнее время и обратно в разные дни года. Страны Южного полушария переводят свои часы вперед, когда страны Северного полушария переводят их назад, и наоборот (если вы немного подумаете, то поймете, почему так происходит).
Стандартная библиотека Python имеет множество модулей для работы с датой и временем: datetime, time, calendar, dateutil и др. Их функции немного пересекаются друг с другом, что иногда приводит к путанице.
Високосные годы — еще одна проблема. Наверняка вы знаете, что каждый четвертый год — високосный (в такие годы проходят летние Олимпийские игры и выборы президента в США). Но знаете ли вы, что каждый сотый год не является високосным, а каждый 400-й — является? Рассмотрим пример кода, в котором проверяется, високосный год или нет:
>>> import calendar
>>> calendar.isleap(1900)
False
>>> calendar.isleap(1996)
True
>>> calendar.isleap(1999)
False
>>> calendar.isleap(2000)
True
>>> calendar.isleap(2002)
False
>>> calendar.isleap(2004)
True
Справка для любопытных:
• год состоит из 365,242 196 дня (после одного оборота вокруг Солнца Земля на четверть оборота вокруг своей оси отстает от места, где начинала движение);
• каждые четыре года добавляется один день. Теперь в среднем каждый год состоит из 365,242 196 – 0,25 = 364,992 196 дня;
• каждые 100 лет один день вычитается. Теперь средний год состоит из 364,992 196 + + 0,01 = 365,002 196 дня;
• каждые 400 лет добавляется один день. Теперь средний год состоит из 365,002 196 – – 0,0025 = 364,999 696 дня.
Получилось достаточно точно! О високосных секундах мы говорить не будем (/).
Модуль стандартной библиотеки datetime позволяет работать с датами и временем (что вполне ожидаемо). В нем определены четыре основных класса объектов, и в каждом содержится множество методов:
•date — для годов, месяцев и дней;
• time — для часов, минут, секунд и долей секунды;
• datetime — для даты и времени одновременно;
•timedelta — для интервалов даты и/или времени.
Вы можете создать объект date, указав год, месяц и день. Эти значения будут доступны как атрибуты:
>>> from datetime import date
>>> halloween = date(2019, 10, 31)
>>> halloween
datetime.date(2019, 10, 31)
>>> halloween.day
31
>>> halloween.month
10
>>> halloween.year
2019
Вы можете вывести на экран содержимое объекта date с помощью его метода isoformat():
>>> halloween.isoformat()
'2019-10-31'
iso в данном контексте ссылается на ISO 8601 — международный стандарт для представления даты и времени. Согласно этому стандарту мы начинаем записывать дату с самого общего элемента (год), а заканчиваем самым точным (день). Вследствие чего есть возможность корректно отсортировать даты: сначала по году, затем по месяцу и по дню. Я обычно выбираю этот формат для представления данных в программах и для имен файлов, которые сохраняют данные по дате. В следующем разделе будут показаны более сложные методы strptime() и strftime() для анализа и форматирования дат.
В этом примере используется метод today() для генерации сегодняшней даты:
>>> from datetime import date
>>> now = date.today()
>>> now
datetime.date(2019, 4, 5)
В следующем примере объект timedelta используется для того, чтобы добавить к объекту date некоторый временной интервал:
>>> from datetime import timedelta
>>> one_day = timedelta(days=1)
>>> tomorrow = now + one_day
>>> tomorrow
datetime.date(2019, 4, 6)
>>> now + 17*one_day
datetime.date(2019, 4, 22)
>>> yesterday = now — one_day
>>> yesterday
datetime.date(2019, 4, 4)
Объект date может иметь значение в диапазоне, начинающемся с date.min(year=1,month=1,day=1) и заканчивающемся date.max(year=9999,month=12,day=31). Таким образом, этот метод неприменим для исторических или астрономических расчетов.
Объект time модуля datetime применяется для представления времени дня:
>>> from datetime import time
>>> noon = time(12, 0, 0)
>>> noon
datetime.time(12, 0)
>>> noon.hour
12
>>> noon.minute
0
>>> noon.second
0
>>> noon.microsecond
0
Порядок аргументов таков: от самой крупной единицы времени (часа) до самой мелкой (микросекунды). Если вы передадите не все аргументы, объект time предположит, что остальные имеют значение 0. Кстати, то, что вы можете сохранять и получать микросекунды, не означает, что вы можете извлекать время из вашего компьютера с точностью до микросекунды. Высокая точность измерений зависит от многих факторов, присущих аппаратному обеспечению и операционной системе.
Объект datetime содержит дату и время дня. Вы можете создать такой объект напрямую, как показано в примере: 2 января, 2014, 3:04 утра, плюс 5 секунд и 6 микросекунд:
>>> from datetime import datetime
>>> some_day = datetime(2019, 1, 2, 3, 4, 5, 6)
>>> some_day
datetime.datetime(2019, 1, 2, 3, 4, 5, 6)
Объект datetime также имеет метод isoformat():
>>> some_day.isoformat()
'2019-01-02T03:04:05.000006'
Буква T, которая находится в середине, разделяет дату и время.
Объект datetime имеет метод now(), с помощью которого можно получить текущие дату и время:
>>> from datetime import datetime
>>> now = datetime.now()
>>> now
datetime.datetime(2019, 4, 5, 19, 53, 7, 580562)
>>> now.year
2019
>>> now.month
4
>>> now.day
5
>>> now.hour
19
>>> now.minute
53
>>> now.second
7
>>> now.microsecond
580562
Объединить объекты date и time в объект datetime можно с помощью метода combine():
>>> from datetime import datetime, time, date
>>> noon = time(12)
>>> this_day = date.today()
>>> noon_today = datetime.combine(this_day, noon)
>>> noon_today
datetime.datetime(2019, 4, 5, 12, 0)
Получить объекты date и time из объекта datetime можно с помощью методов date() и time():
>>> noon_today.date()
datetime.date(2019, 4, 5)
>>> noon_today.time()
datetime.time(12, 0)
В Python есть модуль datetime с объектом time, но есть и отдельный модуль time. Более того, в модуле time имеется функция с каким бы вы думали именем? Правильно, time().
Одним из способов представления абсолютного времени является подсчет количества секунд, прошедших с определенной начальной точки. В Unix считают количество секунд, прошедших с полуночи 1 января 1970 года: это значение часто называют epoch. Как правило, данный способ самый простой для обмена датой и временем между системами.
Функция time() модуля time возвращает текущее время как значение epoch:
>>> import time
>>> now = time.time()
>>> now
1554512132.778233
Если выполнить подсчеты, вы увидите, что прошло более миллиарда секунд после наступления нового, 1970 года. И куда ушло время?
Вы можете преобразовать значение epoch в строку с помощью функции ctime():
>>> time.ctime(now)
'Fri Apr 5 19:55:32 2019'
В следующем разделе вы увидите, как создавать более приятные глазу форматы для даты и времени.
Значения epoch полезны для обмена датой и временем с разными системами, например с JavaScript. Однако иногда бывает нужно получить значения именно дней, часов и т.д., которые объект time предоставляет как объекты struct_time. Функция localtime() предоставляет время в вашем текущем часовом поясе, а функция gmtime() — в UTC:
>>> time.localtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=5, tm_hour=19,
tm_min=55, tm_sec=32, tm_wday=4, tm_yday=95, tm_isdst=1)
>>> time.gmtime(now)
time.struct_time(tm_year=2019, tm_mon=4, tm_mday=6, tm_hour=0,
tm_min=55, tm_sec=32, tm_wday=5, tm_yday=96, tm_isdst=0)
В моем (Центральном) часовом поясе 19:55 — это то же самое, что 00:55 следующего дня в поясе UTC (раньше его называли Гринвичским временем или временем Зулу). Если вы опустите аргумент в функциях localtime() или gmtime(), они предположат, что сконвертировать нужно текущее время.
Некоторые значения tm_... из структуры struct_time выглядят неоднозначно, поэтому обратитесь к табл. 13.1 для получения более подробной информации.
Таблица 13.1. Значения структуры struct_time
Индекс | Имя | Значение | Диапазон |
0 | tm_year | Год | от 0000 до 9999 |
1 | tm_mon | Месяц | от 1 до 12 |
2 | tm_mday | День месяца | от 1 до 31 |
3 | tm_hour | Часы | от 0 до 23 |
4 | tm_min | Минуты | от 0 до 59 |
5 | tm_sec | Секунды | от 0 до 61 |
6 | tm_wday | День недели | от 0 (понедельник) до 6 (воскресенье) |
7 | tm_yday | День года | от 1 до 366 |
8 | tm_isdst | Летнее время? | 0 = нет, 1 = да, –1 = неизвестно |
Если вы не хотите вводить все эти имена, начинающиеся с конструкции tm_, структура struct_time также может играть роль именованного кортежа (см. раздел «Именованные кортежи» на с. 219). Поэтому можно использовать индексы из предыдущей таблицы:
>>> import time
>>> now = time.localtime()
>>> now
time.struct_time(tm_year=2019, tm_mon=6, tm_mday=23, tm_hour=12,
tm_min=12, tm_sec=24, tm_wday=6, tm_yday=174, tm_isdst=1)
>>> now[0]
2019
print(list(now[x] for x in range(9)))
[2019, 6, 23, 12, 12, 24, 6, 174, 1]
Функция mktime() идет в другом направлении и преобразует объект struct_time в секунды epoch:
>>> tm = time.localtime(now)
>>> time.mktime(tm)
1554512132.0
Результат не совсем похож на предыдущее значение epoch, полученное с помощью функции now(), поскольку объект struct_time сохраняет время только до секунд.
Небольшой совет: везде, где возможно, используйте часовой пояс UTC. UTC — это абсолютное время, не зависящее от часовых поясов. Если у вас есть сервер, установите его время в UTC, не привязываясь к местному времени.
Еще один совет: никогда не используйте летнее время, если можно без этого обойтись. Если вы учитываете подобные переходы, в какой-то момент час у вас пропадет (весной), а в другой — наступит дважды (осенью). По разным причинам многие организации пользуются летним временем в своих компьютерных системах, а потом удивляются удвоению и потере данных из-за этого таинственного часа.
Функция Isoformat() — это не единственный способ записывать дату и время. Вы уже видели функцию ctime() в модуле time, которую можете использовать для преобразования времени epoch в строку:
>>> import time
>>> now = time.time()
>>> time.ctime(now)
'Fri Apr 5 19:58:23 2019'
Вы также можете преобразовывать дату и время с помощью функции strftime(). Она предоставляется как метод в объектах datetime, date и time и как функция в модуле time. Функция strftime() использует для вывода информации на экран спецификаторы формата, которые вы можете увидеть в табл. 13.2.
Таблица 13.2. Спецификаторы вывода для strftime()
Спецификатор | Единица даты/времени | Диапазон |
%Y | Год | 1900–… |
%m | Месяц | 01–12 |
%B | Название месяца | Январь… |
%b | Сокращение для месяца | Янв… |
%d | День месяца | 01–31 |
%А | Название дня | Воскресенье… |
А | Сокращение для дня | Вск… |
%Н | Часы (24 часа) | 00–23 |
%I | Часы (12 часов) | 01–12 |
%p | AM или PM | AM, PM |
%M | Минуты | 00–59 |
%S | Секунды | 00–59 |
Слева к числам добавляется ноль.
Рассмотрим пример работы функции strftime(), предоставленной модулем time. Она преобразует объект struct_time в строку. Сначала мы определим строку формата fmt, и потом снова будем ее использовать:
>>> import time
>>> fmt = "It's %A, %B %d, %Y, local time %I:%M:%S%p"
>>> t = time.localtime()
>>> t
time.struct_time(tm_year=2019, tm_mon=3, tm_mday=13, tm_hour=15,
tm_min=23, tm_sec=46, tm_wday=2, tm_yday=72, tm_isdst=1)
>>> time.strftime(fmt, t)
"It's Wednesday, March 13, 2019, local time 03:23:46PM"
Если мы попробуем сделать это с объектом date, функция отработает только для даты. Время будет установлено в полночь:
>>> from datetime import date
>>> some_day = date(2019, 7, 4)
>>> fmt = "It's %B %d, %Y, local time %I:%M:%S%p"
>>> some_day.strftime(fmt)
"It's Friday, July 04, 2019, local time 12:00:00AM"
Для объекта time будут преобразованы только части, касающиеся времени:
>>> from datetime import time
>>> some_time = time(10, 35)
>>> some_time.strftime(fmt)
"It's Monday, January 01, 1900, local time 10:35:00AM"
Очевидно, вам не нужно использовать те части объекта time, которые касаются дней, поскольку они бессмысленны.
Чтобы пойти другим путем и преобразовать строку в дату или время, используйте функцию strptime() с такой же строкой формата. Эта строка работает не так, как регулярные выражения, — части строки, не касающиеся формата (без символа %), должны совпадать точно. Укажем формат год-месяц-день, например 2019-01-29.
Что произойдет, если строка даты, которую вы хотите проанализировать, имеет пробелы вместо дефисов?
>>> import time
>>> fmt = "%Y-%m-%d"
>>> time.strptime("2019 01 29", fmt)
Traceback (most recent call last):
File "<stdin>",
line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/_strptime.py",
line 494, in _strptime_time
tt = _strptime(data_string, format)[0]
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/_strptime.py",
line 337, in _strptime(data_string, format))
ValueError: time data '2019 01 29' does not match format '%Y-%m-%d'
Будет ли довольна функция strptime(), если мы передадим ей несколько дефисов?
>>> time.strptime("2019-01-29", fmt)
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=29, tm_hour=0, tm_min=0,
tm_sec=0, tm_wday=6, tm_yday=29, tm_isdst=-1)
Да.
Даже если строка совпадает с заданным форматом, но одно из значений находится вне диапазона, будет сгенерировано исключение:
>>> time.strptime("2019-13-29", fmt)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/_strptime.py",
line 494, in _strptime_time
tt = _strptime(data_string, format)[0]
File "/Library/Frameworks/Python.framework/Versions/3.3/lib/python3.3/_strptime.py",
line 337, in _strptime(data_string, format))
ValueError: time data '2019-13-29' does not match format '%Y-%m-%d'
Имена соответствуют вашей локали — региональному набору настроек операционной системы. Чтобы вывести на экран другие названия месяцев и дней, измените свою локаль с помощью функции setlocale(): ее первый аргумент — locale.LC_TIME для даты и времени, а второй аргумент — это строка, содержащая сокращенное обозначение языка и страны. Давайте пригласим на вечеринку в честь Дня Всех Святых наших иностранных друзей. Выведем на экран дату (месяц, число и день недели) на английском, французском, немецком, испанском и исландском. (А что? Думаете, исландцы не любят вечеринки? У них даже есть настоящие эльфы.)
>>> import locale
>>> from datetime import date
>>> halloween = date(2014, 10, 31)
>>> for lang_country in ['en_us', 'fr_fr', 'de_de', 'es_es', 'is_is',]:
... locale.setlocale(locale.LC_TIME, lang_country)
... halloween.strftime('%A, %B %d')
...
'en_us'
'Thursday, October 31'
'fr_fr'
'Jeudi, octobre 31'
'de_de'
'Donnerstag, Oktober 31'
'es_es'
'jueves, octubre 31'
'is_is'
'fimmtudagur, október 31'
>>>
Откуда можно взять эти волшебные значения аргумента lang_country? Следующий способ не самый надежный, но вы можете попробовать получить их все сразу (все несколько сотен):
>>> import locale
>>> names = locale.locale_alias.keys()
Из переменной names получим только имена локалей, которые, похоже, будут работать с методом setlocale(): например, те, которые мы использовали в предыдущем примере, — двухсимвольный код языка (), за которым следуют подчеркивание и двухсимвольный код страны ():
>>> good_names = [name for name in names if \
len(name) == 5 and name[2] == '_']
Как будут выглядеть первые пять из них?
>>> good_names[:5]
['sr_cs', 'de_at', 'nl_nl', 'es_ni', 'sp_yu']
Если вы хотите получить все локали для Германии, используйте следующий код:
>>> de = [name for name in good_names if name.startswith('de')]
>>> de
['de_at', 'de_de', 'de_ch', 'de_lu', 'de_be']
Если вы запустите функцию set_locale() и получите ошибку locale.Error: unsupported locale, это будет означать, что установка данной локали не поддерживается вашей операционной системой. Вам потребуется выяснить, что нужно операционной системе для добавления локали. Ошибка может произойти даже в том случае, если Python с помощью функции locale.locale_alias.keys() сообщит вам, что эта локаль хорошая. Я получил ошибку при тестировании на macOS с использованием локали cy_gb (валлийский, Великобритания), даже несмотря на то, что локаль is_is (исландский, Исландия) перед этим была принята.
На рис. 13.1, взятом из вики Python (), обобщены все стандартные преобразования времени, которые можно выполнить в Python.
Рис. 13.1. Преобразования даты и времени
Если вы считаете, что модули стандартной библиотеки только создают путаницу или в них не хватает какого-то конкретного преобразования, можете использовать альтернативные модули от сторонних разработчиков. Рассмотрим некоторые из них:
•arrow (/). Этот модуль содержит множество функций для работы с датой и временем и имеет простой API;
• dateutil (). Модуль может проанализировать любой формат даты и хорошо работает с относительными датами и временем;
• iso8601 (). Этот модуль заполняет пробелы, связанные с работой модулей стандартной библиотеки, когда речь идет о формате ISO 8601;
• fleming (). Модуль содержит множество функций для работы с часовыми поясами;
• maya (). Интуитивный интерфейс для дат, времени и интервалов;
•dateinfer (). Определяет правильные строки формата на основе строк, содержащих дату и время.
Файлы и каталоги тоже хотят внимания.
13.1. Запишите текущие дату и время как строку в текстовый файл today.txt.
13.2. Прочтите текстовый файл today.txt и разместите данные в строке today_string.
13.3. Проанализируйте дату из строки today_string.
13.4. Создайте объект date с датой вашего рождения.
13.5. В какой день недели вы родились?
13.6. Когда вам будет (или уже было) 10 000 дней от роду?
Примерно в это время появилась система Unix, если не обращать внимания на досадные високосные секунды.