Espressioni rapide e regolari sintassi

1. Introduzione

In poche parole, le espressioni regolari (regex o regexps in breve) sono un modo per specificare i modelli di stringa. Hai senza dubbio familiarità con la funzione di ricerca e sostituzione nel tuo editor di testo o IDE preferito. Puoi cercare parole e frasi esatte. È inoltre possibile attivare opzioni, ad esempio insensibilità alle maiuscole / minuscole, in modo che la ricerca della parola "colore" trovi anche "Colore", "COLORE" e "CoLoR". Ma cosa succede se si desidera cercare le varianti di ortografia della parola "colore" (ortografia americana: colore, ortografia inglese: colore) senza dover eseguire due ricerche distinte?

Se quell'esempio sembra troppo semplice, che ne dici se vuoi cercare tutte le varianti di ortografia del nome inglese "Katherine" (Catherine, Katharine, Kathreen, Kathryn, ecc. Per citarne alcuni). Più in generale, si potrebbe voler cercare un documento per tutte le stringhe che assomigliano a numeri esadecimali, date, numeri di telefono, indirizzi e-mail, numeri di carte di credito, ecc..

Le espressioni regolari sono un modo potente di (parzialmente o completamente) affrontare questi (e molti altri) problemi pratici che coinvolgono il testo.

Schema

La struttura di questo tutorial è la seguente. Introdurrò i concetti fondamentali che è necessario comprendere adattando un approccio utilizzato nei libri di testo teorici (dopo aver eliminato qualsiasi rigore o pedanteria non necessari). Preferisco questo approccio perché ti consente di capire il 70% delle funzionalità di cui avrai bisogno, nel contesto di alcuni principi di base. Il restante 30% sono funzioni più avanzate che puoi imparare in seguito o saltare, a meno che tu non voglia diventare un regex maestro.

C'è una quantità copiosa di sintassi associata alle espressioni regolari, ma la maggior parte è solo lì per consentire di applicare le idee principali nel modo più sintetico possibile. Li presenterò in modo incrementale, piuttosto che far cadere un grande tavolo o un elenco da memorizzare.

Invece di saltare direttamente a un'implementazione di Swift, esploreremo le basi attraverso un eccellente strumento online che ti aiuterà a progettare e valutare le espressioni regolari con il minimo di attrito e bagaglio non necessario. Una volta che ti senti a tuo agio con le idee principali, scrivere codice Swift è fondamentalmente un problema di mappatura delle tue conoscenze con l'API Swift.

Durante tutto, cercheremo di mantenere una mentalità pragmatica. I regex non sono lo strumento migliore per ogni situazione di elaborazione delle stringhe. In pratica, dobbiamo identificare le situazioni in cui le regex funzionano molto bene e le situazioni in cui non lo fanno. C'è anche una via di mezzo in cui le regex possono essere usate per fare parte del lavoro (di solito qualche pre-elaborazione e filtraggio) e il resto del lavoro lasciato alla logica algoritmica.

Concetti principali

Le espressioni regolari hanno le loro basi teoriche nella "teoria della computazione", uno degli argomenti studiati dall'informatica, in cui giocano il ruolo dell'input applicato a una classe specifica di macchine di calcolo astratte chiamate automi finiti.

Rilassati, però, non è necessario studiare il background teorico per utilizzare praticamente le espressioni regolari. Li menziono solo perché l'approccio che userò per motivare inizialmente le espressioni regolari da zero rispecchia l'approccio usato nei libri di testo di informatica per definire espressioni regolari "teoriche".

Supponendo che tu abbia una certa dimestichezza con la ricorsione, vorrei che tu ricordassi come sono definite le funzioni ricorsive. Una funzione è definita in termini di versioni più semplici di se stessa e, se si traccia attraverso una definizione ricorsiva, si deve finire in un caso base definito in modo esplicito. Ne parlo perché anche la nostra definizione di seguito sarà ricorsiva.

