Ricerca - espressioni regolari

Introduzione

Oggi parleremo di ricerca nei dati usando le cosidette Espressioni regolari (regex):

Per compiere operazioni di ricerca su testo, quando il problema è relativamente semplice si possono usare alcuni metodi delle stringhe (replace, search, index, upper, lower, etc…), ma in casi più compicati, per evitare di scrivere tonnellate di codice ed if può essere più pratico utilizzare delle espressioni regolari.

In questo questo tutorial ci occuperemo di

  • esempio dai dati dei trasporti

  • filtreremo strade provinciali

  • re.search e altro

Per capire velocemente cosa sono le regex, prova a giocare un po’ con regexcrossword.com (vedere istruzioni e tutorial)

Quando hai dubbi durante il tutorial, prova le regex online su regex101.com

ATTENZIONE: ricordati di selezionare 'Python' nella barra a sinistra in FLAVOR !

Che fare

  • scompatta lo zip in una cartella, dovresti ottenere qualcosa del genere:

search
    regex.ipynb
    regex-sol.ipynb
    jupman.py

ATTENZIONE: Per essere visualizzato correttamente, il file del notebook DEVE essere nella cartella szippata.

  • apri il Jupyter Notebook da quella cartella. Due cose dovrebbero aprirsi, prima una console e poi un browser. Il browser dovrebbe mostrare una lista di file: naviga la lista e apri il notebook regex.ipynb

  • Prosegui leggendo il file degli esercizi, ogni tanto al suo interno troverai delle scritte ESERCIZIO, che ti chiederanno di scrivere dei comandi Python nelle celle successive.

Scorciatoie da tastiera:

  • Per eseguire il codice Python dentro una cella di Jupyter, premi Control+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E selezionare la cella seguente, premi Shift+Invio

  • Per eseguire il codice Python dentro una cella di Jupyter E creare una nuova cella subito dopo, premi Alt+Invio

  • Se per caso il Notebook sembra inchiodato, prova a selezionare Kernel -> Restart

1 Prendiamoci i dati

Per partire da casi concreti, come già fatto in precedenza andiamo a cercarci dei dati dal catalogo opendata dati.trentino.it. In questo caso sceglieremo un file dal dataset Trasporti pubblici del Trentino (formato GTFS). Sono dati dei trasporti, ma quello che impareremo vale per qualunque dataset che contenga del testo.

✪ DOMANDA 1.1: Quale è la licenza del dataset? Possiamo farci tutto quello che vogliamo ?

Mostra risposta

Il formato GTFS è un formato pratico per gli orari e tracciati del trasporto pubblico. Questo formato ci descrive i campi che ci aspettiamo nei file. Ma i file, fisicamente, in che formato sono?

Nel dataset troviamo la risorsa GTFS Urbano TTE che al suo interno contiene un link ad uno zip. Se apriamo lo zip troveremo diversi .txt che se attentamente osservati rivelano essere in formato CSV (cominciate a notare l’utilità del formato ;-) ?

Concentriamoci sul file stops.txt, di cui vediamo un estratto qui:

stop_id,stop_code,stop_name,stop_desc,stop_lat,stop_lon,zone_id,wheelchair_boarding
1,28105z,Baselga Del Bondone,,46.078317,11.046924,10110,2
2,28105x,Baselga Del Bondone,,46.078581,11.047541,10110,2
3,27105c,Belvedere,,46.044406,11.105342,10110,2
4,22220z,Lamar Ponte Avisio,,46.134620,11.110914,10110,2
5,28060z,Sp 85 Bivio Sopramonte,,46.085226,11.069313,10110,2
7,24405z,Maso Bolleri,,46.102485,11.124174,10110,2
8,24405x,Maso Bolleri,,46.102234,11.123940,10110,2
9,25205x,Borino,,46.067367,11.165050,10110,2

Come ci aspettiamo da un buon file CSV, nella prima riga costituisce le intestazioni e vediamo che i campi sono separati da virgole.

✪ DOMANDA 1.2: Dove possiamo trovare il significato del file ?

Mostra risposta

2. Verifichiamo che i dati siano corretti

Come avrai intuito, il file stops.txt è un file CSV che contiene le informazioni riguardanti le stazioni degli autobus di Trento.

Supponiamo che tu voglia fare avere la lista di tutte le fermate su strade provinciali (sigla SP):

✪ DOMANDA 2.1: Cerca un po’ manualmente dentro il file completo: i dati sono sempre perfettamente regolari come ci piacerebbe? Riesci ad individuare dei criteri per filtrare le righe con strade provinciali ?

Mostra risposta

✪✪ ESERCIZIO 2.2: Con quello che sai dalla lezione sui formati, prova ad aprire il CSV e stampare solo le linee che contengono qualcosa che assomiglia a strade provinciali.

NOTA: non serve che filtri per bene tutte, fai solo qualche tentativo per i casi più ovvi, usando funzioni sulle stringhe che già conosci. Ai casi più difficili ci penseremo in seguito con le regex!

SUGGERIMENTO 1: se devi usare diversi condizioni alternative in un if, separale con or

SUGGERIMENTO 2: usa il metodo upper delle stringhe:

Mostra soluzione
[1]:
# scrivi qui


['5', '28060z', 'Sp 85 Bivio Sopramonte', '', '46.085226', '11.069313', '10110', '2']
['48', '22080z', 'Sp 76 Carpenedi', '', '46.117195', '11.108678', '10110', '2']
['49', '22080x', 'Sp 76 Carpenedi', '', '46.117171', '11.108438', '10110', '2']
['104', '24040z', 'Sp 131 "Maso Pradiscola"', '', '46.083189', '11.135736', '10110', '2']
['105', '24040x', 'Sp 131 "Maso Pradiscola"', '', '46.083255', '11.135874', '10110', '1']
['109', '24350z', 'Sp 131 "Res. Silvana"', '', '46.097726', '11.126962', '10110', '2']
['110', '24350x', 'Sp 131 "Res. Silvana"', '', '46.097493', '11.126925', '10110', '2']
['115', '24355z', 'Sp 131 Al Maso Specchio', '', '46.104391', '11.123568', '10110', '2']
['116', '24355x', 'Sp 131 Al Maso Specchio', '', '46.104353', '11.123420', '10110', '2']
['131', '23040z', 'Sp 76 "Piac"', '', '46.126690', '11.114532', '10110', '1']
['133', '23035z', 'Sp 76 "Maregioli"', '', '46.130755', '11.121750', '10110', '2']
['134', '23035x', 'Sp 76 "Maregioli"', '', '46.130836', '11.121666', '10110', '1']
['139', '23045z', 'Sp 76 "Via Rossa"', '', '46.119858', '11.111416', '10110', '2']
['140', '23045x', 'Sp 76 "Via Rossa"', '', '46.119812', '11.111266', '10110', '2']
['143', '23055z', 'Sp 76 Dos di Lamar', '', '46.124939', '11.113463', '10110', '2']
['144', '23055x', 'Sp 76 Dos di Lamar', '', '46.124718', '11.113018', '10110', '1']
['154', '24520z', 'Sp 131 "Paganin"', '', '46.106288', '11.137610', '10110', '2']
['155', '24520x', 'Sp 131 "Paganin"', '', '46.106524', '11.137306', '10110', '2']
['245', '28045z', 'Sp 85 "Soraval"', '', '46.083258', '11.063955', '10110', '2']
['246', '28045x', 'Sp 85 "Soraval"', '', '46.083094', '11.063877', '10110', '2']
['439', '27050z', 'Sp 90 "Maso Prudenza"', '', '46.028867', '11.111521', '10110', '2']
['440', '27050x', 'Sp 90 "Maso Prudenza"', '', '46.029290', '11.111771', '10110', '2']
['1296', '32651x', "Sp.2 Loc. Beccache'", '', '45.882087', '11.071643', '10101', '2']
['1334', '32651z', "Sp.2 Loc. Beccache'", '', '45.882063', '11.071733', '10101', '2']
['1338', '32661x', 'Sp.2 Fr. Campolongo', '', '45.886995', '11.063902', '10101', '2']
['1339', '32671x', 'Sp.2 Noriglio', '', '45.884263', '11.070323', '10101', '2']
['1340', '32671z', 'Sp.2 Noriglio', '', '45.884371', '11.069842', '10101', '2']
['1341', '32661z', 'Sp.2 Fr. Campolongo', '', '45.886988', '11.063994', '10101', '2']
['2217', '35021x', 'Brancolino Sp.90', '', '45.900719', '11.020254', '15091', '1']
['2218', '35021z', 'Brancolino Sp.90', '', '45.900853', '11.020410', '15091', '1']
['2285', '38601z', 'Sp.20 S.Sisinio', '', '45.924877', '11.021092', '18511', '2']
['2286', '38611z', 'Sp.20 Zisi', '', '45.931009', '11.022102', '18511', '2']
['2287', '38611x', 'Sp.20 Zisi', '', '45.931416', '11.022116', '18511', '2']
['2288', '38601x', 'Sp.20 S.Sisinio', '', '45.924981', '11.021093', '18511', '2']
['2363', '38591x', 'Sp.20 Maso Tiaf', '', '45.936511', '11.008597', '18511', '2']
['2364', '38621x', 'Sp.20 Bivio Per Bordala', '', '45.930910', '11.007172', '18511', '2']
['2367', '38621z', 'Sp.20 Bivio Per Bordala', '', '45.930864', '11.007233', '18511', '2']
['2368', '38591z', 'Sp.20 Maso Tiaf', '', '45.935890', '11.008594', '18511', '2']
['2377', '23050x', 'Sp 76 Bivio S.Lazzaro', '', '46.129006', '11.115500', '10110', '2']
['2510', '32641z', 'Sp.2 Bivio Per Cisterna', '', '45.881281', '11.075365', '10101', '2']
['2843', '35161x', 'Sp.20 Bivio Noarna', '', '45.915825', '11.016559', '15091', '2']
['2844', '35161z', 'Sp.20 Bivio Noarna', '', '45.915880', '11.016803', '15091', '2']
['2897', '32218x', 'Sp.23 "Nero Cubo"', '', '45.855421', '11.002179', '10101', '2']
['2936', '37551z', 'Sp.89 Maso Brentegam', '', '45.880439', '11.050882', '10101', '2']
[2]:
"Ciao MONDO".upper()
[2]:
'CIAO MONDO'
[3]:
"SoftPython".upper()
[3]:
'SOFTPYTHON'

