Para terminar el tutorial de creación de ficheros docx con Python vamos a ver un caso práctico de creación de un informe a partir de información de internet.
Lo que vamos a hacer es crear un informe de la predicción generada por la AEMet (Agencia Estatal de Meteorología de España) para una localidad en concreto, digamos Palma de Mallorca, que es un lugar ideal para vivir y para crear franquicias de empresas guays.
La información la podéis encontrar en este enlace. Lo que haremos será descargar el fichero XML que hay en al anterior enlace y, a partir del mismo, crear el fichero docx.
Empezamos importando unas pocas cosas que necesitaremos:
1 2 3 4 5 6 7 8 9 10 11 |
from urllib.request import urlopen import xml.etree.ElementTree as ET from xml.etree.ElementTree import Element from typing import Dict from collections import defaultdict from pprint import pprint import datetime as dt import docx from docx.table import Table from docx.shared import Pt |
Obtención de la información
La información que vamos a descargar está en formato XML y es información, como he comentado antes, de predicción meteorológica proporcionada por la AEMet (Agencia Estatal de Meteorología de España).
La que voy a descargar corresponde a la ciudad de Palma pero puedes elegir otras localidades (cambiando la url a usar).
1 2 |
url = "http://www.aemet.es/xml/municipios_h/localidad_h_07040.xml" aemetdoc = ET.fromstring(urlopen(url).read()) |
De la información que acabamos de descargar podemos extraer algunos metadatos que luego usaremos en nuestro informe automatizado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Algunos metadatos # Fecha de elaboración de la predicción print(aemetdoc.find("elaborado").text) # Ciudad para la predicción print(aemetdoc.find("nombre").text) # Provincia de la predicción print(aemetdoc.find("provincia").text) # Metadata del productor de los datos print(aemetdoc.find("origen/copyright").text) print(aemetdoc.find("origen/enlace").text) print(aemetdoc.find("origen/nota_legal").text) print(aemetdoc.find("origen/productor").text) print(aemetdoc.find("origen/web").text) |
El resultado de lo anterior debería mostrar algo parecido a:
1 2 3 4 5 6 7 8 |
2020-07-26T12:47:46 Palma Illes Balears © AEMET. Autorizado el uso de la información y su reproducción citando a AEMET como autora de la misma. http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/palma-id07040 http://www.aemet.es/es/nota_legal Agencia Estatal de Meteorología - AEMET. Gobierno de España http://www.aemet.es |
Los datos de predicción los podemos extraer de cada día para el que hay predicción disponible:
1 |
dias = aemetdoc.findall("prediccion/dia") |
Para ello nos ayudaremos de la siguiente función adaptada de una respuesta en SO para convertir el parte de predicción de cada día del formato original en XML a un diccionario que sea más digerible.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# Función adaptada de https://stackoverflow.com/a/10077069 def extract_info_day(daytag: Element) -> Dict[str, str]: d = {daytag.tag: {} if daytag.attrib else None} children = list(daytag) if children: dd = defaultdict(list) for dc in map(extract_info_day, children): for k, v in dc.items(): dd[k].append(v) d = {daytag.tag: {k:v[0] if len(v) == 1 else v for k, v in dd.items()}} if daytag.attrib: d[daytag.tag].update(('@' + k, v) for k, v in daytag.attrib.items()) if daytag.text: text = daytag.text.strip() if children or daytag.attrib: if text: d[daytag.tag]['#text'] = text else: d[daytag.tag] = text return d |
Meto la información en un diccionario junto con algunos metadatos.
Empiezo por los metadatos:
1 2 3 4 5 6 7 8 9 10 11 |
fcst_data = {} fcst_data["elaborado"] = aemetdoc.find("elaborado").text fcst_data["nombre"] = aemetdoc.find("nombre").text fcst_data["provincia"] = aemetdoc.find("provincia").text fcst_data["origen_copyright"] = aemetdoc.find("origen/copyright").text fcst_data["origen_enlace"] = aemetdoc.find("origen/enlace").text fcst_data["origen_notalegal"] = aemetdoc.find("origen/nota_legal").text fcst_data["origen_productor"] = aemetdoc.find("origen/productor").text fcst_data["origen_web"] = aemetdoc.find("origen/web").text fcst_data["prediccion_comienzo"] = f"{dias[0].get('fecha')}" fcst_data["prediccion_fin"] = f"{dias[-1].get('fecha')}" |
Y ahora extraigo los datos para cada hora (viento, temperatura,…), periodo (probabilidad de tormenta, probabilidad de lluvia,…) y día (orto y ocaso).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
fcst_data["datos"] = { "hourly": {"datetime": []}, "periods": {"datetime": []}, "daily": {"datetime": []}, } for dia in dias: d = extract_info_day(dia) keys = d['dia'].keys() datetime = dt.datetime.strptime( # NAIVE DATETIMES!!! d['dia']["@fecha"], "%Y-%m-%d", ) # Prepare the datetimes # hourly data for dd in d['dia']['temperatura']: timedelta = int(dd["@periodo"]) fcst_data["datos"]["hourly"]["datetime"].append( datetime + dt.timedelta(hours=timedelta) ) # 6-hourly period data for dd in d['dia']['prob_nieve']: timedelta = int(dd["@periodo"][:2]) fcst_data["datos"]["periods"]["datetime"].append( datetime + dt.timedelta(hours=timedelta) ) # daily data fcst_data["datos"]["daily"]["datetime"].append( datetime ) # Prepare the data for k in keys: if k.startswith("prob_"): if fcst_data["datos"]["periods"].get(k, None) is None: fcst_data["datos"]["periods"][k] = [] for p in d["dia"][k]: fcst_data["datos"]["periods"][k].append( p.get("#text", None) # Aquí el campo me vale como str ) elif k.startswith("@"): if k == "@fecha": continue if fcst_data["datos"]["daily"].get(k[1:], None) is None: fcst_data["datos"]["daily"][k[1:]] = [] p = d["dia"][k] fcst_data["datos"]["daily"][k[1:]].append(p) else: if k != "viento": if fcst_data["datos"]["hourly"].get(k, None) is None: fcst_data["datos"]["hourly"][k] = [] else: if fcst_data["datos"]["hourly"].get(f"{k}_vel", None) is None: fcst_data["datos"]["hourly"][f"{k}_vel"] = [] if fcst_data["datos"]["hourly"].get(f"{k}_dir", None) is None: fcst_data["datos"]["hourly"][f"{k}_dir"] = [] for p in d["dia"][k]: if k == "estado_cielo": fcst_data["datos"]["hourly"][k].append( p.get("@descripcion", None) ) elif k == "viento": try: fcst_data["datos"]["hourly"][f"{k}_vel"].append( int(p["velocidad"]) ) except: fcst_data["datos"]["hourly"][f"{k}_vel"].append( None ) fcst_data["datos"]["hourly"][f"{k}_dir"].append( p.get("direccion", None) ) else: try: fcst_data["datos"]["hourly"][k].append( int(p["#text"]) ) except: fcst_data["datos"]["hourly"][k].append( None ) |
Muestro el resultado de mi diccionario:
1 2 3 |
pprint(fcst_data, indent=1, compact=True) |
La salida será:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
{'datos': {'daily': {'datetime': [datetime.datetime(2020, 7, 26, 0, 0), datetime.datetime(2020, 7, 27, 0, 0), datetime.datetime(2020, 7, 28, 0, 0)], 'ocaso': ['21:07', '21:06', '21:06'], 'orto': ['06:43', '06:44', '06:45']}, 'hourly': {'datetime': [datetime.datetime(2020, 7, 26, 8, 0), datetime.datetime(2020, 7, 26, 9, 0), datetime.datetime(2020, 7, 26, 10, 0), datetime.datetime(2020, 7, 26, 11, 0), datetime.datetime(2020, 7, 26, 12, 0), datetime.datetime(2020, 7, 26, 13, 0), datetime.datetime(2020, 7, 26, 14, 0), datetime.datetime(2020, 7, 26, 15, 0), datetime.datetime(2020, 7, 26, 16, 0), datetime.datetime(2020, 7, 26, 17, 0), datetime.datetime(2020, 7, 26, 18, 0), datetime.datetime(2020, 7, 26, 19, 0), datetime.datetime(2020, 7, 26, 20, 0), datetime.datetime(2020, 7, 26, 21, 0), datetime.datetime(2020, 7, 26, 22, 0), datetime.datetime(2020, 7, 26, 23, 0), datetime.datetime(2020, 7, 27, 0, 0), datetime.datetime(2020, 7, 27, 1, 0), datetime.datetime(2020, 7, 27, 2, 0), datetime.datetime(2020, 7, 27, 3, 0), datetime.datetime(2020, 7, 27, 4, 0), datetime.datetime(2020, 7, 27, 5, 0), datetime.datetime(2020, 7, 27, 6, 0), datetime.datetime(2020, 7, 27, 7, 0), datetime.datetime(2020, 7, 27, 8, 0), datetime.datetime(2020, 7, 27, 9, 0), datetime.datetime(2020, 7, 27, 10, 0), datetime.datetime(2020, 7, 27, 11, 0), datetime.datetime(2020, 7, 27, 12, 0), datetime.datetime(2020, 7, 27, 13, 0), datetime.datetime(2020, 7, 27, 14, 0), datetime.datetime(2020, 7, 27, 15, 0), datetime.datetime(2020, 7, 27, 16, 0), datetime.datetime(2020, 7, 27, 17, 0), datetime.datetime(2020, 7, 27, 18, 0), datetime.datetime(2020, 7, 27, 19, 0), datetime.datetime(2020, 7, 27, 20, 0), datetime.datetime(2020, 7, 27, 21, 0), datetime.datetime(2020, 7, 27, 22, 0), datetime.datetime(2020, 7, 27, 23, 0), datetime.datetime(2020, 7, 28, 0, 0), datetime.datetime(2020, 7, 28, 1, 0), datetime.datetime(2020, 7, 28, 2, 0), datetime.datetime(2020, 7, 28, 3, 0), datetime.datetime(2020, 7, 28, 4, 0), datetime.datetime(2020, 7, 28, 5, 0), datetime.datetime(2020, 7, 28, 6, 0), datetime.datetime(2020, 7, 28, 7, 0)], 'estado_cielo': ['Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado', 'Despejado'], 'humedad_relativa': [49, 40, 33, 30, 28, 32, 32, 32, 34, 36, 39, 41, 48, 55, 54, 57, 61, 58, 51, 49, 48, 52, 54, 51, 42, 33, 28, 27, 28, 31, 30, 29, 30, 32, 35, 37, 44, 50, 56, 56, 55, 56, 59, 62, 65, 60, 51, 49], 'nieve': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'precipitacion': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 'racha_max': [11, 12, 16, 17, 20, 38, 40, 40, 39, 33, 29, 24, 19, 11, 9, 12, 12, 11, 9, 8, 8, 9, 9, 9, 16, 18, 18, 17, 22, 35, 33, 32, 32, 31, 28, 23, 18, 11, 5, 7, 8, 10, 11, 11, 10, 7, 5, 7], 'sens_termica': [27, 29, 31, 31, 33, 32, 32, 31, 31, 31, 30, 29, 28, 28, 26, 26, 25, 24, 24, 24, 24, 23, 24, 25, 28, 30, 32, 33, 33, 33, 33, 33, 33, 32, 31, 31, 29, 28, 28, 26, 26, 25, 25, 24, 24, 25, 26, 26], 'temperatura': [27, 29, 31, 31, 33, 32, 32, 31, 31, 31, 30, 29, 28, 27, 26, 26, 25, 24, 24, 24, 24, 23, 24, 25, 28, 30, 32, 33, 33, 33, 33, 33, 33, 32, 31, 31, 29, 28, 27, 26, 26, 25, 25, 24, 24, 25, 26, 26], 'viento_dir': ['E', 'E', 'E', 'E', 'S', 'SO', 'SO', 'SO', 'SO', 'SO', 'SO', 'SO', 'O', 'SO', 'E', 'NE', 'NE', 'NE', 'N', 'N', 'NE', 'NE', 'NE', 'NE', 'NE', 'E', 'E', 'E', 'S', 'SO', 'SO', 'SO', 'SO', 'SO', 'SO', 'SO', 'SO', 'O', 'NO', 'N', 'NE', 'NE', 'NE', 'NE', 'NE', 'N', 'E', 'E'], 'viento_vel': [7, 6, 7, 8, 9, 22, 25, 25, 21, 18, 15, 12, 8, 3, 7, 8, 9, 7, 5, 6, 6, 7, 6, 6, 10, 11, 9, 7, 10, 20, 20, 20, 20, 18, 15, 12, 8, 4, 4, 5, 6, 8, 8, 8, 6, 4, 3, 5]}, 'periods': {'datetime': [datetime.datetime(2020, 7, 26, 2, 0), datetime.datetime(2020, 7, 26, 8, 0), datetime.datetime(2020, 7, 26, 14, 0), datetime.datetime(2020, 7, 26, 20, 0), datetime.datetime(2020, 7, 27, 2, 0), datetime.datetime(2020, 7, 27, 8, 0), datetime.datetime(2020, 7, 27, 14, 0), datetime.datetime(2020, 7, 27, 20, 0), datetime.datetime(2020, 7, 28, 2, 0), datetime.datetime(2020, 7, 28, 8, 0), datetime.datetime(2020, 7, 28, 14, 0), datetime.datetime(2020, 7, 28, 20, 0)], 'prob_nieve': [None, '0', '0', '0', '0', '0', '0', '0', '0', None, None, None], 'prob_precipitacion': [None, '0', '0', '0', '0', '0', '0', '0', '0', None, None, None], 'prob_tormenta': [None, '0', '0', '0', '0', '0', '0', '0', '0', None, None, None]}}, 'elaborado': '2020-07-26T12:47:46', 'nombre': 'Palma', 'origen_copyright': '© AEMET. Autorizado el uso de la información y su ' 'reproducción citando a AEMET como autora de la misma.', 'origen_enlace': 'http://www.aemet.es/es/eltiempo/prediccion/municipios/horas/palma-id07040', 'origen_notalegal': 'http://www.aemet.es/es/nota_legal', 'origen_productor': 'Agencia Estatal de Meteorología - AEMET. Gobierno de ' 'España', 'origen_web': 'http://www.aemet.es', 'prediccion_comienzo': '2020-07-26', 'prediccion_fin': '2020-07-28', 'provincia': 'Illes Balears'} |
Además de los datos de la predicción vamos a descargar también datos climáticos para poder comparar si lo que se nos viene encima es normal para estas fechas, si está por encima o por debajo de lo que podríamos esperar,… Para hacer esto voy a descargar los datos climatológicos de la estación del puerto de Palma para el periodo 1980-2010. La información la podéis encontrar aquí donde veréis un botón que pone csv. Esa es la información que vamos a descargar:
1 2 3 4 5 |
url = ( "http://www.aemet.es/es/serviciosclimaticos/" "datosclimatologicos/valoresclimatologicos_palma-puerto.csv" "?l=B228&k=bal" ) |
Creo una función para convertir lo que descargamos en algo útil para procesar. En este punto, lo sencillo sería usar pandas
pero como lo quiero mantener simple (de dependencias) voy a usar estructuras de datos básicas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
def get_clima(url: str) -> Dict[str, Dict]: d = { "T": { "descripcion": "Temperatura media mensual/anual (°C)", "valor_mensual": [], "valor_anual": None, }, "TM": { "descripcion": "Media mensual/anual de las temperaturas máximas diarias (°C)", "valor_mensual": [], "valor_anual": None, }, "Tm": { "descripcion": "Media mensual/anual de las temperaturas mínimas diarias (°C)", "valor_mensual": [], "valor_anual": None, }, "R": { "descripcion": "Precipitación mensual/anual media (mm)", "valor_mensual": [], "valor_anual": None, }, "H": { "descripcion": "Humedad relativa media (%)", "valor_mensual": [], "valor_anual": None, }, "DR": { "descripcion": "Número medio mensual/anual de días de precipitación superior o igual a 1 mm", "valor_mensual": [], "valor_anual": None, }, "DN": { "descripcion": "Número medio mensual/anual de días de nieve", "valor_mensual": [], "valor_anual": None, }, "DT": { "descripcion": "Número medio mensual/anual de días de tormenta", "valor_mensual": [], "valor_anual": None, }, "DF": { "descripcion": "Número medio mensual/anual de días de niebla", "valor_mensual": [], "valor_anual": None, }, "DH": { "descripcion": "Número medio mensual/anual de días de helada", "valor_mensual": [], "valor_anual": None, }, "DD": { "descripcion": "Número medio mensual/anual de días despejados", "valor_mensual": [], "valor_anual": None, }, "I": { "descripcion": "Número medio mensual/anual de horas de sol", "valor_mensual": [], "valor_anual": None, }, } raw_data = urlopen(url).read() raw_data = raw_data.split()[3:] for line in raw_data[:-1]: values = line.decode().replace('"', '').split(",") for key, value in zip(d.keys(), values[1:]): # (*) d[key]["valor_mensual"].append(float(value)) values = raw_data[-1].decode("ISO-8859-1").replace('"', '').split(",") for key, value in zip(d.keys(), values[1:]): # (*) d[key]["valor_anual"] = float(value) return d |
En la función anterior he dejado un (*) para indicar que eso funcionaría a partir de CPython 3.6. A partir de esa versión de CPython se preserva el orden de inserción en un diccionario. Si usas una versión más antigua de CPython podrías usar un OrderedDict
. Si usas PyPy no deberías tener problema puesto que este detalle de implementación en CPython proviene de cómo funcionan los diccionarios en PyPy.
Descargamos y formateamos los datos climatológicos:
1 |
clima_data = get_clima(url) |
Si muestro el resultado en pantalla:
1 2 3 |
pprint(clima_data, indent=1, compact=True) |
Mostrará algo como:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
{'DD': {'descripcion': 'Número medio mensual/anual de días despejados', 'valor_anual': 71.6, 'valor_mensual': [4.1, 3.4, 4.1, 4.1, 5.1, 8.9, 16.2, 11.0, 4.9, 3.3, 3.0, 4.0]}, 'DF': {'descripcion': 'Número medio mensual/anual de días de niebla', 'valor_anual': 4.2, 'valor_mensual': [0.6, 0.7, 1.0, 0.4, 0.5, 0.1, 0.0, 0.0, 0.0, 0.2, 0.2, 0.4]}, 'DH': {'descripcion': 'Número medio mensual/anual de días de helada', 'valor_anual': 0.0, 'valor_mensual': [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}, 'DN': {'descripcion': 'Número medio mensual/anual de días de nieve', 'valor_anual': 0.4, 'valor_mensual': [0.3, 0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]}, 'DR': {'descripcion': 'Número medio mensual/anual de días de precipitación ' 'superior o igual a 1 mm', 'valor_anual': 53.1, 'valor_mensual': [5.8, 5.6, 4.5, 5.1, 3.6, 1.7, 0.7, 1.9, 4.7, 6.7, 6.4, 6.5]}, 'DT': {'descripcion': 'Número medio mensual/anual de días de tormenta', 'valor_anual': 15.9, 'valor_mensual': [0.7, 0.7, 0.9, 1.3, 1.2, 0.8, 0.6, 1.5, 3.0, 2.1, 1.8, 1.3]}, 'H': {'descripcion': 'Humedad relativa media (%)', 'valor_anual': 71.0, 'valor_mensual': [73.0, 72.0, 70.0, 68.0, 69.0, 69.0, 68.0, 70.0, 72.0, 74.0, 74.0, 74.0]}, 'I': {'descripcion': 'Número medio mensual/anual de horas de sol', 'valor_anual': 2779.0, 'valor_mensual': [167.0, 170.0, 205.0, 237.0, 284.0, 315.0, 346.0, 316.0, 226.0, 205.0, 161.0, 151.0]}, 'R': {'descripcion': 'Precipitación mensual/anual media (mm)', 'valor_anual': 449.0, 'valor_mensual': [42.0, 37.0, 28.0, 39.0, 36.0, 11.0, 6.0, 22.0, 52.0, 69.0, 59.0, 48.0]}, 'T': {'descripcion': 'Temperatura media mensual/anual (°C)', 'valor_anual': 18.2, 'valor_mensual': [11.9, 11.9, 13.4, 15.5, 18.8, 22.7, 25.7, 26.2, 23.5, 20.2, 15.8, 13.1]}, 'TM': {'descripcion': 'Media mensual/anual de las temperaturas máximas ' 'diarias (°C)', 'valor_anual': 21.8, 'valor_mensual': [15.4, 15.5, 17.2, 19.2, 22.5, 26.5, 29.4, 29.8, 27.1, 23.7, 19.3, 16.5]}, 'Tm': {'descripcion': 'Media mensual/anual de las temperaturas mínimas ' 'diarias (°C)', 'valor_anual': 14.6, 'valor_mensual': [8.3, 8.4, 9.6, 11.7, 15.1, 18.9, 21.9, 22.5, 19.9, 16.6, 12.3, 9.7]}} |
Más o menos, tenemos todo lo que necesitamos. ¿Pasamos ya a la parte de Docx?
Formateado del documento
Voy a crear una clase con cierta funcionalidad que me ayudará a formatear el texto y a crear el informe final en formato docx:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
weekdays = { 0: "Lunes", 1: "Martes", 2: "Miércoles", 3: "Jueves", 4: "Viernes", 5: "Sábado", 6: "Domingo", } class Report: def __init__(self, fcst: Dict, climate: Dict) -> None: self._fcst_data = fcst self._clima_data = climate self._doc = docx.Document() self._title_page() self._nota_legal() self._footer() self._fcst_page() def save(self, filename: str) -> None: self._doc.save(filename) def _title_page(self) -> None: # no header nor footer on cover self._doc.sections[0].different_first_page_header_footer = True # some space at the beginning for i in range(5): self._doc.add_paragraph() # Title p = self._doc.add_paragraph() p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER p.add_run( "Informe de predicción meteorológica para " ) r = p.add_run( f"{self._fcst_data['nombre']} " f"({self._fcst_data['provincia']})" ) r.bold = True r.font.color.rgb = docx.shared.RGBColor(200, 200, 200) # Subtitle p = self._doc.add_paragraph() p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER p.add_run("Válido para los días comprendidos entre ") r = p.add_run( f"{self._fcst_data['prediccion_comienzo']}" " y " f"{self._fcst_data['prediccion_fin']}" ) r.bold = True r.font.color.rgb = docx.shared.RGBColor(200, 0, 0) # some more space for i in range(2): self._doc.add_paragraph() # Elaborado por p = self._doc.add_paragraph() p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER p.add_run("Documento elaborado por ") r = p.add_run("Pybonacci industries") r.bold = True r.font.color.rgb = docx.shared.RGBColor(0, 200, 0) def _nota_legal(self) -> None: # Page break self._doc.add_page_break() p = self._doc.add_paragraph("NOTA LEGAL:") p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY p = self._doc.add_paragraph( "Toda la información presente en este informe " f"ha sido elaborada por {self._fcst_data['origen_productor']} " f"({self._fcst_data['origen_copyright']})." "Puedes obtener más información en los siguientes enlaces: " f"{self._fcst_data['origen_notalegal']} y " f"{self._fcst_data['origen_web']}." ) p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY p = self._doc.add_paragraph( "Puedes consultar estos datos en la web de AEMet en el enlace " f"{self._fcst_data['origen_enlace']}." ) p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY p = self._doc.add_paragraph( "Toda la información presente en este informe " f"ha sido elaborada por {self._fcst_data['origen_copyright']}" ) p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY self._doc.add_paragraph() p = self._doc.add_paragraph() p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY r = p.add_run( "La información aquí contenida es meramente informativa y " "Pybonacci Industries no se hace responsable del uso que se " "haga de la misma." ) r.bold = True r.font.color.rgb = docx.shared.RGBColor(255, 0, 0) def _footer(self): # Add footer section = self._doc.sections[-1] footer = section.footer p = footer.add_paragraph("Elaborado por Pybonacci Industries.") p.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.CENTER def _fcst_page(self) -> None: # Page break self._doc.add_page_break() # Add title self._doc.add_heading( ( f'Predicción elaborada el {fcst_data["elaborado"]} ' f'para {fcst_data["nombre"]} ({fcst_data["provincia"]})' ), level=0 ) # Información sobre el orto y el ocaso self._doc.add_heading( "Orto y Ocaso para los próximos días", level=1 ) self._doc.add_paragraph() # añado un párrafo vacio para dejar 'aire' for d, orto, ocaso in zip( fcst_data["datos"]["daily"]["datetime"], fcst_data["datos"]["daily"]["orto"], fcst_data["datos"]["daily"]["ocaso"] ): dia = weekdays[d.weekday()] self._doc.add_paragraph( ( f'El {dia.lower()}, {d.strftime("%d-%m-%Y")}, ' f'el sol saldrá a las {orto} y ' f'se pondrá a las {ocaso}.' ) ) self._doc.add_paragraph() # añado un párrafo vacio para dejar 'aire' # Información sobre probabilidades de tormenta y precipitación self._doc.add_heading( "Probabilidad de caída de meteoros y de tormenta", level=1 ) self._doc.add_paragraph() # añado un párrafo vacio para dejar 'aire' datos = fcst_data["datos"]["periods"] rows = 2 for d in datos["prob_precipitacion"]: if d is not None: rows += 1 cols = 5 t = self._doc.add_table(rows, cols, style="Table Grid") row = t.rows[0] a, b = row.cells[:2] a.merge(b) a.text = "Válido (hora local)" a = row.cells[2] for cell in row.cells[3:]: a.merge(cell) a.text = "Probabilidad (%)" row = t.rows[1] row.cells[0].text = "Desde" row.cells[1].text = "Hasta" col_names = [k for k in datos.keys() if "prob_" in k] for cell, n in zip(row.cells[2:cols], col_names): cell.text = n for i, row in enumerate(t.rows[2:]): if datos[col_names[0]][i] is not None: ini = datos["datetime"][i].strftime("%y-%m-%d %H:%M") row.cells[0].text = ini fin = ( datos["datetime"][i] + dt.timedelta(hours=6) ).strftime("%y-%m-%d %H:%M") row.cells[1].text = fin for cell, name in zip(row.cells[2:], col_names): cell.text = datos[name][i] self._doc.add_paragraph() # añado un párrafo vacio para dejar 'aire' # Información horaria # Page break self._doc.add_page_break() self._doc.add_heading( "Predicción horaria", level=1 ) self._doc.add_paragraph() # añado un párrafo vacio para dejar 'aire' datos = fcst_data["datos"]["hourly"] rows = 1 + len(datos["precipitacion"]) cols = len(datos.keys()) t = self._doc.add_table(rows, cols, style="Table Grid") row = t.rows[0] for cell, k in zip(row.cells, datos.keys()): if k == "datetime": cell.text = "fecha (hora local)" if k in ("precipitacion", "nieve"): cell.text = f"{k} (mm)" if k in ("precipitacion", "nieve"): cell.text = f"{k} (mm)" if k in ("temperatura", "sens_termica"): cell.text = f'{k.replace("_", " ")} (ºC)' if k == "humedad_relativa": cell.text = f'{k.replace("_", " ")} (%)' if k in ("viento_vel", "racha_max"): cell.text = f'{k.replace("_", " ")} (km/h)' if k == "viento_dir": cell.text = k.replace("_", " ") col_names = [k for k in datos.keys() if k != "datetime"] for i, row in enumerate(t.rows[1:]): ini = datos["datetime"][i].strftime("El %d del %m a las %H") row.cells[0].text = ini for cell, name in zip(row.cells[1:], col_names): cell.text = str(datos[name][i]) for row in t.rows: for cell in row.cells: paragraphs = cell.paragraphs for paragraph in paragraphs: for run in paragraph.runs: font = run.font font.size= Pt(8) # Climatología # Page break self._doc.add_page_break() self._doc.add_heading( "Valores esperados", level=0 ) self._doc.add_paragraph( "Los valores esperados para el mes en que se ha iniciado " "este pronóstico figuran a continuación." ) mes = fcst_data["datos"]["daily"]["datetime"][0].month for key in clima_data.keys(): self._doc.add_paragraph( f'{clima_data[key]["descripcion"]}: ' f'{clima_data[key]["valor_mensual"][mes - 1]}' ) |
La clase anterior solo tiene un método útil, que es save
, el resto son métodos que se usan internamente para crear la portada del documento, _title_page
, una página con una nota legal informando sobre el origen de los datos y sobre la responsabilidad de los usos de los mismos, _nota_legal
, un método _footer
que añade algo de información en los pies de página, y el método _fcst_page
que en realidad añade varias páginas con la información meteorológica y climatológica que hemos procesado. Todos estos métodos internos se invocan al instanciar la clase. Lo podemos hacer de la siguiente forma:
1 |
r = Report(fcst_data, clima_data) |
Ese objeto ya tendrá la información procesada y lo único que nos quedaría es guardarlo en disco con el formato docx con el nombre que le indiquemos:
1 2 |
r.save("ejemplo.docx") |
El resultado final lo podéis ver en este enlace.
Espero que os hayan gustado los distintos capítulos del tutorial. Si tenéis dudas en alguno de los capítulos podéis preguntar en los comentarios del capítulo que corresponda.
Hola,
Muchas gracias por el tutorial.
No tengo ni idea de programación y estoy empezando con python porque me interesa automatizar un proceso de extracción de autor, fecha y artículo de páginas webs de noticias para guardarlo en un documento de Word docx.
Consigo todos los datos con la librería newspaper3k https://newspaper.readthedocs.io/en/latest/
Puedo crear documentos nuevos de Word con la librería https://python-docx.readthedocs.io/en/latest/
¿Alguna idea de cómo podría “pegar” los datos obtenidos (autor, fecha y artículo) en un documento Word docx?
Gracias anticipadas,
Hola, Joan.
Muchos ánimos con lo que quieres hacer. Durante todo el tutorial se explica cómo crear un documento docx. Lo ideal es que lo intentes siguiendo los distrintos capítulos.
Saludos.
Muchas gracias Kiko por tu rápida respuesta!
Un saludo,
Hola, en el caso de tener un CRUD y se quisiera hacer un reporte del mismo con Python-docx. ¿Cómo se haría?
Gracias de antemano.
Hola, gracias por el tutorial. Disculpa, en el caso de tener un CRUD y se quisiera hacer un reporte del mismo con Python-docx. ¿Cómo se haría?
Gracias de antemano.
Hola.
Tu pregunta es muy amplia e involucra varios temas. Lo mejor es que los vayas aprendiendo por separado, aquí ya tienes un tutorial de una de las partes, y verás que es fácil unirlo.
Saludos.