Nota che, quando parliamo di stringhe in generale, abbiamo implicitamente in mente un set di caratteri, come ASCII, Unicode, ecc. Facciamo finta per il momento di vivere in un universo in cui le stringhe sono composte da 26 lettere minuscole alfabeto (a, b, ... z) e nient'altro.

Regole

Iniziamo affermando che ogni personaggio in questo set può essere considerato un'espressione regolare che si abbina come una stringa. Così un come espressione regolare corrisponde a "a" (considerato come una stringa), B è un'espressione regolare che corrisponde alla stringa "b", ecc. Diciamo anche che esiste un'espressione regolare "vuota" Ɛ che corrisponde alla stringa vuota "". Tali casi corrispondono ai banali "casi base" della ricorsione.

Ora, consideriamo le seguenti regole che ci aiutano a creare nuove espressioni regolari da quelle esistenti:

  1. Il concatenazione (cioè "unire insieme") di due espressioni regolari è una nuova espressione regolare che corrisponde alla concatenazione di due stringhe corrispondenti alle espressioni regolari originali.
  2. Il alternanza di due espressioni regolari è una nuova espressione regolare che corrisponde a una delle due espressioni regolari originali.
  3. Il Stella Kleene di un'espressione regolare corrisponde a zero o più istanze adiacenti di qualsiasi cosa corrisponda all'espressione regolare originale.

Facciamo questo concreto con diversi semplici esempi con le nostre stringhe alfabetiche.

Esempio 1

Dalla regola 1, unB essere espressioni regolari che corrispondono a "a" e "b", significa ab è un'espressione regolare che corrisponde alla stringa "ab". Da ab e c sono espressioni regolari, abc è un'espressione regolare che corrisponde alla stringa "abc" e così via. Continuando in questo modo, possiamo creare espressioni regolari lunghe e arbitrarie che corrispondono a una stringa con caratteri identici. Non è ancora successo nulla di interessante.

Esempio 2

Dalla regola 2, o e un essere espressioni regolari, o | a corrisponde a "o" o "a". La barra verticale rappresenta l'alternanza. c e t sono espressioni regolari e, in combinazione con la regola 1, possiamo affermarlo c (o | a) t è un'espressione regolare. Le parentesi vengono utilizzate per il raggruppamento.

Cosa combacia?? c e t si abbinano solo a se stessi, il che significa che la regex c (o | a) t corrisponde a "c" seguito da "a" o "o" seguito da "t", ad esempio, la stringa "cat" o "cot". Nota che lo fa non abbinare "cappotto" come o | a corrisponde solo a "a" o "o", ma non entrambi contemporaneamente. Ora le cose stanno diventando interessanti.

Esempio 3

Dalla regola 3, un* corrisponde a zero o più istanze di "a". Corrisponde alla stringa vuota o alle stringhe "a", "aa", "aaa" e così via. Esercitiamo questa regola in combinazione con le altre due regole.

Cosa fa caldo incontro? Corrisponde a "ht" (con zero istanze di "o"), "hot", "hoot", "hooot" e così via. Che dire b (o | a) *? Può corrispondere a "b" seguito da un numero qualsiasi di istanze di "o" e "a" (compreso nessuno di essi). "b", "boa", "baa", "bao", "baooaoaoaoo" sono solo alcune delle infinite stringhe che questa espressione regolare corrisponde. Nota ancora che le parentesi sono usate per raggruppare la parte dell'espressione regolare a cui il * viene applicato.

Esempio 4

Proviamo a scoprire le espressioni regolari che corrispondono alle stringhe che abbiamo già in mente. Come faremmo un'espressione regolare che riconosca il belato delle pecore, che considererò come qualsiasi numero di ripetizioni del suono di base "baa" ("baa", "baabaa", "baabaabaa", ecc.)

