viernes, enero 16, 2015

Gestión de fecha y hora en Python

Uno de los puntos débiles en la programación es la gestión de las fechas y las horas. En general, tendemos a creer que, "como nuestra aplicación sólo se va a ejecutar en un mismo ordenador, no habrá problemas por la fecha-hora". Sin embargo, incluso en ese caso más básico, multitud de problemas pueden surgir el día del cambio de hora (por el cambio de / hacia hora de verano).
Además de contolar el calendario Gregoriano, otros conceptos hay que manejar:
  • Las zonas horarias, sus nombres y como afectan a la hora
  • UTC Zona horaria de referencia. Se correspondería con la zona GMT de Reino unido, o de Greenwich
  • DST u hora de verano, que sería la hora modificada en periodo veraniego para que haya más luz a la hora de trabajar. En España se corresponde con adelantar la hora en marzo y volverla a atrasar en octubre.
Biblioteca estándar
La biblioteca estándar de Python viene con diferentes módulos para gestionar fechas y horas aunque, luego veremos, no es tan completa como uno pudiera esperar en algunos aspectos.
La funcionalidad principal esta en los módulos time y datetime.
El módulo time permite manejar la hora "estilo unix", esto es, como un número de segundos desde el 1 de enero de 1970. A partir de ahí, se puede obtener la hora actual, se puede imprimir de forma textual la fecha,...
La hora que maneja es como un número, que puede tener decimales, o la que llama struct_time, que es una tupla de 9 elementos: año, mes (1-12), día, hora, minutos, segundos, día de la semana (0-6 lunes - domingo), día del año, es DST (0 o 1 para indicar no / si, y -1 para indicar modo automático).
Para obtener la hora actual:
  • localtime() --> struct_time
  • time() --> milisegundos desde 1970
Por otro lado, un módulo más usable es datetime. Compuesto por las clases:
  • time para almacenar una hora del día
  • date para manejar fechas
  • datetime maneja el par fecha-hora junto
  • timedelta para indicar una diferencia de tiempo
  • tzinfo es una clase abstracta para manejar zonas horarias, pero que con la biblioteca básica sólo disponemos de la clase correspondiente a horas UTC.
Para obtener la hora local actual, se llama al método estático datetime.now(). Si queremos la hora en UTC, sería con datetime.utcnow(). En ambos casos va sin información de zona horaria. A partir del datetime podemos obtener el objeto time, que solo lleva la hora.
La fecha actual se obtiene como el método estático date.today()

Conversiones

Según la funcionalidad que deseemos conseguir en cada caso, será más interesante usar un tipo u otro, por lo que tendremos que poder convertir formatos de uno a otro.
  • datetime.timetuple() --> struct_time . Para obtener la tupla de 9 dígitos a partir de un objeto datetime
  • estático datetime.fromtimestamp(número) --> datetime . Para obtener un objeto datetime a partir de un número
  • time.mktime(struct_time) --> número . Para pasar de tupla a número
  • static datetime.combine(date, time) --> datetime . Junta la información de una fecha y una hora, para tener un datetime completo
  • datetime.date() --> Obtener sólo la fecha
  • datetime.time() --> Obtener sólo la hora
Conversión a texto
Al final de las manipulaciones de fechas y horas, en muchos casos hay que recibir / mostrar la fecha al usuario, a través de una cadena de texto. Para ello existen las funciones / métodos strftime y strptime. Ambas manejas una cadena de texto que indica el formato en que se espera la fecha-hora.

import datetime
import locale

formato_local = "%x %X"
formato_elaborado = "%Y %B %d %A %I:%M"
ahora = datetime.datetime.now()
#Italiano en Windows. 
locale.setlocale(locale.LC_ALL, "ita")
#Salida: '17/12/2014 13.20.35'
print(ahora.strftime(formato_local))
#Salida: '2014 dicembre 17 mercoledì 01:20'
print(ahora.strftime(formato_elaborado))
#Español en Windows
locale.setlocale(locale.LC_ALL, "esp")
#Español en Linux
locale.setlocale(locale.LC_ALL, "es_ES.UTF-8")
#Salida: '17/12/2014 13:20:35'
print(ahora.strftime(formato_local))
#Salida: '2014 diciembre 17 miércoles 01:20'
print(ahora.strftime(formato_elaborado))

