Espressioni regolari Python

Le espressioni regolari sono un linguaggio efficace per trovare pattern di testo corrispondenti. Questa pagina fornisce un'introduzione di base alle espressioni regolari stesse sufficienti per i nostri esercizi Python e mostra come funzionano le espressioni regolari in Python. Il modulo "re" di Python supporta le espressioni regolari.

In Python, una ricerca basata su espressioni regolari è solitamente scritta come segue:

match = re.search(pat, str)

Il metodo re.search() prende un modello di espressione regolare e una stringa e cerca quel pattern all'interno della stringa. Se la ricerca ha esito positivo, search() restituisce un oggetto corrispondenza o None negli altri casi. Pertanto, la ricerca di solito è seguita immediatamente da un'istruzione if per verificare se la ricerca è riuscita, come mostrato nell'esempio seguente che cerca il pattern "parola:" seguito da una parola di 3 lettere (i dettagli sono riportati di seguito):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

Il codice match = re.search(pat, str) memorizza il risultato di ricerca in una variabile denominata "match". Quindi l'istruzione if-verifica la corrispondenza: se true la ricerca è riuscita e match.group() è il testo corrispondente (ad es. "word:cat"). Altrimenti, se la corrispondenza è falsa (non per essere più specifici), la ricerca non è riuscita e non è presente alcun testo corrispondente.

La "r" all'inizio della stringa pattern indica una stringa "raw" Python che passa attraverso le barre rovesciate senza apportare modifiche, il che è molto utile per le espressioni regolari (Java ha davvero bisogno di questa funzione). Ti consiglio di scrivere sempre stringhe pattern con la "r" come abitudine.

Pattern di base