Se hai detto, (BAA) *, allora sei quasi corretto. Ma si noti che questa espressione regolare corrisponde anche alla stringa vuota, che non vogliamo. In altre parole, vogliamo ignorare le pecore non-belanti. Baa (Baa) * è l'espressione regolare che stiamo cercando. Allo stesso modo, potrebbe essere un muggito di mucca moo (moo) *. Come possiamo riconoscere il suono di entrambi gli animali? Semplice. Usa l'alternanza. Baa (Baa) * | moo (moo) *

Se hai compreso le idee di cui sopra, congratulazioni, sei sulla buona strada.

2. Questioni di sintassi

Ricordiamo che abbiamo posto una sciocca restrizione sulle nostre corde. Potrebbero essere composti solo da lettere minuscole dell'alfabeto. Ora elimineremo questa restrizione e prenderemo in considerazione tutte le stringhe composte da caratteri ASCII.

Dobbiamo renderci conto che, affinché le espressioni regolari siano uno strumento utile, esse devono essere rappresentate esse stesse come stringhe. Quindi, a differenza di prima, non possiamo più usare caratteri come *, |, (, ), ecc. senza in qualche modo segnalare se li stiamo usando come caratteri "speciali" che rappresentano alternanza, raggruppamento, ecc. o se li stiamo trattando come caratteri ordinari che devono essere abbinati letteralmente.

La soluzione è trattare questi e altri "metacaratteri" che possono avere un significato speciale. Per passare da un uso all'altro, dobbiamo essere in grado di sfuggirli. Questo è simile all'idea di usare "\ n" (sfuggire alla n) per indicare una nuova riga in una stringa. È leggermente più complicato in quanto, a seconda del carattere di contesto che è normalmente "meta", potrebbe rappresentare il suo sé letterale senza scappatoia. Vedremo esempi di questo in seguito.

Un'altra cosa che apprezziamo è concisione. Molte espressioni regolari che possono essere espresse usando solo la notazione della sezione precedente sarebbero noiosamente prolisse. Ad esempio, supponiamo di voler trovare tutte e due le stringhe di caratteri composte da una lettera minuscola seguita da un numero (ad esempio stringhe come "a0", "b9", "z3", ecc.). Usando la notazione di cui abbiamo discusso in precedenza, ciò comporterebbe la seguente espressione regolare:

(A | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z) (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)

Basta scrivere quel mostro mi ha spazzato via.

non [ABCDEFGHIJKLMNOPQRSTUVWXYZ] [0123456789] sembra una rappresentazione migliore? Nota i metacaratteri [ e ] che significa un insieme di personaggi, ognuno dei quali dà una corrispondenza positiva. In realtà, se consideriamo che le lettere dalla a alla z, ei numeri da 0 a 9 si verificano in sequenza nell'insieme ASCII, possiamo ridimensionare la regex in un modo cool [A-z] [0-9].

Entro i confini di un set di caratteri, il trattino, -, è un altro metacarattere che indica un intervallo. Si noti che è possibile comprimere più intervalli nella stessa coppia di parentesi quadre. Per esempio, [0-9a-zA-Z] può corrispondere a qualsiasi carattere alfanumerico. Il 9 e un (e  z e UN)stretti l'uno contro l'altro potrebbero sembrare divertenti, ma ricorda che le espressioni regolari sono tutte di brevità e il significato è chiaro.

Parlando di brevità, ci sono modi ancora più concisi per rappresentare alcune classi di personaggi correlati, come vedremo tra un minuto. Si noti che la barra di alternanza, |, è ancora una sintassi valida e utile come vedremo tra poco.

Più sintassi

Prima di iniziare a praticare, diamo un'occhiata a un po 'più di sintassi.

Periodo

Il periodo, ., corrisponde a qualsiasi singolo carattere, ad eccezione delle interruzioni di riga. Ciò significa che c.t può corrispondere a "cat", "crt", "c9t", "c% t", "c.t", "c t" e così via. Se volessimo abbinare il periodo come carattere ordinario, ad esempio, per abbinare la stringa "c.t", potremmo scappare (c \ .t) o metterlo in una classe di caratteri a sé stante (c [.] t).

In generale, queste idee si applicano ad altri metacaratteri, come [, ], (, )*, e altri che non abbiamo ancora incontrato.

parentesi

Parentesi (( e )) sono usati per raggruppare come abbiamo visto prima. Useremo la parola gettone per significare un singolo carattere o un'espressione tra parentesi. Il motivo è che molti operatori di espressioni regolari possono essere applicati a entrambi.

Le parentesi sono anche usate per definire gruppi di cattura, permettendoti di capire quale parte della tua partita era catturato da un particolare gruppo di cattura nella regex. Parlerò di più di questa utilissima funzionalità in seguito.

Più

UN + seguendo un token ci sono una o più istanze di quel token. Nel nostro esempio di belato di pecore, Baa (Baa) * potrebbe essere rappresentato più succintamente come (BAA)+. Richiama questo * significa zero o più occorrenze. Nota che (BAA)+ è diverso da belato+, perché nel primo caso + è applicato al belato token mentre nel secondo si applica solo al un prima di cio. In quest'ultimo, combina stringhe come "baa", "baaa" e "baaaa".

Punto interrogativo

UN ? seguire un token significa zero o una istanza di quel token.

Pratica

RegExr è un eccellente strumento online per sperimentare espressioni regolari. Quando sei a tuo agio a leggere e scrivere espressioni regolari, sarà molto più semplice utilizzare l'API delle espressioni regolari del framework Foundation. Anche in questo caso, sarà più facile testare la tua espressione regolare in tempo reale sul sito web.

Visita il sito web e concentrati sulla parte principale della pagina. Questo è quello che vedrai:

Immetti un'espressione regolare nella casella in alto e inserisci il testo in cui cerchi le corrispondenze.

Il "/ g" alla fine della casella di espressione non fa parte dell'espressione regolare di per sé. È una bandiera che influisce sul comportamento di corrispondenza generale del motore regex. Aggiungendo "/ g" all'espressione regolare, il motore cerca tutte le possibili corrispondenze dell'espressione regolare nel testo, che è il comportamento che vogliamo. L'evidenziazione blu indica una corrispondenza. Passare con il mouse sopra l'espressione regolare è un modo pratico per ricordare il significato delle sue parti costituenti.

Sappi che le espressioni regolari sono disponibili in vari modi, a seconda della lingua o della libreria che stai utilizzando. Ciò non solo significa che la sintassi può essere leggermente diversa tra i vari sapori, ma anche le capacità e le caratteristiche. Swift, ad esempio, utilizza la sintassi del pattern specificata da ICU. Non sono sicuro di quale flavor sia usato in RegExr (che gira su JavaScript), ma nell'ambito di questo tutorial, sono abbastanza simili, se non identici.

Ti incoraggio anche a esplorare il riquadro sul lato sinistro, che ha un sacco di informazioni presentate in modo conciso.

Il nostro primo esempio pratico

Per evitare la potenziale confusione, dovrei menzionare che, quando si parla di corrispondenza di espressioni regolari, potremmo indicare una di queste due cose:

  1. cercando qualsiasi sottostringa (o tutto) di una stringa che corrisponda a un'espressione regolare
  2. controllando se la stringa completa corrisponde o meno all'espressione regolare

Il significato predefinito con cui operano i motori regex è (1). Quello di cui abbiamo parlato finora è (2). Fortunatamente, è facile implementare il significato (2) per mezzo di metacaratteri che verranno introdotti in seguito. Non preoccuparti di questo per ora.

Iniziamo semplice testando il nostro esempio di belato di pecore. genere (BAA)+ nella casella dell'espressione e alcuni esempi per testare le corrispondenze come mostrato di seguito.

Spero che tu capisca perché le partite riuscite siano effettivamente riuscite e perché gli altri abbiano fallito. Anche in questo semplice esempio, ci sono alcune cose interessanti da sottolineare.

Partite golose

La stringa "baabaa" contiene due fiammiferi o uno? In altre parole, ogni individuo "baa" corrisponde o è l'intera "baabaa" una singola partita? Ciò dipende dal fatto che venga ricercata o meno una "partita avida". Una partita golosa tenta di abbinare il più possibile a una stringa.

In questo momento il motore regex si combina avidamente, il che significa che "baabaa" è una singola partita. Ci sono modi per fare abbinamenti pigri, ma questo è un argomento più avanzato e, dato che abbiamo già i nostri piatti pieni, non lo copriremo in questo tutorial.

Lo strumento RegExr lascia un piccolo ma discernibile spazio nell'evidenziazione se due parti adiacenti di una stringa singolarmente (ma non collettivamente) corrispondono all'espressione regolare. Vedremo un esempio di questo comportamento tra un po '.

Maiuscolo e minuscolo

"Baabaa" fallisce a causa della "B" maiuscola. Supponiamo che tu voglia permettere che solo la prima "B" sia maiuscola, quale sarebbe l'espressione regolare corrispondente? Cerca di capirlo prima da solo.

Una risposta è (B | b) aa (BAA) *. Aiuta se lo leggi ad alta voce. Una "b" maiuscola o minuscola, seguita da "aa", seguita da zero o più istanze di "baa". Questo è fattibile, ma si noti che questo potrebbe rapidamente diventare scomodo, specialmente se volessimo ignorare del tutto le maiuscole. Ad esempio, dovremmo specificare delle alternative per ogni caso, il che risulterebbe in qualcosa di poco maneggevole ([Bb] [Aa] [Aa])+.

Fortunatamente, i motori di espressioni regolari hanno in genere la possibilità di ignorare il caso. Nel caso di RegExr, fare clic sul pulsante che legge "flag" e selezionare la casella "ignore case". Si noti che la lettera "i" è anteposta all'elenco delle opzioni alla fine dell'espressione regolare. Prova alcuni esempi con lettere maiuscole e minuscole, come "bAABaa".

Un altro esempio

Proviamo a progettare un'espressione regolare in grado di acquisire varianti del nome "Katherine". Come affronteresti questo problema? Scriverò tante variazioni, osserverò le parti comuni e poi cercherò di esprimere a parole le variazioni (con enfasi sulle lettere alternate e opzionali) come una sequenza. Successivamente, tenterei di formulare l'espressione regolare che assimila tutte queste variazioni.

Proviamoci con questa lista di varianti: Katherine, Katharine, Catherine, Kathreen, Kathleen, Katryn e Catrin. Lascerò a te decidere di scriverne altri se lo desideri. Guardando queste variazioni, posso dire più o meno che:

  • il nome inizia con "k" o "c"
  • seguito da "at"
  • seguito possibilmente da una "h"
  • eventualmente seguito da una "a" o "e"
  • seguito da una "r" o "l"
  • seguito da uno di "i", "ee" o "y"
  • e sicuramente seguito da una "n"
  • forse una "e" alla fine

Con questa idea in mente, posso venire con la seguente espressione regolare:

[Kc] ath [ae]? (R | l) (i | EE | y) ne?

Si noti che la prima riga "KatherineKatharine" ha due corrispondenze senza alcuna separazione tra loro. Se lo guardi da vicino nell'editor di testo di RegExr, puoi osservare la piccola interruzione nell'evidenziazione tra le due partite, che è quello di cui stavo parlando in precedenza.

Si noti che l'espressione regolare di cui sopra eguaglia anche nomi che non abbiamo considerato e che potrebbero non esistere nemmeno, ad esempio "Cathalin". Nel contesto attuale, ciò non influisce negativamente su di noi. Ma in alcune applicazioni, come la convalida della posta elettronica, vuoi essere più specifico sulle stringhe che combini e quelle che rifiuti. Questo di solito aggiunge alla complessità dell'espressione regolare.

Più sintassi ed esempi

Prima di passare a Swift, vorrei discutere alcuni aspetti della sintassi delle espressioni regolari.