Mediante strftime, tanto del módulo time, como del módulo datatime, se convierte de fecha-hora a texto.
Con strptime se interpreta una cadena de texto que representa una fecha, para obtener la fecha en formato interno, manejable.
Los campos que se pueden interpretar son:
%a Día de la semana abreviado usando localización 
%A Día de la semana usando localización
%b Nombre del mes abreviado, usando localización 
%B Nombre del mes, usando localización 
%c Fecha y hora en la representación que indica la localización
%d Día del mes [01,31].  
%f Microsegundos como número decimal [0,999999], relleno de ceros por la izquierda para mantener longitud constante
%H Hora en formato 24 horas [00,23].  
%I Hora en formato 12 horas [01,12].  
%j Día del año [001,366].  
%m Mes del año [01,12].  
%M Minutos [00,59].  
%p AM / PM en formato localizado
%S Segundos dentro del minuto [00,61].
%U Número de semana del año [00, 53] (suponiendo que el domingo sea el primer día de la semana).
%w Día de la semana como número [0 (Domingo),6].  
%W Número de semana del año [00, 53] (suponiendo que el lunes sea el primer día de la semana).
%x Fecha en formato localizado 
%X Hora en formato localizado
%y Año como 2 cifras (decenas y unidades) [00,99].  
%Y Año con todas sus cifras 
%z Diferencia horaria con UTC en formato +HHMM o -HHMM
%Z Nombre de la zona horaria 
%% Para incluir el carácter '%' literalmente.
Como vemos, hay campos para todo, en formato numérico y textual localizado.
Los formatos textuales localizados emplean utilidades del sistema operativo o de módulo locale correctamente configurado, para manejar la información como se ha configurado para un idioma - lugar.
Por ejemplo, para el formato empleado en el ejemplo anterior,"%Y %B %d %A %I:%M":
  • %Y Año con todas sus cifras. Ej: 2015
  • %B Nombre del mes como texto. Por ejemplo, en España podría ser Enero, en Reino Unido podría ser January
  • %d Día del mes
  • %A Día de la semana, como texto. Por ejemplo, podría ser Domingo o Lunes, y en Reino Unido sería Sunday, Monday.
  • %I:%M  Horas en formato 12 horas y minutos. Luego mostraría cosas como 08:34 , pero no mostraría 15:43, ya que eso sería 24 horas.
Información de zona horaria en Python 2
A partir del módulo time, se puede acceder tanto al nombre de la zona horaria normal, como de la zona horaria durante el horario de verano, así como las diferencias horarias en ambos casos:
import time

#Tupla con el nombre de la zona horaria normal, y en DST
zonas_horarias_locales = time.tzname
print(zonas_horarias_locales)
# 0 / 1 si esta definida zona para DST
hay_zona_DST = time.daylight
print(hay_zona_DST)
#Diferencia horaria con UTC en segundos
dif_UTC = time.timezone
print(dif_UTC)
#Diferencia horaria con UTC en segundos durante DST
dif_UTC_en_DST = time.altzone
print(dif_UTC_en_DST)

Información de zona horaria en Python 3
Igualmente, a partir del módulo time se puede acceder a la información de la zona horaria, pero de forma ligeramente distinta:
import time

#Hora local
hora_local = time.localtime()
print(hora_local)
#Abreviatura de la Zona horaria
zona_horaria = hora_local.tm_zone # Ej: 'CET'
print(zona_horaria)
#Diferencia con hora UTC
dif_horaria = hora_local.tm_gmtoff # Ej: 3600
print(dif_horaria)
if time.daylight!=0:#Esta definido DST
    print(time.altzone)