Il vantaggio delle espressioni regolari sta nel fatto che possono specificare pattern, non solo caratteri fissi. Di seguito sono riportati i pattern più basilari che corrispondono a caratteri singoli:

  • a, X, 9, < -- caratteri ordinari si associano esattamente. I metacaratteri che non corrispondono perché hanno significati speciali sono: . ^ $ * + ? { [ ] \ | ( ) (dettagli di seguito)
  • . (un punto) -- corrisponde a qualsiasi carattere singolo tranne alla nuova riga "\n"
  • \w -- (w minuscolo) corrisponde a un carattere "parola": una lettera o un numero o il trattino basso [a-zA-Z0-9_]. Tieni presente che, sebbene "parola" sia il mnemonico utilizzato, corrisponde solo all'carattere di una singola parola, non a una parola intera. \W (W maiuscola) corrisponde a qualsiasi carattere diverso da una parola.
  • \b: confine tra una parola e una non parola
  • \s -- (minuscolo) corrisponde a un singolo carattere di spazio vuoto: spazio, nuova riga, ritorno, tabulazione, modulo [ \n\r\t\f]. \S (S maiuscola) corrisponde a qualsiasi carattere che non sia spazio vuoto.
  • \t, \n, \r -- scheda, nuova riga, ritorno
  • \d -- cifra decimale [0-9] (alcune vecchie utilità di regex non supportano \d, ma tutte supportano \w e \s)
  • ^ = inizio, $ = fine: corrisponde all'inizio o alla fine della stringa
  • \ -- inibisce la "specialità" di un personaggio. Ad esempio, utilizza \. per trovare la corrispondenza con un punto o \\ per trovare la corrispondenza con una barra. Se non hai la certezza che un carattere abbia un significato speciale, ad esempio "@", puoi provare a inserire una barra davanti al carattere, \@. Se non è una sequenza di escape valida, come \c, il tuo programma Python si interromperà generando un errore.

Esempi di base

Barzelletta: come si chiama un maiale con tre occhi? ciao!

Le regole di base della ricerca tramite espressione regolare di un pattern all'interno di una stringa sono:

  • La ricerca procede attraverso la stringa dall'inizio alla fine, interrompendosi alla prima corrispondenza trovata
  • Deve corrispondere tutto il pattern, ma non tutta la stringa
  • Se match = re.search(pat, str) ha esito positivo, la corrispondenza non è None e, in particolare, match.group() è il testo corrispondente
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

Ripetizioni

Le cose diventano più interessanti quando usi + e * per specificare la ripetizione nel pattern

  • + -- Una o più occorrenze del pattern alla sua sinistra, ad es. 'i+' = una o più occorrenze
  • * -- Una o più occorrenze del pattern alla sua sinistra
  • ? -- corrisponde a 0 o 1 occorrenze del pattern alla sua sinistra.

Più a sinistra e Più grande

Prima la ricerca trova la corrispondenza più a sinistra per il pattern, poi cerca di utilizzare il maggior numero possibile di stringa, ad esempio + e * vanno il più possibile (i segni + e * vengono definiti "avida").

Esempi di ripetizioni

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

Esempio di email

Supponiamo che tu voglia trovare l'indirizzo email all'interno della stringa "xyz alice-b@google.com viola scimmia". Utilizzeremo questo esempio come esempio corrente per dimostrare altre funzionalità di espressione regolare. Ecco un tentativo utilizzando il pattern r'\w+@\w+':

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

In questo caso la ricerca non ottiene l'intero indirizzo email perché il carattere \w non corrisponde a "-" o "." nell'indirizzo. Per risolvere il problema, utilizza le funzioni di espressione regolare riportate di seguito.

Staffe quadrate

Le parentesi quadre possono essere utilizzate per indicare un insieme di caratteri, quindi [abc] corrisponde ad "a", "b" o "c". I codici \w, \s e così via funzionano anche tra parentesi quadre, con l'unica eccezione per cui il punto (.) indica solo un punto letterale. Per il problema delle email, le parentesi quadre sono un modo semplice per aggiungere '.' e '-' all'insieme di caratteri che possono apparire intorno a @ con il pattern r'[\w.-]+@[\w.-]+' per ottenere l'indirizzo email completo:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
(Altre funzionalità di parentesi quadre) Puoi anche utilizzare un trattino per indicare un intervallo, in modo che [a-z] corrisponda a tutte le lettere minuscole. Per utilizzare un trattino senza indicare un intervallo, inserisci il trattino per ultimo, ad esempio [abc-]. Un cappello alto (^) all'inizio di un insieme di parentesi quadre lo inverte, quindi [^ab] indica qualsiasi carattere tranne "a" o "b".

Estrazione gruppi

La funzione "raggruppa" di un'espressione regolare ti consente di scegliere parti del testo corrispondente. Supponiamo che per il problema con l'email vogliamo estrarre separatamente il nome utente e l'host. Per farlo, aggiungi le parentesi ( ) intorno al nome utente e all'host nel pattern, ad esempio r'([\w.-]+)@([\w.-]+)'. In questo caso, le parentesi non modificano la corrispondenza del pattern, ma stabiliscono "gruppi" logici all'interno del testo della corrispondenza. In una ricerca riuscita, match.group(1) è il testo corrispondente alla prima parentesi aperta, mentre match.group(2) è il testo corrispondente alla seconda parentesi aperta. Il semplice match.group() è ancora l'intero testo della corrispondenza, come al solito.

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

Un flusso di lavoro comune con le espressioni regolari consiste nel scrivere un pattern per ciò che si sta cercando, aggiungendo gruppi di parentesi per estrarre le parti desiderate.

trova tutto

Findall() è probabilmente l'unica funzione più potente del modulo re. Sopra abbiamo usato re.search() per trovare la prima corrispondenza di un pattern. findall() trova *tutte* le corrispondenze e le restituisce sotto forma di elenco di stringhe, dove ogni stringa rappresenta una corrispondenza.
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

trova tutto con Files

Per i file, potresti essere abituato a scrivere un ciclo da ripetere sulle righe del file, per poi chiamare findall() su ogni riga. Lascia che findall() esegua l'iterazione al posto tuo, molto meglio! Basta inserire l'intero testo del file in findall() e lasciare che restituisca un elenco di tutte le corrispondenze in un singolo passaggio (ricorda che f.read() restituisce l'intero testo di un file in un'unica stringa):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

Findall e Gruppi

Il meccanismo di raggruppamento delle parentesi ( ) può essere combinato con Findall(). Se il pattern include due o più gruppi di parentesi, anziché restituire un elenco di stringhe, findall() restituisce un elenco di *tuples*. Ogni tupla rappresenta una corrispondenza del pattern e all'interno della tupla è presente il gruppo(1), il gruppo(2) ... dati. Quindi, se al pattern email vengono aggiunti due gruppi di parentesi, findall() restituisce un elenco di tuple, ciascuna lunghezza 2 contenente il nome utente e l'host, ad esempio ("alice", "google.com").

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

Una volta ottenuto l'elenco delle tuple, puoi eseguire il loop per eseguire il calcolo di ciascuna tupla. Se il pattern non include parentesi, Findall() restituisce un elenco di stringhe trovate come negli esempi precedenti. Se il pattern include una singola serie di parentesi, Findall() restituisce un elenco di stringhe corrispondenti al singolo gruppo. Funzione facoltativa e poco chiara: a volte nel pattern sono presenti raggruppamenti separati ( ) ma che non desideri estrarre. In tal caso, scrivi le parentesi graffe con un ?: all'inizio, ad esempio (?: ) e la virgola sinistra non verrà conteggiata come risultato di gruppo.

Ripeti flusso di lavoro e debug

I pattern delle espressioni regolari racchiudono molto significato in pochi caratteri, ma sono così densi che puoi dedicare molto tempo al debug dei tuoi pattern. Configura il runtime in modo da poter eseguire un pattern e stampare facilmente ciò che corrisponde, ad esempio eseguendolo su un testo di prova di piccole dimensioni e stampando il risultato di findall(). Se il pattern non corrisponde a niente, prova a indebolire il pattern, rimuovendone alcune parti in modo da ottenere troppe corrispondenze. Quando non corrisponde nulla, non puoi fare progressi perché non c'è nulla di concreto da esaminare. Quando la corrispondenza è eccessiva, puoi lavorare per aumentarla in modo incrementale per raggiungere esattamente ciò che desideri.

Opzioni

Le funzioni ricorrono a opzioni per modificare il comportamento della corrispondenza di schema. Il flag di opzione viene aggiunto come argomento aggiuntivo alle funzioni search() o findall() e così via, ad esempio re.search(pat, str, re.IGNORECASE).

  • IGNORECASE: ignora le differenze tra maiuscole e minuscole per le corrispondenze, quindi "a" corrisponde sia ad "a" che ad "A".
  • DOTALL: consenti la corrispondenza tra il punto (.) e la nuova riga, in genere corrisponde a qualsiasi cosa tranne a nuova riga. Questo può farti ingannare: pensi che .* corrisponda a tutto, ma per impostazione predefinita non supera la fine di una riga. Tieni presente che \s (spazio vuoto) include i ritorni a capo, quindi se vuoi creare una corrispondenza con uno spazio vuoto che potrebbe includere un'altra riga, puoi semplicemente usare \s*
  • MULTILINEA: all'interno di una stringa composta da più righe, consenti ai simboli ^ e $ di corrispondere all'inizio e alla fine di ogni riga. Normalmente ^/$ corrisponde solo all'inizio e alla fine dell'intera stringa.

greedy e non greedy (facoltativo)

Questa sezione è facoltativa e mostra una tecnica di espressione regolare più avanzata non necessaria per gli esercizi.

Supponi di avere del testo con tag al suo interno: <b>foo</b> e <i>così via</i>

Supponiamo che tu stia tentando di far corrispondere ogni tag al pattern "(<.*>)": qual è la prima corrispondenza?

Il risultato è un po' sorprendente, ma l'aspetto avido di .* fa corrispondere l'intero '<b>foo</b> e <i>così via</i>' come un'unica corrispondenza importante. Il problema è che il simbolo .* arriva fino al punto in cui può, invece di fermarsi all'inizio > (ovvero "avido").

È disponibile un'estensione all'espressione regolare in cui aggiungi un ? alla fine, ad esempio .*? o .+?, modificandole in modo da non essere ingorde. Ora si fermano il prima possibile. Quindi il pattern "(<.*?>)" riceverà solo "<b>" come prima corrispondenza e "</b>" come seconda corrispondenza e così via ottenendo ogni coppia <..> alla volta. In genere, lo stile prevede l'utilizzo di un .*? seguito immediatamente da un indicatore di cemento (> in questo caso) a cui la corsa .*? è costretta a estendersi.

L'estensione *? ha avuto origine in Perl, mentre le espressioni regolari che includono estensioni di Perl sono note come espressioni regolari compatibili con Perl -- pcre. Python include il supporto per PC. Molti util della riga di comando e così via hanno un flag in cui accettano pattern pcre.

Una tecnica meno recente ma ampiamente utilizzata per codificare questa idea di "tutti questi caratteri tranne fermarsi a X" utilizza lo stile delle parentesi quadre. Per quanto sopra puoi scrivere il pattern, ma invece di .* per ottenere tutti i caratteri, usa [^>]* che salta tutti i caratteri che non sono > (la ^ iniziale "inverte" la parentesi quadra impostata, in modo che corrisponda a qualsiasi carattere non incluso tra parentesi).

Sostituzione (facoltativa)

La funzione re.sub(pat, replacement, str) cerca tutte le istanze del pattern nella stringa specificata e le sostituisce. La stringa sostitutiva può includere "\1", "\2" che fanno riferimento al testo di group(1), group(2) e così via dal testo corrispondente originale.

Ecco un esempio che cerca tutti gli indirizzi email e li modifica per mantenere l'utente (\1) ma avere yo-yo-dyne.com come host.

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

Allenamento

Per esercitarti con le espressioni regolari, vedi Esercizio sui nomi dei bambini.