Простой Python — страница 38 из 66

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

Даты могут быть представлены множеством способов — их даже слишком много. Даже англоговорящие люди, использующие римский календарь, применяют множество вариантов представления простой даты:

• July 29 1984;

• 29 Jul 1984;

• 29/7/1984;

• 7/29/1984.

Помимо других проблем, представление даты может быть двусмысленным. В предыдущих примерах довольно легко определить, что 7 означает месяц, а 29 — день месяца, в основном потому что у месяца не может быть номера 29. Но как насчет даты 1/6/2012? Мы говорим о 6 января или 1 июня?

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

Високосные годы — это еще одна проблема. Вы, возможно, знаете, что каждый четвертый год является високосным (в этом году проходят летняя олимпиада и выборы президента в Америке). Знаете ли вы, что каждый сотый год не является високосным, а каждый 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

Работа с временем также может доставить неприятности, особенно из-за часовых поясов и перехода на летнее время. Если вы взглянете на карту часовых поясов, то окажется, что эти пояса больше соответствуют политическим и историческим границам, вместо того чтобы сменяться каждые 15° (360°/24) долготы. Кроме того, разные страны переходят на летнее время и обратно в разные дни года. Фактически страны южного полушария переводят свои часы вперед, когда страны северного полушария переводят их назад, и наоборот (если вы немного задумаетесь, то поймете, почему так происходит).

Стандартная библиотека Python имеет множество модулей для работы с датой и временем: datetime, time, calendar, dateutil и др. Их функции немного пересекаются друг с другом, и это может создать путаницу.

Модуль datetime

Начнем с рассмотрения стандартного модуля datetime. В нем определены четыре основных объекта, каждый из которых содержит множество методов:

• date для годов, месяцев и дней;

• time для часов, минут, секунд и долей секунды;

• datetime для даты и времени одновременно;

• timedelta для интервалов даты и/или времени.

Вы можете создать объект date, указав год, месяц и день. Эти значения будут доступны как атрибуты:

>>> from datetime import date

>>> halloween = date(2014, 10, 31)

>>> halloween

datetime.date(2014, 10, 31)

>>> halloween.day

31

>>> halloween.month

10

>>> halloween.year

2014

Вы можете вывести на экран содержимое объекта date с помощью его метода isoformat():

>>> halloween.isoformat()

'2014-10-31'

iso в данном контексте ссылается на ISO 8601 — международный стандарт для представления даты и времени. В этом формате мы записываем дату, начиная с самого общего элемента (год) и заканчивая самым точным (день). С его помощью можно также корректно отсортировать даты: сначала по году, затем по месяцу, затем по дню. Я обычно выбираю этот формат для представления данных в программах и для имен файлов, которые сохраняют данные по дате. В следующем разделе будут показаны более сложные методы strptime() и strftime() для анализа и форматирования дат.

В этом примере метод today() используется для генерации сегодняшней даты:

>>> from datetime import date

>>> now = date.today()

>>> now

datetime.date(2014, 2, 2)

В следующем примере объект timedelta используется для того, чтобы добавить к объекту date некоторый временной интервал:

>>> from datetime import timedelta

>>> one_day = timedelta(days=1)

>>> tomorrow = now + one_day

>>> tomorrow

datetime.date(2014, 2, 3)

>>> now + 17*one_day

datetime.date(2014, 2, 19)

>>> yesterday = now — one_day

>>> yesterday

datetime.date(2014, 2, 1)

Объект 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(2014, 1, 2, 3, 4, 5, 6)

>>> some_day

datetime.datetime(2014, 1, 2, 3, 4, 5, 6)

Объект datetime также имеет метод isoformat():

>>> some_day.isoformat()

'2014-01-02T03:04:05.000006'

Буква T, которая находится в середине, разделяет дату и время.

Объект datetime имеет метод now(), с помощью которого вы можете получить текущие дату и время:

>>> from datetime import datetime