Aritmética de fechas
En ocasiones hay que sumar un día a una fecha, o sumar unas cuantas horas,... y no es plan llenar nuestro código de sentencias "if" encadenadas. Para ayudarnos podemos usar la clase timedelta que, junto con las operaciones definidas para las clases de datetime, nos permite lograr lo que queremos:

from __future__ import print_function
import datetime

ahora = datetime.date.today()
#Ahora: 2015-01-04
print("Ahora: ", ahora)
delta = datetime.timedelta(days=1)
#delta =  1 day, 0:00:00
print("delta = ", delta)
ahora_mas_1_dia = ahora + delta
#Ahora + delta =  2015-01-05
print("Ahora + delta = ", ahora_mas_1_dia)

En el ejemplo superior, vemos que se puede crear un objeto timedelta con la diferencia de tiempo que queramos emplear (se pueden pasar como parámetros con nombre cosas como días, horas,... ), y luego basta con realizar la suma / resta necesaria, y así tendrá en cuenta cosas como años bisiestos, meses con más o menos días,...

Biblioteca pytz y tzlocal
Zonas horarias
Zonas horarias


Hasta ahora hemos visto como gestionar las fecha-hora con la biblioteca estándar, pero hay 2 bibliotecas extras que nos pueden ayudar mucho para el caso de tener que lidiar con diferentes zonas horarias, pytz y tzlocal:
  • pytz tiene una base de datos con las zonas horarias conocidas. Tanto su nombre, como su comportamiento. Se puede instalar con un simple: pip install pytz
  • tzlocal permite conocer la zona horaria que tiene configurada el sistema local donde se ejecuta la aplicación. Se puede instalar con un: pip install tzlocal
Por un lado, tenemos la zona horaria de referencia, pytz.utc, que nos da un objeto tzinfo, y por otro, a partir de pytz.timezone("nombre estandar zona horaria") se puede obtener un objeto tzinfo sobre la zona horaria indicada.

import pytz
import datetime

zona_UTC = pytz.utc
madrid = pytz.timezone("Europe/Madrid")
eastern = pytz.timezone("US/Eastern")

ahora = datetime.datetime.now()
#Nos da un datetime con tzinfo la zona de Madrid
ahora_madrid = madrid.localize(ahora)
print(ahora_madrid)
#Pero la hora en USA, costa Este
costa_este = ahora_madrid.astimezone(eastern) 
print(costa_este)

#Conocer las zonas horarias para un pais
print(repr(pytz.country_timezones("ES"))) 
#[u'Europe/Madrid', u'Africa/Ceuta', u'Atlantic/Canary']
#Para obtener listado de todas las zonas horarias
print(repr(pytz.all_timezones)) 
#Lista con ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa' ...


Sin embargo, la gran mejora viene cuando tenemos una hora en un zona horaria, y queremos conocer su equivalente en otra zona horaria.
Si quisiéramos listar todas las zonas horarias, ya sea para que elija el usuario, o para ver las posibilidades, se encuentran en la lista pytz.all_timezones. Pero esta lista es muy grande y dificil de manejar. Para conocer las zonas de un país, se podría preguntar a partir de la función country_timezones. Por ejemplo, para España, daría 3 zonas horarias: Europe/Madrid, Africa/Ceuta, Atlantic/Canary.

Para obtener la zona horaria en que se encuentra el sistema donde se ejecuta la aplicación hay que emplear el módulo tzlocal, con tzlocal.get_localzone().
import pytz
import tzlocal
import datetime

#Para obtener el nombre bueno de la zona local del ordenador
zona_local = tzlocal.get_localzone()
ahora = datetime.datetime.now()
ahora_local = zona_local.localize(ahora)
print(ahora_local)
#Pasar a hora UTC
ahora_UTC = ahora_local.astimezone(pytz.utc)
print(ahora_UTC)

Como hemos visto, no hay razón para no gestionar correctamente las fechas y horas en nuestras aplicaciones usando Python. Es verdad que al principio parece que lo hacen todo más largo y engorroso, pero acompañado de una auto-documentación mínima, puede evitar grandes errores y despistes en el mantenimiento y evolución del software.

No hay comentarios: