Created on 26 Mar 2020 ;    Modified on 23 Aug 2020 ;    Translationenglish

Come aggiorno gli articoli sul Coronavirus

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?

Premessa

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'organizzazione generale

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:

  • il linguaggio reStructuredText per scrivere il testo del template;
  • il linguaggio Python, per creare l'algoritmo generale da seguire, per scaricare i dati aggiornati da Internet e per fondere i dati nel template;
  • la libreria Pandas di Python, per importare i dati aggiornati in una forma tabellare standard, elaborarli secondo necessità, formare le tabelle da visualizzare.

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.

Il template

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:

  • in template_text il contenuto dell'esempio predetto;
  • in adate la data di aggiornamento, come stringa;
  • in data_table le linee csv che vogliamo inserire al posto di $DATA_TABLE;

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:

  • inseriamo i nostri dati nel dizionario d;
  • creiamo l'oggetto tmpl di tipo Template utilizzando template_text;
  • all'oggetto tmpl chiediamo di fondere i dati presenti in d.

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.

La tabella di dati in formato csv

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:

situazione dei due paesi più colpiti al 2020-03-26
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:

  • una sorgente da cui scaricare i dati aggiornati, e
  • uno strumento che ci permetta facilmente di caricare, elaborare e rappresentare i dati in questione.

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.

Conclusione

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


Aggiornamento del 23 Agosto 2020

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:

  • ARTICLES una lista di articoli da caricare
  • URLL l'indirizzo della pagina di login
  • URL l'indirizzo della pagina di upload
  • AUTH una tupla con utenza e password da utilizzae
  • ARTICLE_PATH la directory in cui trovare gli articoli

Quindi:

  • alla riga 10 istanzia una sessione che terrà traccia dei cookies; ne abbiamo bisogno perché il token csrf utilizzato da Django nelle form lo troviamo qui dentro;
  • alle righe 13-16 impostiamo i dati per fare login, visto che solo un utente loggato può fare upload di file;
  • e alla riga 17 li inviamo alla form di login; scoprire questa cosa ci ha fatto consumare un bel po' di tempo;
  • così come abbiamo impiegato non poco a capire che Django cambiava anche il token csrf (linea 18);
  • ora ci siamo: possiamo caricare i file voluti con il loop alle righe 22-24, ricordando
    • che nella nostra form il nome del campo è article, e non file, come usualmente indicato nei vari articoli su Internet,
    • che la URL cui inviare è quella di upload e non di login.

[1]

Per chi non conosce reStructuredText:

  • .. csv-table:: titolo è la direttiva che indica alla libreria reStructuredText di interpretare quel che segue come una tabella in formato csv.

Attenzione al fatto che il contenuto della tabella deve essere:

  • distanziato dalla direttiva con una riga vuota;
  • indentato rispetto la direttiva, ad esempio con un paio di spazio bianchi prima di ogni riga che forma il contenuto.

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.