SUGGERIMENTO 3: usa anche il metodo find che ritorna la posizione di una sottostringa all’interno di un’altra.

[4]:
"ab cde".find('cd')
[4]:
3
[5]:
"ab cde".find(' c')
[5]:
2

Ricordati che gli indici delle stringhe iniziano da zero:

[6]:
"ab cde".find('a')
[6]:
0

Quando find non trova qualcosa ritorna -1 per segnalarlo:

[7]:
"ab cde".find('z')
[7]:
-1

Ricordati che find distingue tra maiuscole / minuscole, quindi non troverà la D maiuscola:

[8]:
"ab cde".find('D')
[8]:
-1

Discussione

Prosegui la lettura solo dopo aver provato l’esercizio precedente.

Pur avendo analizzato un file piccolo, sono spuntati fuori parecchi casi da trattare. Per poter quindi filtrare agevolmente insiemi grandi di dati senza specificare mille casi particolari, è bene cominciare a pensare a tutte le caratteristiche comuni dell’insieme di stringhe che vogliamo ottenere. Si può poi implementare dei filtri in Python usando le regex.

3. Introduzione alle regex

Vediamo cosa sono queste espressioni regolari:

WIKIPEDIA

Una espressione regolare (in lingua inglese regular expression o, in forma abbreviata, regexp, regex o RE) è una sequenza di simboli (quindi una stringa) che identifica un insieme di stringhe: essa definisce una funzione che prende in ingresso una stringa, e restituisce in uscita un valore del tipo sì/no, a seconda che la stringa segua o meno un certo pattern.

L’utilizzo di una espressione regolare è sicuramente più veloce perché ci permette di cercare non solo una stringa bensì un intero insieme di stringhe, detto appunto pattern. Una considerazione ESERCIZIO è che nonostante i concetti riguardo le regex siano universali, alcune implementazioni si differenziano nel comportamento in alcuni casi particolari oppure aggiungendo funzionalità non standard. Quindi, se già avete usate le regex nel vostro linguaggio preferito, state attenti a controllare le eventuali differenze con Python!

Stringhe e sequenze di escape

Le regex si esprimono usando semplici stringhe Python, per cui è meglio spendere 5 minuti per capire meglio alcune peculiarità delle stringhe. Quando indichiamo una stringa in Python, possiamo inserire delle squenze speciali dette sequenze di escape, come per esempio \n in "ciao\nSoftPython" che dice a Python che quando stampiamo la stringa, dopo aver stampato la stringa ciao deve andare a capo e quindi stampare la seguente SoftPython:

[9]:
print("ciao\nSoftPython")
ciao
SoftPython

Se non vogliamo che Python interpreti queste sequenze, perchè vogliamo che in fase di stampa sia invece proprio stampato il \n, possiamo aggiungere prima della stringa una r così :

[10]:
print(r"ciao\nSoftPython")
ciao\nSoftPython

La r prima dell’inizio della stringa serve ad indicare a Python che la seguente è una raw string, cioè una stringa in cui non deve espandere le sequenze di escape (cioè \ seguito da altri caratteri al fine di generare caratteri non stampabili, per esempio \n è il carattere di new-line).

✪ ESERCIZIO 3.1: magari già conosci le sequenze di escape, si trovano in molti linguaggi. Se non le conosci, prova a scrivere i comandi qua sotto, sempre in nuove celle:

  • print("ciao mondo")

  • print("ciao\tmondo")

  • print("ciao\nmondo")

  • print("ciao\rmondo") (questo è strano…)

  • print("ciao\\mondo")