>>> now = datetime.now()

>>> now

datetime.datetime(2014, 2, 2, 23, 15, 34, 694988)

14

>>> now.month

2

>>> now.day

2

>>> now.hour

23

>>> now.minute

15

>>> now.second

34

>>> now.microsecond

694988

Вы можете объединить объекты 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(2014, 2, 2, 12, 0)

Вы можете получить объекты date и time из объекта datetime с помощью методов date() и time():

>>> noon_today.date()

datetime.date(2014, 2, 2)

>>> noon_today.time()

datetime.time(12, 0)

Модуль time

В Python имеется модуль datetime, имеющий объект time, а также отдельный модуль time, что создает путаницу. Дальше — больше, в модуле time имеется функция с именем — что вы подумали? — time().

Одним из способов представления абсолютного времени является подсчет количества секунд, прошедших с некоторой стартовой точки. В Unix используется количество секунд, прошедших с полуночи 1 января 1970 года (примерно в это время появилась система Unix). Это значение часто называют epoch, и зачастую оно является простейшим способом обмениваться датой и временем между системами.

Функция time() модуля time возвращает текущее время как значение epoch:

>>> import time

>>> now = time.time()

>>> now

1391488263.664645

Если выполнить подсчеты, вы увидите, что прошло более миллиарда секунд после наступления нового, 1970 года. И куда ушло время?

Вы можете преобразовать значение epoch в строку с помощью функции ctime():

>>> time.ctime(now)

'Mon Feb··3 22:31:03 2014'

В следующем разделе вы увидите, как создавать более приятные глазу форматы для даты и времени.

Значения epoch полезны для обмена датой и временем с разными системами вроде JavaScript. Однако иногда вам нужно получить именно значения дней, часов и т. д., объект time предоставляет их как объекты struct_time. Функция localtime() предоставляет время в вашем текущем часовом поясе, а функция gmtime() — в UTC:

>>> time.localtime(now)

time.struct_time(tm_year=2014, tm_mon=2, tm_mday=3, tm_hour=22, tm_min=31,

tm_sec=3, tm_wday=0, tm_yday=34, tm_isdst=0)

>>> time.gmtime(now)

time.struct_time(tm_year=2014, tm_mon=2, tm_mday=4, tm_hour=4, tm_min=31,

tm_sec=3, tm_wday=1, tm_yday=35, tm_isdst=0)

В моем (Центральном) часовом поясе 22:31 — это то же самое, что 04:31 следующего дня в поясе UTC (раньше его называли Гринвичским временем или временем Зулу). Если вы опустите аргумент функции localtime() или gmtime(), они предположат, что сконвертировать нужно текущее время.

Их противоположностью является функция mktime(), которая преобразует объект struct_time в секунды epoch:

>>> tm = time.localtime(now)

>>> time.mktime(tm)

1391488263.0

Результат не совсем похож на предыдущее значение epoch, полученное с помощью функции now(), поскольку объект struct_time сохраняет время лишь до секунд.

Небольшой совет: везде, где возможно, используйте часовой пояс UTC. UTC — это абсолютное время, не зависящее от часовых поясов. Если у вас есть сервер, установите его время согласно часовому поясу UTC, не используйте местное время.

Еще один совет (совершенно бесплатный): никогда не используйте летнее время, если можете этого избежать. Если вы используете летнее время, в одно время года час пропадет (весной вперед), а в другое — наступит дважды (осенью обратно). По каким-то причинам многие организации пользуются летним временем в своих компьютерных системах, а потом удивляются удвоению и потере данных. Заканчивается все печально.


Помните своих друзей — UTC для времени и UTF-8 для строк (о UTF-8 подробнее можно прочитать в главе 7).

Читаем и записываем дату и время

Функция Isoformat() — это не единственный способ записывать дату и время. Вы уже видели функцию ctime() в модуле time, которую можете использовать для преобразования времени epoch в строку:

