Created on 26 Mar 2020 ; Modified on 23 Aug 2020 ; Translation: english
Ho scritto un paio di articoli riguardo l'andamento dell'epidemia di Coronavirus COVID-19. Dopo di che, mi si è posto il problema di aggiornare quotidianamente i dati. Farlo manualmente era un notevole dispendio di tempo. Quindi: perché non far fare ai computer il loro lavoro?
In questo periodo, qui in Italia, siamo ipnotizzati dai dati relativi all'epidemia di Coronavirus Covid-19. Dati che cambiano giorno dopo giorno.
Per chi, come il sottoscritto, ha avuto la ventura di stendere qualche articolo riguardo questa epidemia, si pone il problema dell'aggiornamento dei dati cui si fa riferimento negli articoli.
Farlo manualmente non è una strada perseguibile. Si tratta di un impegno di almeno mezz'ora, se non di più, da eseguire tutti i giorni per mesi. Una noia!
Di conseguenza, prendiamo monitor e tastiera e mettiamo al lavoro il nostro amato/odiato compagno di vita: il computer.
L'idea consiste nell'avere un articolo formato da un testo che non cambia nel tempo, inframezzato da alcune aree, che accoglieranno grafici e tabelle di dati il cui contenuto viene aggiornato automaticamente con la cadenza che desideriamo.
Nel seguito, chiamiamo template l'artefatto predetto.
Per raggiungere questo scopo utilizziamo:
Certo, non si tratta di poca cosa: abbiamo un bel po' di roba da cuocere al fuoco. Ma, vi assicuro, utilizzando un altro linguaggio, ad esempio il C, più veloce in esecuzione ma anche molto più impegnativo da gestire in fase di progetto, sarebbe andata molto peggio. Come vedremo, con Python la cosa è meno impegnativa di quanto sembri a prima vista.
Cominciamo dall'inizio: il template.
Come detto si tratta di avere una struttura fissa entro cui calare i dati che possono variare. E questi dati possono essere di qualunque tipo: testo, immagini, tabelle.
Ricordate che usualmente, per scrivere i nostri articoli utilizziamo il reStructuredText? Bene, continuiamo così. Semplicemente, dove vogliamo scrivere qualcosa che cambia nel tempo, mettiamo una parola che comincia con il $.
Perché il $? Questo è il segnale che indica alla libreria Python che gestisce i template, dove fare una sostituzione e con che cosa. Ad esempio, prendiamo un estratto dal template per l'andamento dell'infezione nel mondo [1]:
... <cut> ... Qui di seguito una sintesi tabellare della situazione per i venti paesi più colpiti alla data di aggiornamento di questo documento. Sono riportati i casi totali, i decessi, e il rapporto tra decessi e casi totali .. csv-table:: situazione dei venti paesi più colpiti al $UPDATED $DATA_TABLE ... <cut> ...
In questo esempio abbiamo i segnaposto $UPDATED e $DATA_TABLE. Come si intuisce, vogliamo sostituire in $UPDATED la data dell'aggiornamento, e in $DATA_TABLE una tabella con i dati aggiornati, in formato csv.
Come facciamo per effettuare la fusione tra template e dati? Supponiamo di avere:
possiamo procedere così:
from string import Template # ...<cut>... code to istantiate template_text, adate, data_table d = {'UPDATED': adate, 'DATA_TABLE': data_table, } tmpl = Template(template_text) article = tmpl.safe_substitute(d) # ...<cut>... code to write article to file
In pratica:
Ce la siamo cavata con sei righe di codice, salvo gli accessori per preparare i dati all'elenco precedente e per salvare su file il lavoro fatto. Non male.
Adesso consideriamo il motivo per cui stiamo facendo tutto questo. La tabella che vogliamo inserire al posto di $DATA_TABLE, andrà ricalcolata ogni giorno, al variare dei dati dell'epidemia.
Diciamo che vogliamo inserire una tabella di questo tipo:
date | cases | country |
2020-03-26 | 81968 | China |
2020-03-26 | 74386 | Italy |
si capisce che i dati nella tabella il 27 Marzo saranno diversi da quelli qui riportati, calcolati con i dati del 26 Marzo.
Ci servono:
La sorgente da cui scaricare i dati aggiornati andrà individuata in ragione di cosa ci interessa. In questo caso, facciamo riferimento a questa URL dell'agenzia European Centre for Disease Prevention and Control.
Supponiamo di avere la URL predetta registrata nella variabile url e di voler salvare i dati in un file di nome memorizzato nella variabile fname. Usando la libreria standard urllib, per scaricare e salvare il file possiamo procedere così [2]:
import urllib.request # ...<cut>... code to istantiate variables url and fname with urllib.request.urlopen(url) as response: from_web = response.read() with open(fname, 'wb') as f: f.write(from_web)
Salvato il file dati da Internet sul nostro computer, ora utilizziamo Pandas per importare i dati in forma tabellare e cominciare a elaborarli.
Importare i dati non è complesso. Se fname è il nome del file che contiene i dati:
import pandas as pd # ...<cut>... code to istantiate variable fname df = pd.read_csv(fname)
importa i dati nella variabile df.
df è l'acronimo di DataFrame. La struttura dati tabellare su cui tipicamente si lavora utilizzando Pandas.
In questo articolo non abbiamo intenzione di esplorare le possibilità di Pandas. Non lo conosciamo così bene, e sono veramente tante. Ci accontentiamo di capire rapidamente come passare da un DataFrame proveniente dal file csv, e strutturato così [3]:
date ... cases ... country id 2020-03-18 ... 33 ... China CN 2020-03-17 ... 110 ... China CN 2020-03-16 ... 25 ... China CN ... ... ... ... ... .. 2020-01-01 ... 0 ... United_States_of_America US 2019-12-31 ... 0 ... United_States_of_America US
dove ogni singola riga riporta, tra gli altri, i nuovi casi positivi osservati nella giornata per la nazione citata, ad un DataFrame strutturato così:
date cases country 2020-03-25 81847 China 2020-03-25 69176 Italy ...
dove ogni riga riporta il totale dei casi positivi per la nazione e la data dell'ultima osservazione ricevuta.
In pratica possiamo eseguire questo codice:
import pandas as pd # ...<cut>... code to istantiate variable df l = [] # will be: [[date, sum, country], [date, sum, country], ...] countries = df['country'].drop_duplicates() # get a copy of all countries for country in countries: country_df = df.loc[df['country']==country].copy() # get a copy of all records of a country the_cases = country_df['cases'].sum() # total of cases the_date = country_df['date'].max() # the greatest date for this country l.append((the_date, the_cases, country[:])) df2 = pd.DataFrame(l, columns=['date', 'cases','country']) # create a new dataframe with these values
Con otto righe di codice otteniamo il dataframe che ci interessa. E con il seguente lo assegnamo alla variabile data_table per portarlo nel template:
import pandas as pd # ...<cut>... code to istantiate variable df # a single big string with every line starting from 1st column on left side and ending with \n data_table = df2.to_csv(index=False) # we need a couple of spaces from 1st column on left side for every line; so: lines = data_table.split('\n') # splitting in lines lines = [' '+line for line in lines] # two spaces on left side data_table = '\n'.join(lines) # rejoining lines
Qui ci siamo dovuti allargare: più quattro righe per aggiungere quei benedetti spazi bianchi a sinistra, che ci servono ad indentare il contenuto della tabella.
A questo punto abbiamo tutti i mattoni che ci servono per costruire il nostro sistema di aggiornamento automatico dei dati inseriti nell'articolo.
Se siete interessati a leggere tutto il codice del progetto, lo potete consultare in questo repository GitHub.
Enjoy. ldfa
Come detto, questo progetto è nato per aggiornare quotidianamente quattro articoli che graficano gli andamenti temporali del covid in Italia e nel mondo.
Sino ad oggi lanciavo questi due comandi:
python world.py python italy.py
dopodiche aggiornavo il sito caricando manualmente i quattro articoli generati.
Dopo vari mesi di caricamento manuale degli articoli tutti i giorni, mi sono stufato di fare anche questo. Perciò ho deciso di ricorrere all'uso della libreria requests per fare le post necessarie ad effettuare lo upload degli articoli da programma.
Il programma si chiama fu.py: acronimo di file upload
Ne parlo in queste righe, perché il suo sviluppo mi ha dato qualche problema. Finchè non ho messo un breakpoint nel server di sviluppo, non sono riuscito a capire per quali motivi non caricavo i file. Se vi interessa, ecco una sintesi di come fare, al solito: il dettaglio lo potete cogliere leggendo il programma completo da GitHub.
Avremo un file di configurazione in cui mettere i dati sensibili:
[fu] ARTICLES = 001_article1.rst,002article1.it.rst URLL = https://website/login/ URL = https://website/blog/load-article AUTH = user,password
Mentre la maggior parte del codice è come segue:
1 #... 2 import configparser 3 #... 4 import requests 5 6 # ... loading article list, user, password, URL to load article, 7 # URLL to login user, from configuration file (see https://github.com/l-dfa/COVID-19-italia) 8 9 def main(): 10 with requests.Session() as s: # using session 'cause we need cookies to handle csrf token and user login 11 lg = s.get(URL) # get article AND ... 12 csrf_token = lg.cookies['csrftoken'] # ... csrf 13 login_data = {'csrfmiddlewaretoken': csrf_token, # program MUST login user to upload files 14 'username': AUTH[0], 15 'password': AUTH[1], 16 'next': '/blog/load-article'} 17 rl = s.post(URLL, data=login_data) # posting to login (NOT article: weird, isn't it?). it switches to load-article AND ... 18 csrf_token = rl.cookies['csrftoken'] # ... it CHANGES csrf 19 # ... 20 21 load_article_data = {'csrfmiddlewaretoken': csrf_token, } 22 for article in ARTICLES: 23 files = {'article': open(ARTICLE_PATH + '/' + article, 'rb')} # the field name is article, not file 24 ru = s.post(URL, files=files, data=load_article_data) 25 # ... 26 27 # ...
Il main suppone di avere le variabili globali:
Quindi:
[1] | Per chi non conosce reStructuredText:
Attenzione al fatto che il contenuto della tabella deve essere:
Invece l'indicatore ... <cut> ... nell'esempio vuole rimarcare il fatto che abbiamo omesso parte del template. Parte non significativa per il nostro esempio. |
[2] | Lo ammetto, quando leggo da programma qualcosa da Internet, rimango stupefatto dalla facilità con cui è possibile farlo quando si utilizza Python. |
[3] | Questa è una serie temporale: ogni nazione ha un gruppo di date, e per ogni data è riportato il numero di osservazioni nella giornata. |