Che differenze noti? E se metti il carattere r davanti alle stringhe (quindi subito prima del doppio apice ", come in r"hello"), che succede ?

Mostra soluzione
[11]:
# scrivi qui


ciao mondo
ciao    mondo
ciao
mondo
ciao\mondo
mondo
ciao mondo
ciao\tmondo
ciao\nmondo
ciao\\mondo
ciao\rmondo

La nostra prima regex

Proviamo ad eseguire la nostra prima regex:

[12]:
# diciamo a Python che vogliamo usare il modulo per usare le regex, che si chiama 're'
# NOTA: per quanto breve,  're' è proprio il nome del modulo, non un comando speciale !
import re

# usando la funzione search del modulo 're', effettuiamo una ricerca del pattern 'Sp'
# dentro 'Sp.89 Maso Brentegam'
re.search('Sp', 'Sp.89 Maso Brentegam')
[12]:
<_sre.SRE_Match object; span=(0, 2), match='Sp'>

Osservando span=(0, 2), match='Sp' nel risultato, si vede che Python ha individuato la stringa Sp dicendoci che inizia alla posizione 0 (inclusa) e termina alla posizione 2 (esclusa) di 'Sp.89 Maso Brentegam'

Possiamo ottenere i numeri delle posizioni con start() e end():

[13]:
re.search('Sp', 'Sp.89 Maso Brentegam').start()
[13]:
0
[14]:
re.search('Sp', 'Sp.89 Maso Brentegam').end()
[14]:
2

Proviamo ora a cercare un’altra stringa che sappiamo essere presente, come Maso:

[15]:
re.search('Maso', 'Sp.89 Maso Brentegam')
[15]:
<_sre.SRE_Match object; span=(6, 10), match='Maso'>

Notiamo che la stringa è stata trovata tra il sesto carattere (incluso) e il decimo (escluso)

ESERCIZIO: Prova ad estrarre qua sotto le posizioni in cui la stringa sopra è stata trovata

[16]:
import re
# scrivi qui

Proviamo ora a cercare una stringa che sappiamo non esserci, come 'blabla':

[17]:
print(re.search('blabla', 'Sp.89 Maso Brentegam'))
None

Vediamo che Python ci restituisce l’oggetto None, per indicare che non ha trovato nulla.

ESERCIZIO: Perchè in questo caso abbiamo messo il print? Per capirlo, prova a scrivere qui sotto la chiamata alla re.search senza usare la print, e vedi che succede (non dovrebbe succedere proprio niente perchè Jupyter di default non ci mostra gli oggetti None a meno che non li stampiamo esplicitamente)

Mostra soluzione
[18]:
import re
# scrivi qui


Maiuscole e minuscole

La ricerca nelle regex distingue tra maiuscole e minuscole, quindi se cerchiamo SP tutto maiuscolo in 'Sp.89 Maso Brentegam' non vedremo nulla come risultato:

[19]:
re.search('SP', 'Sp.89 Maso Brentegam')

Per dire a Python di ignorare maiuscole/minuscole, si può aggiungere il parametro re.I:

[20]:
re.search('SP', 'Sp.89 Maso Brentegam', re.I)
[20]:
<_sre.SRE_Match object; span=(0, 2), match='Sp'>

Notare come ora il parametro match sia più utile, perchè ci dice esattamente quali caratteri maiuscoli o minuscoli hanno fatto il match (i nquesto caso Sp).

Trovare tutti i match

re.search ritorna il primo match trovato. Se vogliamo trovarli tutti, bisogna usare finditer. Per esempio, possiamo provare a cercare la stringa ab in abcabd:

[21]:
for trovato in re.finditer('ab', 'abcabd'):
    print(trovato)

<_sre.SRE_Match object; span=(0, 2), match='ab'>
<_sre.SRE_Match object; span=(3, 5), match='ab'>

✪ ESERCIZIO 3.2: Se provi a stampare direttamente il risultato restituito re.finditer, non otterrai molte informazioni. Conosci un modo per ottenere una bella lista senza usare il for ?

Mostra soluzione
[22]:
# scrivi qui


[22]:
[<_sre.SRE_Match object; span=(0, 2), match='ab'>,
 <_sre.SRE_Match object; span=(3, 5), match='ab'>]

Il metacarattere punto

Proviamo ora una regex un po’ più interessante, per esempio cerchiamo nella stringa abcabd tutte le stringhe che iniziano con b e sono seguite da un solo qualsiasi carattere. Come facciamo ad indicare che vogliamo un solo carattare, ma senza specificare quale ? Possiamo usare il metacarattere punto ., che ha un significato speciale nelle regex e agisce come jolly:

[23]:
for trovato in re.finditer('b.', 'abcabd'):
    print(trovato)
<_sre.SRE_Match object; span=(1, 3), match='bc'>
<_sre.SRE_Match object; span=(4, 6), match='bd'>

✪ ESERCIZIO 3.3: Prova a cercare tutte le stringhe in abcabd che iniziano con a e sono seguite da due caratteri qualsiasi:

Mostra soluzione
[24]:
import re

# scrivi qui


<_sre.SRE_Match object; span=(0, 3), match='abc'>
<_sre.SRE_Match object; span=(3, 6), match='abd'>

✪ ESERCIZIO 3.4: Prova a cercare tutte le stringhe in abcabd che iniziano con un carattere qualsiasi e sono seguite da b:

Mostra soluzione
[25]:
import re
# scrivi qui


<_sre.SRE_Match object; span=(0, 2), match='ab'>
<_sre.SRE_Match object; span=(3, 5), match='ab'>

Abbiamo capito che i punti sono caratteri speciali. E se volessimo cercare invece proprio un punto, per distinguire per esempio Sp.89 Maso Brentegam da Sp 85 Bivio Sopramonte che invece ha lo spazio dopo Sp? Per filtIn questo caso, prima del punto dovremmo usare il carattere di barra rovesciata \ , detto anche carattere di escape:

[26]:
re.search('Sp\.', 'Sp.89 Maso Brentegam')
[26]:
<_sre.SRE_Match object; span=(0, 3), match='Sp.'>

Mettendo Sp 85 Bivio Sopramonte non dovrebbe trovare nulla (lo spazio non è un punto!):

[27]:
re.search('Sp\.', 'Sp 85 Bivio Sopramonte')

✪✪ ESERCIZIO 3.5: Prova a trovare tutte le stringhe che contengono un punto seguito da qualsiasi carattere nella stringa 'il.corso.soft.python' (dovrebbe trovare match per .c, .s e .p):

Mostra soluzione
[28]:
import re

# scrivi qui


<_sre.SRE_Match object; span=(2, 4), match='.c'>
<_sre.SRE_Match object; span=(8, 10), match='.s'>
<_sre.SRE_Match object; span=(13, 15), match='.p'>

Scriviamoci una funzione di test

Dato che scrivere i comandi per stampare i risultati dei test può diventare ripetitivo e noioso, ci conviene creare una funzione con dentro delle istruzioni di stampa da eseguire automaticamente (per più info sulle funzioni, vedi capitolo 3 Pensare in Python):

[29]:
import re

def test_regex(pattern):
    names = ['Baselga del Bondone',
             'Spini Bregenz',
             'Sp.89 Maso Brentegam',
             'sp 90 "Maso Prudenza"',
             'Brancolino Sp.90']
    for name in names:
        print(re.search(pattern, name, re.I))

test_regex("SP")
None
<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='sp'>
<_sre.SRE_Match object; span=(11, 13), match='Sp'>

In Python, le funzioni si dichiarano con la parola riservata def seguita dal nome della funzione che scegliamo arbitrariamente noi. In questo caso il nome scelto è test_regex:

def test_regex(pattern):

Poi, a questa funzione abbiamo deciso che bisognerà passare un parametro, che chiamiamo pattern (ma potremmo chiamarlo come ci pare, anche pippo).

NOTA: alla fine della prima riga, ci sono dei due punti : se dimentichi di metterli potresti trovarti con strani errori di syntassi !

La nostra funzione farà qualcosa con questa variabile che abbiamo chiamato pattern:

def test_regex(pattern):
    names = ['Baselga Del Bondone',
             'Spini Bregenz',
             'Sp.89 Maso Brentegam',
             'sp 90 "Maso Prudenza"',
             'Brancolino Sp.90']
    for name in names:
        print(re.search(pattern, name, re.I))

In questo caso, per ciascun nome, eseguirà questa riga (vedremo in seguito il contenuto della print):

print(re.search(pattern, name, re.I))

sfruttando la variabile pattern che passeremo al momento di chiamare la funzione:

[30]:
test_regex("SP")
None
<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='sp'>
<_sre.SRE_Match object; span=(11, 13), match='Sp'>

✪ ESERCIZIO 3.6: Copia a mano la funzione di sopra qua sotto, ed eseguila con Control + Invio:

Mostra soluzione
[31]:
# scrivi qui


<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='Sp'>
<_sre.SRE_Match object; span=(0, 2), match='sp'>
<_sre.SRE_Match object; span=(11, 13), match='Sp'>

Filtriamo bene gli sp

È ora di usare la nostra prima regex ‘seria’, per farlo dobbiamo guardare le differenze tra le stringhe corrette e quella errata: in questo caso sappiamo che ogni strada provinciale ha un numero. Proviamo con questa regex:

[32]:
test_regex(r"sp.\d\d")
None
<_sre.SRE_Match object; span=(0, 5), match='Sp.89'>
<_sre.SRE_Match object; span=(0, 5), match='sp 90'>
<_sre.SRE_Match object; span=(11, 16), match='Sp.90'>

Tornando alla regex r"sp.\d\d":

  • Come detto nella sezione precedente, la r prima dell’inizio della stringa serve ad indicare a Python che la seguente è una raw string, cioè una stringa in cui non deve espandere le sequenze di escape (cioè \ seguito da altri caratteri al fine di generare caratteri non stampabili, per esempio \n è il carattere di new-line).

  • Abbiamo visto che le lettere dell’alfabeto (e i numeri) hanno semplicemente il loro valore.

  • Come detto in precedenza, il carattere . in questo caso è un metacarattere che in questo caso si comporta come un “jolly” e può identificare qualsiasi carattere ad eccezione de carattere di fine riga (solitamente).

  • \d sono due caratteri ma ai fini della regex sono da considerarsi uno solo. Ogni volta che vediamo il carattere \ è da considerarsi assieme al carattere successivo. Il significato in questo caso è una qualsiasi cifra tra 0 e 9.

✪✪✪ ESERCIZIO 3.7: Prova ad integrare l’esempio di regex qui sopra con quello di lettura del file stops.txt precedente, usando nell’if le regex invece delle find (se non lo hai risolto prendilo dalle soluzioni):

Mostra soluzione
[33]:
# scrivi qui


['5', '28060z', 'Sp 85 Bivio Sopramonte', '', '46.085226', '11.069313', '10110', '2']
['48', '22080z', 'Sp 76 Carpenedi', '', '46.117195', '11.108678', '10110', '2']
['49', '22080x', 'Sp 76 Carpenedi', '', '46.117171', '11.108438', '10110', '2']
['104', '24040z', 'Sp 131 "Maso Pradiscola"', '', '46.083189', '11.135736', '10110', '2']
['105', '24040x', 'Sp 131 "Maso Pradiscola"', '', '46.083255', '11.135874', '10110', '1']
['109', '24350z', 'Sp 131 "Res. Silvana"', '', '46.097726', '11.126962', '10110', '2']
['110', '24350x', 'Sp 131 "Res. Silvana"', '', '46.097493', '11.126925', '10110', '2']
['115', '24355z', 'Sp 131 Al Maso Specchio', '', '46.104391', '11.123568', '10110', '2']
['116', '24355x', 'Sp 131 Al Maso Specchio', '', '46.104353', '11.123420', '10110', '2']
['131', '23040z', 'Sp 76 "Piac"', '', '46.126690', '11.114532', '10110', '1']
['133', '23035z', 'Sp 76 "Maregioli"', '', '46.130755', '11.121750', '10110', '2']
['134', '23035x', 'Sp 76 "Maregioli"', '', '46.130836', '11.121666', '10110', '1']
['139', '23045z', 'Sp 76 "Via Rossa"', '', '46.119858', '11.111416', '10110', '2']
['140', '23045x', 'Sp 76 "Via Rossa"', '', '46.119812', '11.111266', '10110', '2']
['143', '23055z', 'Sp 76 Dos di Lamar', '', '46.124939', '11.113463', '10110', '2']
['144', '23055x', 'Sp 76 Dos di Lamar', '', '46.124718', '11.113018', '10110', '1']
['154', '24520z', 'Sp 131 "Paganin"', '', '46.106288', '11.137610', '10110', '2']
['155', '24520x', 'Sp 131 "Paganin"', '', '46.106524', '11.137306', '10110', '2']
['245', '28045z', 'Sp 85 "Soraval"', '', '46.083258', '11.063955', '10110', '2']
['246', '28045x', 'Sp 85 "Soraval"', '', '46.083094', '11.063877', '10110', '2']
['439', '27050z', 'Sp 90 "Maso Prudenza"', '', '46.028867', '11.111521', '10110', '2']
['440', '27050x', 'Sp 90 "Maso Prudenza"', '', '46.029290', '11.111771', '10110', '2']
['2217', '35021x', 'Brancolino Sp.90', '', '45.900719', '11.020254', '15091', '1']
['2218', '35021z', 'Brancolino Sp.90', '', '45.900853', '11.020410', '15091', '1']
['2285', '38601z', 'Sp.20 S.Sisinio', '', '45.924877', '11.021092', '18511', '2']
['2286', '38611z', 'Sp.20 Zisi', '', '45.931009', '11.022102', '18511', '2']
['2287', '38611x', 'Sp.20 Zisi', '', '45.931416', '11.022116', '18511', '2']
['2288', '38601x', 'Sp.20 S.Sisinio', '', '45.924981', '11.021093', '18511', '2']
['2363', '38591x', 'Sp.20 Maso Tiaf', '', '45.936511', '11.008597', '18511', '2']
['2364', '38621x', 'Sp.20 Bivio Per Bordala', '', '45.930910', '11.007172', '18511', '2']
['2367', '38621z', 'Sp.20 Bivio Per Bordala', '', '45.930864', '11.007233', '18511', '2']
['2368', '38591z', 'Sp.20 Maso Tiaf', '', '45.935890', '11.008594', '18511', '2']
['2377', '23050x', 'Sp 76 Bivio S.Lazzaro', '', '46.129006', '11.115500', '10110', '2']
['2843', '35161x', 'Sp.20 Bivio Noarna', '', '45.915825', '11.016559', '15091', '2']
['2844', '35161z', 'Sp.20 Bivio Noarna', '', '45.915880', '11.016803', '15091', '2']
['2897', '32218x', 'Sp.23 "Nero Cubo"', '', '45.855421', '11.002179', '10101', '2']
['2936', '37551z', 'Sp.89 Maso Brentegam', '', '45.880439', '11.050882', '10101', '2']

4. Sintassi delle Python RegEx

Proviamo ora a guardare alcuni meta-caratteri importanti nelle regular expression in Python - scusate per l’orribile tabella ma fare tabelle in Jupyter è sempre problematico

Metacaratteri

Modificatore

Descrizione

\

usato come escape (cioè segnalare che il carattere a seguire, nonostante sia un carattere speciale deve essere trattato come se non lo fosse) o per iniziare una sequenza (vedi sotto).

.

usato per rappresentare un qualsiasi carattere ad eccezione della nuova linea (con l’opzione re.A possiamo rimuovere anche questa eccezione).

^

usato per indicare l’inizio della riga

$

usato per indicare la fine della riga

[...]

usate per racchiudere l’insieme di caratteri che verificano questa espressione regolare

[^...]

usate per racchiudere l’insieme di caratteri che se presenti NON verificano questa espressione regolare

A | B

usato per indicare una rappresentazione alternativa, è valida sia che appaia A, sia che appaia B

()

usate come in matematica per indicare la precedenza sulle operazioni

Nel codice qui sotto definiamo la funzione test_regex_num() che è come quella già ma vista, ma usando dei numeri di telefono al posto dei nomi delle fermate; per ogni numero controlliamo se l’espressione regolare viene soddisfatta e se lo è lo stampiamo ed in fine testiamo varie proprietà di questi numeri di telefono.

[34]:
import re
def test_regex_num(pattern):
    numbers = ['3471234567',  #NOTA: i numeri sono in formato stringa !!!!
             '3303303367',
             '3232123323',
             '3383123222']
    for num in numbers:
        if re.search(pattern, num):
            print(num)
    print("-----")

print("Tutti i numeri che contengono 33")
test_regex_num("33")
print("Tutti i numeri che iniziano per 33")
test_regex_num("^33")
print("Tutti i numeri che hanno come penultima cifra 2")
test_regex_num("2.$")
print("Tutti i numeri che contengono 212 o 312")
test_regex_num("212|312")
# Oppure
test_regex_num("[23]12")
# Oppure
test_regex_num("(2|3)12")

Tutti i numeri che contengono 33
3303303367
3232123323
3383123222
-----
Tutti i numeri che iniziano per 33
3303303367
3383123222
-----
Tutti i numeri che hanno come penultima cifra 2
3232123323
3383123222
-----
Tutti i numeri che contengono 212 o 312
3232123323
3383123222
-----
3232123323
3383123222
-----
3232123323
3383123222
-----

✪✪ ESERCIZIO 4.1 Prova a scrivere qua sotto i pattern che soddisfano le proprietà richieste nelle chiamate a test_regex_num. Cerca di non guardare all’esercizio precedente:

[35]:
import re

def test_regex_num(pattern):
    numbers = ['3471234567',
             '3303303367',
             '3232123323',
             '3383123222']
    for num in numbers:
        if re.search(pattern, num):
            print(num)
    print("-----")

print("Tutti i numeri che contengono 32")
test_regex_num("")    # metti il pattern giusto

print("Tutti i numeri che finiscono per 67")
test_regex_num("")

print("Tutti i numeri che hanno come quarta cifra 3")
test_regex_num("")

print("Tutti i numeri che contengono 232 o 233 o 234")
test_regex_num("")
# Oppure
test_regex_num("")
# Oppure
test_regex_num("")
Tutti i numeri che contengono 32
3471234567
3303303367
3232123323
3383123222
-----
Tutti i numeri che finiscono per 67
3471234567
3303303367
3232123323
3383123222
-----
Tutti i numeri che hanno come quarta cifra 3
3471234567
3303303367
3232123323
3383123222
-----
Tutti i numeri che contengono 232 o 233 o 234
3471234567
3303303367
3232123323
3383123222
-----
3471234567
3303303367
3232123323
3383123222
-----
3471234567
3303303367
3232123323
3383123222
-----
Mostra soluzione
[36]:

Tutti i numeri che contengono 32
3232123323
3383123222
-----
Tutti i numeri che finiscono per 67
3471234567
3303303367
-----
Tutti i numeri che hanno come quarta cifra 3
3303303367
3383123222
-----
Tutti i numeri che contengono 232 o 233 o 234
3471234567
3232123323
3383123222
-----
3471234567
3232123323
3383123222
-----
3471234567
3232123323
3383123222
-----

Ripetizioni

Le espressioni regolari possono anche gestire delle ripetizioni di particolari pattern utilizzando altri caratteri speciali.

Modificatori

Descrizione

{m, n}

il carattere o gruppo a cui è riferito viene ripetuto almeno m volte fino ad un massimo di n volte.

{m}

il carrattere o il gruppo a cui è riferito viene ripetuto esattamente m volte

?

il carattere o il gruppo a cui è riferito viene ripetuto 0 o 1 volta. Equivale a {,1}.

*

il carattere o il gruppo a cui è riferito viene ripetuto 0 o più volte . Equivale a {,}.

+

il carattere o il gruppo a cui è riferito viene ripetuto 1 o più volte. Equivale a {1,}.

I caratteri o gruppi a cui si riferiscono i modificatori delle ripetizioni appena precedenti ad essi, vediamo un esempio

[37]:
print(re.search("a+b", "aaaabbb"))
print(re.search("a+b", "bab"))
print(re.search("a+b", "bbb"))
print(re.search("a+b", "aaaa"))
<_sre.SRE_Match object; span=(0, 5), match='aaaab'>
<_sre.SRE_Match object; span=(1, 3), match='ab'>
None
None

Come pui vedere nell’esempio qui sopra il pattern a+b indica a una o più volte, seguito da una b. Quando si ha un match puoi vedere nell’oggetto ritornato dal metodo re.search() la sottostringa che ha verificato la regex (usando il metodo group()) e gli indici della posizione di essa all’interno della stringa (chiamando il metodo .span()).

Funzione pass_n_fail

Qui sotto la funzione pass_n_fail() prende come parametri un pattern e due liste di stringhe: pass_list e fail_list. La funzione verifica se quelle che appartengono alla prima lista matchano l’espressione regolare e se quelle nella seconda non la matchano:

[38]:
def pass_n_fail(pattern, pass_list, fail_list):
    for p in pass_list:
        if not re.search(pattern, p):
            print("ERRORE: '{}' non matcha il pattern '{}' ma dovrebbe farlo!".format(p, pattern))
    for f in fail_list:
        if re.search(pattern, f):
            print("ERRORE: '{}' matcha il pattern '{}' ma non dovrebbe farlo!".format(f, pattern))

Guardiamo quest’esempio:

NOTA: il fatto che nell’output dell’esempio sia scritto ERRORE è voluto, lo scopo della funzione è proprio segnalarci che non abbiamo messo i parametri giusti !

[39]:

# ESEMPIO
pass_n_fail("c",
            ["aa","a"],  # espressioni che vorremmo matchassero il pattern "c"
            ["b","c"]    # espressioni che vorremmo NON matchassero il pattern "c"
           )
ERRORE: 'aa' non matcha il pattern 'c' ma dovrebbe farlo!
ERRORE: 'a' non matcha il pattern 'c' ma dovrebbe farlo!
ERRORE: 'c' matcha il pattern 'c' ma non dovrebbe farlo!

Quest’esempio qua invece non dovrebbe darci nessun output, perchè le stringhe "aa" che "a matchano il pattern "a", e le stringhe "b" e "c" non matchano il pattern "a":

[40]:

# ESEMPIO
pass_n_fail("a",
            ["aa","a"],  # espressioni che vorremmo matchassero il pattern "a"
            ["b","c"]    # espressioni che vorremmo NON matchassero il pattern "a"
           )

✪✪✪ ESERCIZIO 4.2: Prova tu a fornire esempi nelle due liste qua sotto (non preoccuparti se vedi scritto ERRORE nell’output della cella prima ancora di cominciare, se metti esempi giusti ed esegui la cella i messaggi di errore dovrebbero sparire):

[41]:
pass_n_fail("a",
            ["",""], # metti esempi che matchano
            ["",""]  # metti esempi che non matchano
           )
ERRORE: '' non matcha il pattern 'a' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern 'a' ma dovrebbe farlo!
Mostra soluzione
[42]:

[43]:
 # Come sopra, ma un po più difficile:
pass_n_fail("ab+",
            ["",""],
            ["",""]
           )
ERRORE: '' non matcha il pattern 'ab+' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern 'ab+' ma dovrebbe farlo!
Mostra soluzione
[44]:

[45]:
# Come sopra, un po più difficile ancora:
pass_n_fail("ab*",
            ["",""],
            ["",""]
           )
ERRORE: '' non matcha il pattern 'ab*' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern 'ab*' ma dovrebbe farlo!
Mostra soluzione
[46]:

[47]:
# OK! Ancora un paio
pass_n_fail("^[ab]+[^ab]$",
            ["",""],
            ["",""]
           )
ERRORE: '' non matcha il pattern '^[ab]+[^ab]$' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern '^[ab]+[^ab]$' ma dovrebbe farlo!
Mostra soluzione
[48]:

[49]:
# L'ultima
pass_n_fail(".?\.{3}$",
            ["",""], #TODO
            ["",""]
           )
ERRORE: '' non matcha il pattern '.?\.{3}$' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern '.?\.{3}$' ma dovrebbe farlo!
Mostra soluzione
[50]:

5. Sequenze

A volte vogliamo considerare insiemi molto grandi di possibili simboli in una espressione regolare: per esempio se vogliamo validare la struttura di un indirizzo email vogliamo controllare che

  • contenga almeno 3 caratteri

  • una @

  • altri 3 caratteri

  • un punto

  • e almeno altri 2 caratteri.

Il problema è che alcuni caratteri non possono essere presenti nelle email (come ad esempio \|{}()[] etc…), e se dovessimo scrivere un set di caratteri da escludere usando l’espressione [^...] ci costerebbe molto tempo e spazio, inoltre sarebbe facile dimenticarsi qualche simbolo e quasi impossibile da leggere.

Per ovviare a questo problema sono stati introdotte delle scorciatoie: delle sequenze di simboli che vanno a sostituire lunghi set di caratteri di comune utilizzo, eccone alcuni:

Sequenza

Descrizione

\A

Inizio della stringa (simile a ^)

\b

Valida la stringa vuota che delimita una parola

\d

Cifre da 0 a 9

\D

Tutto eccetto le cifre

\s

Spaziature

\S

Tutto eccetto le spaziature

\w

Tutti i caratteri alfanumerici e _

\W

Tutto eccetto i caratteri alfanumerici e l’underscore _

\Z

Fine della stringa (simile a $)

✪✪✪ ESERCIZIO 5.1: Proviamo a variare l’esercizio precedente adesso quando matcherà sarai tu a scrivere dei pattern che verifichi le stringhe nella pass_list ed escluda quelle nella fail_list, dove troverai il pattern sarà come l’esercizio precedente (di nuovo non preoccuparti se vedi ‘ERRORE’ scritto sotto le celle prima ancora di iniziare, se metti i giusti pattern / esempi come richiesto le scritte ‘ERRORE’ dovrebbero scomparire )

[51]:
#Scegli il PATTERN giusto !
pass_n_fail(r"",
            ["3 ramarri", "2 carri"],  # queste devono matchare
            ["tre ramarri", "due carri", "3 ", "2 "]  # queste non devono matchare
           )
ERRORE: 'tre ramarri' matcha il pattern '' ma non dovrebbe farlo!
ERRORE: 'due carri' matcha il pattern '' ma non dovrebbe farlo!
ERRORE: '3 ' matcha il pattern '' ma non dovrebbe farlo!
ERRORE: '2 ' matcha il pattern '' ma non dovrebbe farlo!
Mostra soluzione
[52]:

[53]:
#Scegli le STRINGHE
pass_n_fail(r"^(w{3}\.)?\w\w+\.\w\w+",
            ["",""],
            ["",""]
           )

ERRORE: '' non matcha il pattern '^(w{3}\.)?\w\w+\.\w\w+' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern '^(w{3}\.)?\w\w+\.\w\w+' ma dovrebbe farlo!
Mostra soluzione
[54]:

[55]:
#Scegli il PATTERN
pass_n_fail(r"",
            ["21.12.2017", "11/01/2018", "16-12-89"],
            ["1621211003", "11/20/2010", "12-12-123"]
           )
ERRORE: '1621211003' matcha il pattern '' ma non dovrebbe farlo!
ERRORE: '11/20/2010' matcha il pattern '' ma non dovrebbe farlo!
ERRORE: '12-12-123' matcha il pattern '' ma non dovrebbe farlo!
Mostra soluzione
[56]:

6. Le funzioni della libreria re

Fino ad ora abbiamo usato una sola funzione della libreria re di Python, ossia re.search() ma sono presenti anche altre funzionalità, la più simile è il metodo re.match():

[57]:
print(re.search('c', 'abcde'))
<_sre.SRE_Match object; span=(2, 3), match='c'>
[58]:
print(re.match('c', 'abcde'))
None
[59]:
print(re.match('a', 'abcde'))
<_sre.SRE_Match object; span=(0, 1), match='a'>

✪✪ DOMANDA 6.1: Riesci a capire la differenza? help(re.match) e help(re.search) possono tornarti utili.

Mostra risposta

7. Sostituzioni con re.sub

Le espressioni regolari possono anche essere utilizzate per sostituire del testo, un po’ come il metodo replace() sulle stringhe. Quando chiamiamo il metodo re.sub(), per ricevere in output la stringa elaborata, dobbiamo passare come argomenti:

  1. il pattern della regular expression

  2. il testo da sostituire

  3. la stringa su cui effettuare la ricerca

Tornando all’esempio delle fermate dell’autobus, adesso vogliamo sostituire tutto quel confusionario “Sp” in “strade provinciali”, vediamo il codice:

[60]:
help(re.sub)
Help on function sub in module re:

sub(pattern, repl, string, count=0, flags=0)
    Return the string obtained by replacing the leftmost
    non-overlapping occurrences of the pattern in string by the
    replacement repl.  repl can be either a string or a callable;
    if a string, backslash escapes in it are processed.  If it is
    a callable, it's passed the match object and must return
    a replacement string to be used.

[61]:
import csv
import re
with open('stops.txt', newline='') as f:
    reader = csv.reader(f, delimiter=',')

    for row in reader:
        nuova = re.sub(r'Sp.(\d+)', r'Strada Provinciale \1',  row[2])
        if nuova != row[2]:
            print(row[2], 'DIVENTA', nuova)
Sp 85 Bivio Sopramonte DIVENTA Strada Provinciale 85 Bivio Sopramonte
Sp 76 Carpenedi DIVENTA Strada Provinciale 76 Carpenedi
Sp 76 Carpenedi DIVENTA Strada Provinciale 76 Carpenedi
Sp 131 "Maso Pradiscola" DIVENTA Strada Provinciale 131 "Maso Pradiscola"
Sp 131 "Maso Pradiscola" DIVENTA Strada Provinciale 131 "Maso Pradiscola"
Sp 131 "Res. Silvana" DIVENTA Strada Provinciale 131 "Res. Silvana"
Sp 131 "Res. Silvana" DIVENTA Strada Provinciale 131 "Res. Silvana"
Sp 131 Al Maso Specchio DIVENTA Strada Provinciale 131 Al Maso Specchio
Sp 131 Al Maso Specchio DIVENTA Strada Provinciale 131 Al Maso Specchio
Sp 76 "Piac" DIVENTA Strada Provinciale 76 "Piac"
Sp 76 "Maregioli" DIVENTA Strada Provinciale 76 "Maregioli"
Sp 76 "Maregioli" DIVENTA Strada Provinciale 76 "Maregioli"
Sp 76 "Via Rossa" DIVENTA Strada Provinciale 76 "Via Rossa"
Sp 76 "Via Rossa" DIVENTA Strada Provinciale 76 "Via Rossa"
Sp 76 Dos di Lamar DIVENTA Strada Provinciale 76 Dos di Lamar
Sp 76 Dos di Lamar DIVENTA Strada Provinciale 76 Dos di Lamar
Sp 131 "Paganin" DIVENTA Strada Provinciale 131 "Paganin"
Sp 131 "Paganin" DIVENTA Strada Provinciale 131 "Paganin"
Sp 85 "Soraval" DIVENTA Strada Provinciale 85 "Soraval"
Sp 85 "Soraval" DIVENTA Strada Provinciale 85 "Soraval"
Sp 90 "Maso Prudenza" DIVENTA Strada Provinciale 90 "Maso Prudenza"
Sp 90 "Maso Prudenza" DIVENTA Strada Provinciale 90 "Maso Prudenza"
Sp.2 Loc. Beccache' DIVENTA Strada Provinciale 2 Loc. Beccache'
Sp.2 Loc. Beccache' DIVENTA Strada Provinciale 2 Loc. Beccache'
Sp.2 Fr. Campolongo DIVENTA Strada Provinciale 2 Fr. Campolongo
Sp.2 Noriglio DIVENTA Strada Provinciale 2 Noriglio
Sp.2 Noriglio DIVENTA Strada Provinciale 2 Noriglio
Sp.2 Fr. Campolongo DIVENTA Strada Provinciale 2 Fr. Campolongo
Brancolino Sp.90 DIVENTA Brancolino Strada Provinciale 90
Brancolino Sp.90 DIVENTA Brancolino Strada Provinciale 90
Sp.20 S.Sisinio DIVENTA Strada Provinciale 20 S.Sisinio
Sp.20 Zisi DIVENTA Strada Provinciale 20 Zisi
Sp.20 Zisi DIVENTA Strada Provinciale 20 Zisi
Sp.20 S.Sisinio DIVENTA Strada Provinciale 20 S.Sisinio
Sp.20 Maso Tiaf DIVENTA Strada Provinciale 20 Maso Tiaf
Sp.20 Bivio Per Bordala DIVENTA Strada Provinciale 20 Bivio Per Bordala
Sp.20 Bivio Per Bordala DIVENTA Strada Provinciale 20 Bivio Per Bordala
Sp.20 Maso Tiaf DIVENTA Strada Provinciale 20 Maso Tiaf
Sp 76 Bivio S.Lazzaro DIVENTA Strada Provinciale 76 Bivio S.Lazzaro
Sp.2 Bivio Per Cisterna DIVENTA Strada Provinciale 2 Bivio Per Cisterna
Sp.20 Bivio Noarna DIVENTA Strada Provinciale 20 Bivio Noarna
Sp.20 Bivio Noarna DIVENTA Strada Provinciale 20 Bivio Noarna
Sp.23 "Nero Cubo" DIVENTA Strada Provinciale 23 "Nero Cubo"
Sp.89 Maso Brentegam DIVENTA Strada Provinciale 89 Maso Brentegam

Bene, ma cosa succede? Conosciamo già tutto fino alla linea 3, poi cerchiamo il pattern Sp.(\d+) ; nota che ho messo delle parentesi dove sembrerebbe non servano ma fai attenzione al prossimo parametro (cioè la stringa sostitutiva): ti accorgerai di un carattere speciale di una sequenza particolare di cui non abbiamo parlato, ovvero \1 . Questo tipo di sequenza è detta gruppo di backreference e viene definito nel pattern da cercare utilizzando le parentesi tonde appunto: tutto quello tra ( and ) diventa un gruppo e per richiamarlo è necessario soltando aggiungere un backslash seguito dal numero del gruppo, contando da sinistra a destra.

Nel nostro caso esiste un solo gruppo quindi non possiamo sbagliare: la ricerca prima esamina il testo per trovare Sp seguito da un carattere e poi (da un gruppo definito come) una o più cifre. La stringa di sostituzione riporterà prima la scritta Strada Provinciale poi uno spazio ed infine la cifra che ha verificato il gruppo definito nel pattern di ricerca.

✪✪✪ ESERCIZIO 7.1: Prova a ricopiare il codice precedente qua sotto, e compi la stessa operazione ma questa volta con le Strade Statali. Riesci a scrivere pattern diversi di ricerca e sostituzione per farlo?

Mostra soluzione
[62]:
# scrivi qui il metodo 1


['64', '22075x', 'Gardolo Svincolo Strada Statale 12', '', '46.106227', '11.109072', '10110', '1']
['72', '22230z', 'Strada Statale 12 "Zona Ind.Le Ftm"', '', '46.119309', '11.106123', '10110', '2']
['94', '26040z', 'Strada Statale 12 "Casteller"', '', '46.025284', '11.132751', '10110', '2']
['95', '26040x', 'Strada Statale 12 "Casteller"', '', '46.025707', '11.132791', '10110', '2']
['99', '26045z', 'Strada Statale 12 "Le Caverne"', '', '46.030862', '11.132677', '10110', '2']
['100', '26045x', 'Strada Statale 12 "Le Caverne"', '', '46.030338', '11.132911', '10110', '1']
['158', '28055z', 'Strada Statale 45 "Montevideo"', '', '46.080499', '11.098333', '10110', '2']
['214', '22230x', 'Strada Statale 12 "Zona Ind.Le Ftm"', '', '46.120242', '11.105822', '10110', '2']
['278', '21680z', 'Strada Statale 47 Muralta', '', '46.077059', '11.131773', '10110', '2']
['286', '22710x', 'Strada Statale 12 "Pioppeto"', '', '46.108369', '11.108528', '10110', '1']
['287', '22715x', 'Strada Statale 12 Bivio Meano', '', '46.116062', '11.106197', '10110', '2']
['288', '22720x', 'Strada Statale 12 Canova', '', '46.098985', '11.111739', '10110', '1']
['289', '22705x', 'Strada Statale 12 "Paludi"', 'via Bolzano incrocio via Noce', '46.102643', '11.110490', '10110', '2']
['290', '22720z', 'Strada Statale 12 Canova', '', '46.100264', '11.111578', '10110', '1']
['340', '25815z', 'Strada Statale 349 Pozzata', '', '46.039534', '11.144070', '10110', '2']
['1350', '31221z', 'Lizzanella Strada Statale 12', '', '45.875522', '11.031822', '10101', '2']
['1351', '31221x', 'Lizzanella Strada Statale 12', '', '45.875686', '11.032030', '10101', '2']
['1422', '32221z', 'Strada Statale 12 "Rovercenter"', '', '45.864490', '11.029338', '10101', '2']
['2289', '32251x', 'Strada Statale 240 "Millennium Sud"', '', '45.868678', '11.014119', '10101', '2']
['2291', '32251z', 'Strada Statale 240 "Millennium Sud"', '', '45.868802', '11.014170', '10101', '2']
['2293', '32241z', 'Strada Statale 240 "Millennium Nord"', '', '45.870320', '11.016768', '10101', '2']
['2294', '32241x', 'Strada Statale 240 "Millennium Nord"', '', '45.870588', '11.017358', '10101', '2']
['2337', '39041z', 'Volano Strada Statale 12', '', '45.917871', '11.064024', '19011', '2']
['2338', '39041x', 'Volano Strada Statale 12', '', '45.917904', '11.063866', '19011', '2']
['2418', '22710z', 'Strada Statale 12 "Pioppeto"', '', '46.108623', '11.108717', '10110', '1']
['2419', '22730z', 'Strada Statale 12 S.Anna', '', '46.112409', '11.107438', '10110', '2']
['2438', '22725z', 'Strada Statale 12 Talvera', '', '46.103288', '11.110545', '10110', '1']
['2500', '21685z', 'Strada Statale 45 "Scala"', '', '46.077367', '11.099792', '10110', '2']
['2514', '34101x', "Strada Statale 240 Ponte Sull'adige", '', '45.863191', '10.997289', '14051', '2']
['2515', '34101z', "Strada Statale 240 Ponte Sull'adige", '', '45.863235', '10.997355', '14051', '2']
['2699', '32221x', 'Strada Statale 12 "Rovercenter"', '', '45.864206', '11.029050', '10101', '2']
['2701', '25815x', 'Strada Statale 349 Pozzata', '', '46.041779', '11.142383', '10110', '2']
['2780', '21687z', 'Strada Statale 45 S.Giorgio', '', '46.078668', '11.099302', '10110', '2']
['2806', '32236x', 'Strada Statale 240 "Alla Staffa"', '', '45.872760', '11.024323', '10101', '2']
['2838', '39061z', 'Strada Statale 12 Fornaci', '', '45.919443', '11.085327', '19011', '2']
['2856', '36511z', 'Strada Statale 12 Castel Pietra', '', '45.921721', '11.091309', '16501', '2']
['2857', '36511x', 'Strada Statale 12 Castel Pietra', '', '45.922938', '11.092225', '16501', '2']
['2858', '36521x', 'Strada Statale 12 Campagnole', '', '45.928854', '11.093011', '16501', '2']
['2859', '36521z', 'Strada Statale 12 Campagnole', '', '45.928851', '11.093153', '16501', '2']
['2870', '32236z', 'Strada Statale 240 "Alla Staffa"', '', '45.872820', '11.024167', '10101', '2']
['2877', '32256x', 'Strada Statale 240 "Meb"', '', '45.867858', '11.011826', '10101', '2']
['2906', '36541z', 'Calliano Strada Statale 12', '', '45.934315', '11.096017', '16501', '2']
['2907', '36541x', 'Calliano Strada Statale 12', '', '45.934334', '11.095870', '16501', '2']
Mostra soluzione
[63]:
# scrivi qui il metodo 2


['64', '22075x', 'Gardolo Svincolo Strada Statale.12', '', '46.106227', '11.109072', '10110', '1']
['72', '22230z', 'Strada Statale 12 "Zona Ind.Le Ftm"', '', '46.119309', '11.106123', '10110', '2']
['94', '26040z', 'Strada Statale 12 "Casteller"', '', '46.025284', '11.132751', '10110', '2']
['95', '26040x', 'Strada Statale 12 "Casteller"', '', '46.025707', '11.132791', '10110', '2']
['99', '26045z', 'Strada Statale 12 "Le Caverne"', '', '46.030862', '11.132677', '10110', '2']
['100', '26045x', 'Strada Statale 12 "Le Caverne"', '', '46.030338', '11.132911', '10110', '1']
['158', '28055z', 'Strada Statale 45 "Montevideo"', '', '46.080499', '11.098333', '10110', '2']
['214', '22230x', 'Strada Statale 12 "Zona Ind.Le Ftm"', '', '46.120242', '11.105822', '10110', '2']
['278', '21680z', 'Strada Statale 47 Muralta', '', '46.077059', '11.131773', '10110', '2']
['286', '22710x', 'Strada Statale 12 "Pioppeto"', '', '46.108369', '11.108528', '10110', '1']
['287', '22715x', 'Strada Statale 12 Bivio Meano', '', '46.116062', '11.106197', '10110', '2']
['288', '22720x', 'Strada Statale 12 Canova', '', '46.098985', '11.111739', '10110', '1']
['289', '22705x', 'Strada Statale 12 "Paludi"', 'via Bolzano incrocio via Noce', '46.102643', '11.110490', '10110', '2']
['290', '22720z', 'Strada Statale 12 Canova', '', '46.100264', '11.111578', '10110', '1']
['340', '25815z', 'Strada Statale 349 Pozzata', '', '46.039534', '11.144070', '10110', '2']
['1350', '31221z', 'Lizzanella Strada Statale.12', '', '45.875522', '11.031822', '10101', '2']
['1351', '31221x', 'Lizzanella Strada Statale.12', '', '45.875686', '11.032030', '10101', '2']
['1422', '32221z', 'Strada Statale.12 "Rovercenter"', '', '45.864490', '11.029338', '10101', '2']
['2289', '32251x', 'Strada Statale.240 "Millennium Sud"', '', '45.868678', '11.014119', '10101', '2']
['2291', '32251z', 'Strada Statale.240 "Millennium Sud"', '', '45.868802', '11.014170', '10101', '2']
['2293', '32241z', 'Strada Statale.240 "Millennium Nord"', '', '45.870320', '11.016768', '10101', '2']
['2294', '32241x', 'Strada Statale.240 "Millennium Nord"', '', '45.870588', '11.017358', '10101', '2']
['2337', '39041z', 'Volano Strada Statale.12', '', '45.917871', '11.064024', '19011', '2']
['2338', '39041x', 'Volano Strada Statale.12', '', '45.917904', '11.063866', '19011', '2']
['2418', '22710z', 'Strada Statale 12 "Pioppeto"', '', '46.108623', '11.108717', '10110', '1']
['2419', '22730z', 'Strada Statale 12 S.Anna', '', '46.112409', '11.107438', '10110', '2']
['2438', '22725z', 'Strada Statale 12 Talvera', '', '46.103288', '11.110545', '10110', '1']
['2500', '21685z', 'Strada Statale 45 "Scala"', '', '46.077367', '11.099792', '10110', '2']
['2514', '34101x', "Strada Statale.240 Ponte Sull'adige", '', '45.863191', '10.997289', '14051', '2']
['2515', '34101z', "Strada Statale.240 Ponte Sull'adige", '', '45.863235', '10.997355', '14051', '2']
['2699', '32221x', 'Strada Statale.12 "Rovercenter"', '', '45.864206', '11.029050', '10101', '2']
['2701', '25815x', 'Strada Statale 349 Pozzata', '', '46.041779', '11.142383', '10110', '2']
['2780', '21687z', 'Strada Statale 45 S.Giorgio', '', '46.078668', '11.099302', '10110', '2']
['2806', '32236x', 'Strada Statale.240 "Alla Staffa"', '', '45.872760', '11.024323', '10101', '2']
['2838', '39061z', 'Strada Statale.12 Fornaci', '', '45.919443', '11.085327', '19011', '2']
['2856', '36511z', 'Strada Statale.12 Castel Pietra', '', '45.921721', '11.091309', '16501', '2']
['2857', '36511x', 'Strada Statale.12 Castel Pietra', '', '45.922938', '11.092225', '16501', '2']
['2858', '36521x', 'Strada Statale.12 Campagnole', '', '45.928854', '11.093011', '16501', '2']
['2859', '36521z', 'Strada Statale.12 Campagnole', '', '45.928851', '11.093153', '16501', '2']
['2870', '32236z', 'Strada Statale.240 "Alla Staffa"', '', '45.872820', '11.024167', '10101', '2']
['2877', '32256x', 'Strada Statale.240 "Meb"', '', '45.867858', '11.011826', '10101', '2']
['2906', '36541z', 'Calliano Strada Statale.12', '', '45.934315', '11.096017', '16501', '2']
['2907', '36541x', 'Calliano Strada Statale.12', '', '45.934334', '11.095870', '16501', '2']

NOTA In realtà l’utilizzo delle backreferences non è limitato alla funzione di sostituzione re.sub ma può essere usato anche nei pattern: il pattern ^(a+)=\1$ per esempio significa che la stringa deve iniziare ^ con una o più a ( a+ ), e deve essere seguito da un uguale ( = ) e dalla stessa stringa che ha verificato il gruppo 1 ( \1 ) che nel nostro caso equivale a quanto stato verificato da a+ precedentemente.

✪✪✪ ESERCIZIO 7.2 Prova a inserire delle stringhe che verificano i pattern inseriti:

[64]:
pass_n_fail(r'^(a+)=\1$',
           ['', '',''], # Inserisci qui almeno 3 elementi
           [])
ERRORE: '' non matcha il pattern '^(a+)=\1$' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern '^(a+)=\1$' ma dovrebbe farlo!
ERRORE: '' non matcha il pattern '^(a+)=\1$' ma dovrebbe farlo!
Mostra soluzione
[65]:

8. Dividere le stringhe

Uno degli altri task ai quali le espressioni regolari possono essere utile è quello di spezzare le stringhe, magari per isolare parti specifiche. Anche in questo caso esiste un’analogia con il metodo split() per le stringhe visto che entrambe le istruzioni compiono una funzione simile ma re.split() ha due parametri obbligatori, il primo è il pattern dell’espressione regolare che quando verificata spezza il testo e il secondo è la stringa da spezzare.

Nell’esempio qui sotto separare tutte le frasi in un testo, prendiamo il file, lo apriamo e lo leggiamo: f.readlines() legge tutte le linee e restituisce una lista di linee, il metodo "\n".join() usa il carattere di nuova riga \n per unire gli elementi della lista (in questo caso le varie linee, formando il testo).

A questo punto chiamiamo il metodo re.split() usando come pattern di separazione punti, punto e virgola, due punti, etc… presenti almeno una volta e seguiti da almeno uno spazio seguito da opzionalmente da una serie di acapo; abbiamo anche aggiunto il flag re.MULTILINE per permettere le regex su righe multiple.

[66]:
import re

with open('psposi1.txt', encoding='utf-8') as f:
    testo = "\n".join(f.readlines())

frasi = re.split(r'[.;:?!-]+\s+\n*', testo, flags=re.MULTILINE)

frasi[:10]  # con la slice [:10] mostriamo solo le prime 10 frasi
[66]:
['Quel ramo del lago di Como, che volge a mezzogiorno, tra due catene [1] non interrotte di monti, tutto a seni e a golfi, a seconda dello sporgere e del rientrare di quelli, vien, quasi a un tratto, a ristringersi, e a prender corso e figura di fiume, tra un promontorio a destra, e un’ampia costiera dall’altra parte',
 'e il ponte [2], che ivi congiunge le due rive, par che renda ancor più sensibile all’occhio questa trasformazione, e segni il punto in cui il lago cessa, e l’Adda rincomincia, per ripigliar poi nome di lago dove le rive, allontanandosi di nuovo, lascian l’acqua distendersi e rallentarsi in nuovi golfi e in nuovi seni',
 'La costiera, formata dal deposito di tre grossi torrenti [3], scende appoggiata a due monti contigui, l’uno detto di san Martino, l’altro, con voce lombarda, il Resegone, dai molti suoi cocuzzoli in fila, che in vero lo fanno somigliare a una sega',
 'talché non è chi, al primo vederlo, purché sia di fronte, come per esempio di su le mura di Milano che guardano a settentrione, non lo discerna tosto, a un tal contrassegno, in quella lunga e vasta giogaia, dagli altri monti di nome più oscuro e di forma più comune',
 'Per un buon pezzo, la costa sale con un pendìo lento e continuo',
 'poi si rompe in poggi e in valloncelli, in erte e in ispianate, secondo l’ossatura de’ due monti, e il lavoro dell’acque',
 'Il lembo estremo, tagliato dalle foci de’ torrenti, è quasi tutto ghiaia e ciottoloni',
 'il resto, campi e vigne, sparse di terre, di ville, di casali',
 'in qualche parte boschi, che si prolungano su per la montagna',
 'Lecco, la principale di quelle terre, e che dà nome al territorio, giace poco discosto dal ponte, alla riva del lago, anzi viene in parte a trovarsi nel lago stesso, quando questo ingrossa']

9. Cercare pattern nel testo

A volte vogliamo trovare delle informazioni all’interno del testo e sappiamo che queste appaiono in un certo pattern, possiamo scrivere una espressione regolare e utilizzare il metodo re.findall() per estrarle. Questo comando è molto simile al comando re.find() ma al contrario di questo non si ferma al primo match ma prosegue estraendo una lista delle stringhe che hanno verificato la regex oppure, se sono presenti delle backreferences, con una lista di tuple avente i valori di tutti i gruppi di backreference che hanno verificato il pattern.

Proviamo a cercare tutte le parole che seguono la parola "terra" o "terre" : la regular expression cerca prima una stringa che inizi per terr e che abbia una a oppure una e, uno spazio ed infine una stringa alfanumerica di uno o più caratteri ed un delimitatore di fine parola:

[67]:
import re

with open('psposi1.txt', encoding='utf-8') as f:
    testo = "\n".join(f.readlines())

re.findall(r'terr[ae]\s\w+\b', testo)
[67]:
['terre accennate', 'terra cotta', 'terra con']

Come vedi ci viene restituita una lista di stringhe, infatti non ci sono gruppi all’interno della regex e quindi ci viene restituito tutto il matching.

✪✪ ESERCIZIO 9.1: Prova a copiare la linea di sopra qua sotto e ad aggiungere uno o più gruppi nel pattern e vedi cosa succede:

Mostra soluzione
[68]:
# scrivi qui


['terre', 'terra', 'terra']
[('terre', 'accennate'), ('terra', 'cotta'), ('terra', 'con')]
[ ]: