Guida Espressioni regolari in Python

N3v5

Utente Silver
24 Ottobre 2020
169
19
40
80
Le espressioni regolari (regex) sono uno strumento molto potente di Python che ci consente di risolvere facilmente problemi di text mining, web scraping, data processing e molto altro.
Sinceramente non capisco perché risultino poco amate e utilizzate.


Concetti chiave
- Le regex sono stringhe di testo che descrivono in pattern che vogliamo trovare in un testo. Ad esempio, la regex "a+" descrive un pattern che consiste in una sequenza di una o più volte la lettera "a".
- Una regex può contenere qualsiasi carattere, ma alcuni ("meta-caratteri") hanno una funzione speciale. Essi sono: . ^ $ * + ? { } [ ] ( ) \ |
- Dobbiamo usare il modulo "re" della standard library. Con un metodo di questo modulo compiliamo la regex per produrre un "pattern object", il quale potrà poi essere usato per effettuare la ricerca nel testo (con altri metodi del modulo re).


Introduzione
Partiamo con un esempio:
Python:
import re
pattern = re.compile(r"l+")
lista = pattern.findall("elliot alderson")
print(lista)   # Risultato -> ['ll', 'l']

Esiste anche la versione abbreviata:
Python:
import re
lista = re.findall(r"l+","elliot alderson")
print(lista)   # Risultato -> ['ll', 'l']

Il problema di questo secondo approccio è che non memorizza il pattern, quindi è consigliabile solo quando tale pattern viene usato poche volte.

Giustamente vi starete chieendo cosa sia quella r di fronte alla regex. Significa "raw-data" e serve semplicemente per dire all'interprete di non trattare la stringa come una stringa standard. Ad esempio \n secondo l'interpretazione standard indicherebe new-line, mentre qua ha un significato diverso.
Per evitare problemi quindi è sempre buona norma metterlo.

Come possiamo vedere, quindi, per compilare abbiamo usato compile(). Tale metodo prende come parametro la regex (preceduta da r) e ci restituisce un "pattern object".
E' possibile assegnare a compile() un secondo parametro, detto "flag". Se come flag mettiamo re.I (i maiuscola, che sta per "Ignorecase"), ad esempio, diciamo all'interprete di considerare il pattern case-insensitive.
Nota bene: con la versione abbreviata non si usa compile(), Python fa tutto insieme.

Python:
import re
pattern = re.compile(r"l+",re.I)
lista = pattern.findall("elLiot alderson")
print(lista)   # Risultato -> ['lL', 'l']


Match Objects
Il modulo re contiene dei metodi (detti "di ricerca") che ritornano un "match object", cioè un oggetto che contiene il risultato testuale e altre informazioni.
I più importanti metodi di ricerca sono:
- findall() -> ritorna una lista di occorrenze del pattern nel testo.
- search() -> ritorna la prima occorrenza di un pattern nel testo.
- match() -> verifica se il pattern esiste all'inizio del testo, altrimenti ritorna None (che Python interpreterà come False).
Il match object è quindi un oggetto che viene creato quando utilizziamo uno di questi metodi di ricerca.
In realtà, findall() restituisce una lista, non un match object.

Vediamo qualche esempio:
Python:
import re
match_obj = re.search(r"l+","elliot alderson")
print(match_obj)   # Risultato -> <re.Match object; span=(1, 3), match='ll'>

Python:
import re
match_obj = re.match(r"l+","elliot alderson")
print(match_obj)   # Risultato -> None

Come possiamo vedere, provare a stampare un match object non è poi molto bello. E' per questo che i match objects hanno a loro volta vari metodi:
- group() -> ritorna il pattern.
- start() -> ritorna l'indice al quale inizia il pattern.
- end() -> ritorna l'indice al quale finisce il pattern.

Python:
import re
m_obj = re.search(r"sam", "elliot alderson e sam sapiol")
print("{} inizia a {} e finisce a {}".format(m_obj.group(),m_obj.start(),m_obj.end()))


Scrivere la regex
Stringa di soli caratteri: è il metodo più semplice. Cercherà esattamente la stringa.
Insieme di caratteri: usiamo le parentesi quadre per indicare i caratteri che devono essere cercati. Ad esempio [aeiou] non indica "aeiou", ma ognuna delle lettere: "a", "e", "i", "o", "u".
Range: possiamo usare un dash (-) dentro le parentesi quadre per includere, oltre alle due estremità, anche tutti i caratteri tra esse. Esempio: [a-dnx-z] è equivalente a [abcdnxyz].
Negazione: possiamo anche usare un caret (^) subito dopo l'apertura della parentesi quadra per indicare che vogliamo l'opposto di ciò che indichiamo. Esempio: [^0-9] significa qualsiasi carattere tranne un numero.

Python:
import re
lista = re.findall(r"b[aeiou]ll","bill gates drinks red bull")
print(lista)   # Risultato -> ['bill', 'bull']

Possiamo anche utilizzare delle sequenze speciali che ci facilitano il lavoro (senza parentesi quadre):
\d -> numero.
\D -> non numero. [^0-9]
\s -> spazio bianco.
\S -> non spazio bianco.
\w -> carattere alfanumerico. [A-Za-z0-9]
\W -> non carattere alfanumerico. [^A-Za-z0-9]
\/ -> forward slash.
\\ -> backslash.
\" -> double quote.
\' -> single quote.
\n -> newline.
^ -> inizio di una stringa.
$ -> fine di una stringa.
. -> qualsiasi carattere.

Le parentesi tonde e la pipeline ci servono invece per indicare una scelta multipla:
(whiterose|price|ray) significa: o la stringa "whiterose", o la stringa "price", o la stringa "ray".

Abbiamo anche gli operatori di ripetizione, che vanno posizionati dopo l'espressione da ripetere.
* -> zero o più volte.
+ -> una o più volte.
? -> zero o una volta.
{p,q} -> almeno p e al massimo q volte.
{p,} -> almeno p volte.
{p} -> esattamente p volte.

Esempi:

"ab*c" -> significa la lettera a seguita zero o più volte dalla lettera b, seguita a sua volta dalla lettera c.
Matcherà quindi ad esempio le stringhe: "ac", "abc", "abbc", "abbbc", "abbbbc", ...

"(ab)*c -> matcha le stringhe: "c", "abc", "ababc", "abababc", ...​