Rappresentazioni concise

Diverse classi di personaggi correlati hanno una rappresentazione concisa:

  • \ w carattere alfanumerico, incluso il carattere di sottolineatura, equivalente a [A-zA-Z0-9_]
  • \ d rappresenta una cifra, equivalente a [0-9]
  • \S rappresenta lo spazio bianco, cioè lo spazio, la tabulazione o l'interruzione di riga

Queste classi hanno anche corrispondenti classi negative:

  • \ W rappresenta un carattere non alfanumerico, non di sottolineatura
  • \ D un non-cifra
  • \S un personaggio non spaziale

Ricorda le classi incapitalizzate e poi ricorda che la corrispondente maiuscola corrisponde a ciò che la classe nonapitalizzata non corrisponde. Nota che questi possono essere combinati includendo parentesi quadre se necessario. Per esempio, [\ S \ S] rappresenta qualsiasi carattere, comprese le interruzioni di riga. Ricorda che il periodo . corrisponde a qualsiasi carattere tranne le interruzioni di riga.

ancore

^ e $ sono ancore che rappresentano rispettivamente l'inizio e la fine di una stringa. Ricorda che ho scritto che potresti voler abbinare un'intera stringa, piuttosto che cercare le corrispondenze di sottostringa? Questo è come lo fai. ^ C [OUA] t $ corrisponde a "cat", "cot" o "cut", ma non, ad esempio, "catch" o "recut".

Confini di parole

\ b rappresenta un limite tra le parole, ad esempio dovuto allo spazio o alla punteggiatura, e anche l'inizio o la fine della stringa. Si noti che è un po 'diverso in quanto corrisponde a una posizione piuttosto che a un carattere esplicito. Potrebbe essere utile pensare a un limite di parola come a un divisore invisibile che separa una parola da quella precedente / successiva. Come ti aspetteresti, \ B rappresenta "non un limite di parole". \ BCAT \ b trova corrispondenze in "gatto", "un gatto", "Ciao, gatto", ma non in "acat" o "catch".

Negazione

L'idea della negazione può essere resa più specifica usando il ^ metacarattere all'interno di un set di caratteri. Questo è un uso completamente diverso di ^ da "inizio della stringa di ancoraggio". Ciò significa che, per la negazione, ^ deve essere utilizzato in un set di caratteri all'inizio. [^ A] corrisponde a qualsiasi carattere oltre alla lettera "a" e [^ A-z] corrisponde a qualsiasi carattere tranne una lettera minuscola.

Puoi rappresentare \ W usando la negazione e le gamme di caratteri? La risposta è [^ A-Za-z0-9_]. Cosa pensi [A ^] le partite? La risposta è un carattere "a" o "^" poiché non si è verificato all'inizio del set di caratteri. Qui "^" corrisponde letteralmente a se stesso.

In alternativa, potremmo evaderlo esplicitamente in questo modo: [\ ^ A]. Spero che tu stia iniziando a sviluppare qualche intuizione su come funziona la fuga.

quantificatori

Abbiamo visto come * (e +) può essere utilizzato per abbinare un token zero o più (e uno o più) tempi. Questa idea di abbinare un token più volte può essere resa più specifica usando quantificatori in parentesi graffe. Per esempio, 2, 4  significa da due a quattro corrispondenze del token precedente. 2, significa due o più partite e 2 significa esattamente due partite.

Vedremo esempi dettagliati che utilizzano la maggior parte di questi elementi nel prossimo tutorial. Ma per motivi di pratica, ti incoraggio a creare i tuoi esempi e testare la sintassi che abbiamo appena visto con lo strumento RegExr.

Conclusione

In questo tutorial, ci siamo concentrati principalmente sulla teoria e la sintassi delle espressioni regolari. Nel prossimo tutorial, aggiungiamo Swift al mix. Prima di proseguire, assicurati di aver compreso cosa abbiamo trattato in questo tutorial giocando con RegExr.