>>> import time

>>> now = time.time()

>>> time.ctime(now)

'Mon Feb··3 21:14:36 2014'

Вы также можете преобразовывать дату и время с помощью функции strftime(). Она предоставляется как метод в объектах datetime, date и time objects и как функция в модуле time. strftime() использует для вывода информации на экран спецификаторы формата, которые вы можете увидеть в табл. 10.1.


Таблица 10.1. Спецификаторы вывода для strftime()
СпецификаторЕдиница даты/времениДиапазон
%YГод1900–…
%mМесяц01–12
%BНазвание месяцаЯнварь, …
%bСокращение для месяцаЯнв, …
%dДень месяца01–31
Название дняВоскресенье, …
аСокращение для дняВск, …
Часы (24 часа)00–23
%IЧасы (12 часов)01–12
%pAM или PMAM, 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=2014, tm_mon=2, tm_mday=4, tm_hour=19,

tm_min=28, tm_sec=38, tm_wday=1, tm_yday=35, tm_isdst=0)

>>> time.strftime(fmt, t)

"It's Tuesday, February 04, 2014, local time 07:28:38PM"

Если мы попробуем сделать это с объектом date, функция отработает только для даты, время будет установлено в полночь:

>>> from datetime import date

>>> some_day = date(2014, 7, 4)

>>> fmt = "It's %B %d, %Y, local time %I:%M:%S%p"

>>> some_day.strftime(fmt)

"It's Friday, July 04, 2014, 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() с такой же строкой формата. Эта строка работает не так, как регулярные выражения, — части строки, не касающиеся формата (без символа %), должны совпадать точно. Укажем формат «год-месяц-день» вроде 2012-01-29. Что произойдет, если строка даты, которую вы хотите проанализировать, имеет пробелы вместо дефисов?

>>> import time

>>> fmt = "%Y-%m-%d"

>>> time.strptime("2012 01 29", fmt)

Traceback (most recent call last):

··File "", line 1, in 

··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 '2012 01 29' does not match format '%Y-%m-%d'

Будет ли довольна функция strptime(), если мы передадим ей несколько дефисов?

>>> time.strptime("2012-01-29", fmt)

time.struct_time(tm_year=2012, 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("2012-13-29", fmt)

Traceback (most recent call last):

··File "", line 1, in 

··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 '2012-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'

'Friday, October 31'

'fr_fr'

'Vendredi, octobre 31'

'de_de'

'Freitag, Oktober 31'

'es_es'

'viernes, octubre 31'

'is_is'

'föstudagur, október 31'

>>>

Откуда можно взять эти волшебные значения аргумента lang_country? Это немного ненадежно, но вы можете получить их все сразу (всего их несколько сотен):

>>> import locale

>>> names = locale.locale_alias.keys()

Из переменной names получим только те имена локалей, которые будут работать с методом setlocale(), вроде тех, что мы использовали в предыдущем примере, — двухсимвольный код языка (http://bit.ly/iso-639-1), в котором после подчеркивания следует двухсимвольный код страны (http://bit.ly/iso-3166-1):

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

Альтернативные модули

Если вы считаете, что модули стандартной библиотеки только создают путаницу или им не хватает некоторого определенного преобразования, вы можете использовать альтернативные модули от сторонних разработчиков. Рассмотрим несколько из них.

• arrow (http://crsmithdev.com/arrow/). Этот модуль содержит множество функций для работы с датой и временем и имеет простой API.

• dateutil (http://labix.org/python-dateutil). Модуль может проанализировать любой формат даты и хорошо работает с относительными датами и временем.

• iso8601 (https://pypi.python.org/pypi/iso8601). Этот модуль заполняет пробелы, связанные с работой модулей стандартной библиотеки, когда речь идет о формате ISO 8601.

• fleming (https://github.com/ambitioninc/fleming). Модуль содержит множество функций для работы с часовыми поясами.

Упражнения