08 Sezione 8 di 15
Internet e database
Con poche righe un programma Leo scarica dati da Internet, manda un'email, accende un piccolo sito sul tuo computer o conserva migliaia di dati in un database. Tutto con comandi in italiano e senza installare niente.
Scaricare pagine e dati#
variabile html = scarica_pagina("https://www.esempio.it")stampa(lunghezza(html)) variabile meteo = scarica_json("https://api.esempio.it/meteo", parametri = {"città": "Roma"})stampa("A Roma ci sono {meteo["temperatura"]} gradi") scarica_file("https://www.esempio.it/mappa.pdf", "documenti/mappa.pdf", avanzamento = vero)scarica_pagina(indirizzo)restituisce il testo della pagina;scarica_json(indirizzo, parametri)restituisce direttamente dizionari e liste di Leo;scarica_file(indirizzo, percorso, avanzamento = vero)salva un file e mostra la percentuale; il file compare solo quando è completo, quindi una connessione che cade non lascia file rovinati.
Richieste web complete#
Quando vuoi decidere tu che cosa fare in caso di errore, o devi inviare dati (per esempio a un servizio con una chiave), usa richiesta_web. La risposta è un dizionario.
variabile r = richiesta_web("https://api.esempio.it/voti", metodo = "POST", json = {"studente": "Ada", "voto": 30}, intestazioni = {"Authorization": "Bearer il-mio-token"}, tempo_massimo = 10) se r["riuscita"] allora stampa("Salvato! Risposta:", r["json"])altrimenti stampa("Qualcosa non va:", r["spiegazione"])fine| Chiave | Contenuto |
|---|---|
stato | il codice HTTP (200, 404, 500…) |
riuscita | vero se il codice è 2xx |
messaggio, spiegazione | il codice in breve e spiegato in italiano |
testo, json | il corpo della risposta, come testo e come valori Leo |
intestazioni, indirizzo | le intestazioni e l'indirizzo finale |
Anche i codici HTTP si fanno spiegare, e gli indirizzi si compongono senza preoccuparsi di spazi e accenti:
stampa(spiega_stato_http(404))stampa(codifica_url("caffè e latte"))stampa(componi_url("https://www.esempio.it/cerca", {"q": "Italo Calvino"}))Pagina non trovata (404): la pagina non esiste: controlla l'indirizzo caff%C3%A8%20e%20latte https://www.esempio.it/cerca?q=Italo+Calvino
Errori di rete in parole semplici#
I problemi di rete sono ErroreFile con un messaggio chiaro: «Sito non trovato», «Nessuna connessione» (con il consiglio di controllare Wi-Fi o cavo), «Tempo scaduto», «Il certificato di sicurezza non è valido»… Si gestiscono con prova:
se connessione_attiva() allora prova variabile pagina = scarica_pagina("https://www.esempio.it", tempo_massimo = 5) cattura ErroreFile come e stampa("Niente da fare: " + e.messaggio) finealtrimenti stampa("Sei offline: uso i dati salvati.")fineInviare email#
invia_email("ada@esempio.it", "Il programma ha finito", "Tutti i calcoli sono pronti.", "leo@esempio.it", "smtp.esempio.it", utente = "leo@esempio.it", password = leggi_file("password.txt").pulisci())La connessione è cifrata (porta 587 con STARTTLS, o 465); la password non compare mai nei messaggi d'errore e non viene mai inviata senza cifratura. Con html = "..." aggiungi una versione HTML del messaggio, con copia altri destinatari.
Un piccolo server web#
Leo può accendere un piccolo sito sul tuo computer, visibile solo da lì (127.0.0.1): per imparare, per mostrare dati nel browser o per farli leggere a un'altra applicazione. Questo esempio accende il server, lo interroga e lo spegne:
variabile sito = server_web(0, pagina = "<!doctype html><h1>Ciao dal mio programma!</h1>")sito.aggiungi_pagina("/dati", {"voti": [28, 30, 27]})sito.avvia()stampa("Il sito è acceso:", sito.attivo)stampa(scarica_json(sito.indirizzo + "dati"))sito.ferma()Il sito è acceso: vero
{"voti": [28, 30, 27]}server_web(0) sceglie una porta libera; di solito si scrive una porta precisa, come server_web(8080), e si apre il browser su sito.indirizzo. Per rispondere con il tuo codice si usa un gestore quando:
quando server.richiesta fai se evento["percorso"] == "/saluta" allora variabile nome = evento["parametri"].prendi("nome", "sconosciuto") rispondi("<!doctype html><h1>Ciao {nome}!</h1>") altrimenti se evento["percorso"] == "/basta" allora rispondi("Spengo il server.") server.ferma() altrimenti rispondi("Qui non c'è niente", stato = 404) finefine variabile server = server_web(8080)stampa("Apri il browser su " + server.indirizzo + "saluta?nome=Ada")server.servi()In evento trovi percorso, metodo, parametri, corpo, json e intestazioni. rispondi capisce da solo il tipo: un testo che comincia con <!doctype è una pagina, un dizionario o una lista diventano JSON. server.servi() resta in ascolto finché non chiami server.ferma() o premi STOP.
Database: conservare tanti dati#
Un database conserva dati in tabelle, come un foglio di calcolo molto più veloce e sicuro. Leo usa SQLite: il database è un singolo file, e non c'è niente da installare.
variabile db = apri_database("scuola.db") # crea il file se non c'è db.crea_tabella("studenti", { "nome": "testo obbligatorio", "età": "intero", "media": "decimale", "iscritto": "logico"}) db.inserisci("studenti", {"nome": "Ada", "età": 20, "media": 28.5, "iscritto": vero})db.inserisci("studenti", [ {"nome": "Bruno", "età": 17, "media": 24.0, "iscritto": vero}, {"nome": "Carla", "età": 25, "media": 27.2, "iscritto": falso}]) per ogni s in db.cerca("studenti", {"età": [">=", 18]}, ordina = "nome") fai stampa(s["nome"], s["età"], s["media"])finestampa("Iscritti:", db.conta("studenti", {"iscritto": vero})) db.aggiorna("studenti", {"nome": "Ada"}, {"età": 21})stampa(db.primo("studenti", {"nome": "Ada"})["età"])db.elimina("studenti", {"nome": "Carla"})stampa("Rimasti:", db.conta("studenti"))db.chiudi()Ada 20 28.5 Carla 25 27.2 Iscritti: 2 21 Rimasti: 2
- i tipi delle colonne sono
intero,decimale,testo,logico, seguiti se vuoi dachiave,obbligatorioounico; se non c'è una colonnaid, Leo ne aggiunge una automatica; crea_tabellanon fa niente se la tabella esiste già: puoi chiamarla ogni volta che il programma parte;- i risultati sono sempre liste di dizionari, una riga = un dizionario;
apri_database()senza nome crea un database solo in memoria, che sparisce alla fine: ottimo per provare.
Le condizioni si scrivono come dizionari; valgono tutte insieme:
| Scrivi | Significa |
|---|---|
{"nome": "Ada"} | uguale |
{"età": [">", 18]} | anche <, >=, <=, != |
{"nome": ["contiene", "an"]} | contiene il testo (anche "inizia", "finisce") |
{"voto": ["tra", 18, 30]} | compreso tra i due valori |
{"città": ["in", ["Roma", "Milano"]]} | uno dei valori della lista |
{"email": nulla} | vuoto |
Sicurezza: niente SQL injection#
L'attacco più comune ai programmi con un database consiste nello scrivere, al posto di un nome, un pezzo di comando che cambia il significato della ricerca. Con i comandi di Leo non può succedere: i valori restano sempre separati dal comando, e i nomi di tabelle e colonne vengono controllati.
variabile db = apri_database()db.crea_tabella("utenti", {"nome": "testo"})db.inserisci("utenti", {"nome": "Ada"})variabile furbo = "x' OR '1'='1"stampa(db.cerca("utenti", {"nome": furbo})) # nessun risultato: è solo un nomestampa(db.esegui("SELECT nome FROM utenti WHERE nome = ?", ["Ada"]))[]
[{"nome": "Ada"}]Se scrivi tu il comando SQL con db.esegui, la regola è una sola: i valori mai dentro il testo del comando, sempre nei parametri (? con una lista, o :nome con un dizionario).
Transazioni: tutto o niente#
Una transazione raggruppa più modifiche: o riescono tutte, o nessuna. È il caso del bonifico: se il programma si fermasse dopo aver tolto i soldi da un conto ma prima di aggiungerli all'altro, i soldi sparirebbero.
variabile db = apri_database()db.crea_tabella("conti", {"nome": "testo unico", "saldo": "intero"})db.inserisci("conti", [{"nome": "Ada", "saldo": 100}, {"nome": "Bruno", "saldo": 20}]) funzione bonifico(da, a, quanto) db.transazione(funzione () variabile conto = db.primo("conti", {"nome": da}) db.aggiorna("conti", {"nome": da}, {"saldo": conto["saldo"] - quanto}) se conto["saldo"] < quanto allora lancia ErroreValore("Saldo insufficiente") fine variabile altro = db.primo("conti", {"nome": a}) db.aggiorna("conti", {"nome": a}, {"saldo": altro["saldo"] + quanto}) fine)fine bonifico("Ada", "Bruno", 30)prova bonifico("Bruno", "Ada", 500)cattura ErroreValore come e stampa("Bonifico annullato:", e.messaggio)fineper ogni c in db.cerca("conti") fai stampa(c["nome"], c["saldo"])fineBonifico annullato: Saldo insufficiente Ada 70 Bruno 50
Nel secondo bonifico il saldo di Bruno era già stato cambiato quando è arrivato l'errore: la transazione ha annullato tutto, e i conti sono rimasti giusti.
Esportare e importare CSV#
variabile db = apri_database()db.crea_tabella("studenti", {"nome": "testo", "età": "intero"})db.inserisci("studenti", [{"nome": "Ada", "età": 20}, {"nome": "Bruno", "età": 17}])db.esporta_csv("studenti", "studenti.csv", separatore = ";")stampa(leggi_file("studenti.csv"))variabile altro = apri_database()stampa("Importate", altro.importa_csv("studenti.csv", "copia"), "righe")id;nome;età 1;Ada;20 2;Bruno;17 Importate 2 righe
importa_csv crea la tabella se non c'è, indovinando i tipi delle colonne, ed è «tutto o niente»: se una riga non va bene non viene inserito niente e il messaggio dice il numero della riga.