Espressioni regolari leggibili senza perdere il loro potere?


77

Molti programmatori conoscono la gioia di creare una rapida espressione regolare, in questi giorni spesso con l'aiuto di alcuni servizi web, o più tradizionalmente su richiesta interattiva, o forse scrivendo un piccolo script che ha l'espressione regolare in fase di sviluppo e una raccolta di casi di test . In entrambi i casi il processo è iterativo e abbastanza veloce: continua a hackerare sulla stringa dall'aspetto criptico fino a quando non corrisponde e cattura ciò che vuoi e rifiuta ciò che non vuoi.

Per un semplice caso il risultato potrebbe essere qualcosa del genere, come un regexp Java:

Pattern re = Pattern.compile(
  "^\\s*(?:(?:([\\d]+)\\s*:\\s*)?(?:([\\d]+)\\s*:\\s*))?([\\d]+)(?:\\s*[.,]\\s*([0-9]+))?\\s*$"
);

Molti programmatori conoscono anche il dolore della necessità di modificare un'espressione regolare o semplicemente di codificare un'espressione regolare in una base di codice legacy. Con un po 'di editing per dividerlo, sopra regexp è ancora molto facile da capire per chiunque abbia ragionevolmente familiarità con regexps, e un veterano di regexp dovrebbe vedere subito cosa fa (rispondere alla fine del post, nel caso in cui qualcuno desideri l'esercizio di capirlo da soli).

Tuttavia, le cose non hanno bisogno di diventare molto più complesse perché una regexp diventi una cosa veramente di sola scrittura, e anche con una documentazione diligente (cosa che tutti ovviamente fanno per tutte le regexps complesse che scrivono ...), la modifica delle regexps diventa un compito arduo. Può anche essere un compito molto pericoloso, se regexp non è accuratamente testato dall'unità (ma tutti ovviamente hanno test unitari completi per tutte le loro complesse regexps, sia positive che negative ...).

Quindi, per farla breve, esiste una soluzione / alternativa in lettura e scrittura per le espressioni regolari senza perdere il loro potere? Come sarebbe il regexp sopra con un approccio alternativo? Qualsiasi lingua va bene, sebbene una soluzione multilingue sia la migliore, nella misura in cui i regexps sono multilingue.


E poi, ciò che fa il regexp precedente è questo: analizzare una stringa di numeri in formato 1:2:3.4, catturando ogni numero, dove gli spazi sono consentiti e solo 3è richiesto.


2
cosa correlata su SO: stackoverflow.com/a/143636/674039
wim

24
Leggere / modificare regex è in realtà banale se sai cosa dovrebbero catturare. Potresti aver sentito parlare di questa funzionalità usata raramente nella maggior parte delle lingue chiamate "commenti". Se non ne metti uno sopra una regex complessa che spiega cosa fa pagherai il prezzo in seguito. Inoltre, revisione del codice.
TC1,

2
Due opzioni per ripulirlo senza romperlo in pezzi più piccoli. La loro presenza o assenza varia da lingua a lingua. (1) regex a linee estese, in cui gli spazi bianchi nella regex vengono ignorati (a meno che non siano sottoposti a escape) e viene aggiunto un modulo di commento a riga singola, in modo da poterlo suddividere in blocchi logici con rientro, interlinea e commenti. (2) gruppi di acquisizione denominati, in cui è possibile assegnare un nome a ciascun parentetico, che aggiunge entrambi un po 'di autocertificazione e popola automaticamente un hash di corrispondenze, molto meglio di una matrice di corrispondenze indicizzate numericamente o di variabili $ N.
Ben Lee,

3
Parte del problema è il linguaggio regex stesso e le cattive scelte storiche nel suo design che vengono trascinate come bagaglio. In un linguaggio sano, raggruppare le parentesi è puramente un dispositivo sintattico per modellare l'albero di analisi. Ma nelle implementazioni regex risalenti a Unix hanno semantica: registri vincolanti per le partite di sottoespressione. Quindi hai bisogno di alcune parentesi più complicate e brutte solo per ottenere un puro raggruppamento!
Kaz,

2
Non proprio una risposta pratica, ma può essere utile menzionare che il potere dell'espressione regolare è esattamente quello di un automa finito. Cioè, le regex possono validare / analizzare la stessa classe di stringhe validate e analizzate da automi finiti. Quindi, una rappresentazione leggibile dall'uomo di una regex dovrebbe probabilmente essere in grado di costruire rapidamente un grafico, e credo che la maggior parte del linguaggio basato su testo sia davvero pessimo; ecco perché usiamo strumenti visivi per queste cose. Dai un'occhiata a hackingoff.com/compilers/regular-expression-to-nfa-dfa per trarre ispirazione.
damix911,

Risposte:


80

Un certo numero di persone ha menzionato la composizione di parti più piccole, ma nessuno ha ancora fornito un esempio, quindi ecco il mio:

string number = "(\\d+)";
string unit = "(?:" + number + "\\s*:\\s*)";
string optionalDecimal = "(?:\\s*[.,]\\s*" + number + ")?";

Pattern re = Pattern.compile(
  "^\\s*(?:" + unit + "?" + unit + ")?" + number + optionalDecimal + "\\s*$"
);

Non il più leggibile, ma mi sembra che sia più chiaro dell'originale.

Inoltre, C # ha l' @operatore che può essere anteposto a una stringa per indicare che deve essere preso alla lettera (nessun carattere di escape), quindi numbersarebbe@"([\d]+)";


Proprio ora ho notato come entrambi [\\d]+e [0-9]+dovrebbero essere solo \\d+(beh, alcuni potrebbero trovare [0-9]+più leggibili). Non ho intenzione di modificare la domanda, ma potresti voler risolvere questa risposta.
hyde,

@hyde - Buona cattura. Tecnicamente non sono esattamente la stessa cosa - \dcorrisponderanno a tutto ciò che è considerato un numero, anche in altri sistemi di numerazione (cinese, arabo, ecc.), Mentre [0-9]corrisponderanno solo alle cifre standard. \\dTuttavia, mi sono standardizzato e l'ho inserito nel optionalDecimalmodello.
Bobson,

42

La chiave per documentare l'espressione regolare è documentarla. Troppo spesso le persone si lanciano in quello che sembra essere un rumore di linea e lo lasciano.

All'interno di perl l' /xoperatore alla fine dell'espressione regolare sopprime gli spazi bianchi permettendo di documentare l'espressione regolare.

L'espressione regolare di cui sopra diventerebbe quindi:

$re = qr/
  ^\s*
  (?:
    (?:       
      ([\d]+)\s*:\s*
    )?
    (?:
      ([\d]+)\s*:\s*
    )
  )?
  ([\d]+)
  (?:
    \s*[.,]\s*([\d]+)
  )?
  \s*$
/x;

Sì, consuma un po 'di spazio verticale, anche se uno potrebbe accorciarlo senza sacrificare troppo la leggibilità.

E poi, ciò che fa il regexp precedente è questo: analizza una stringa di numeri nel formato 1: 2: 3.4, catturando ogni numero, dove gli spazi sono consentiti e solo 3 è richiesto.

Guardando questa espressione regolare si può vedere come funziona (e non funziona). In questo caso, questa regex corrisponderà alla stringa 1.

Approcci simili possono essere adottati in un'altra lingua. L' opzione python re.VERBOSE funziona lì.

Perl6 (l'esempio sopra è stato per perl5) va oltre con il concetto di regole che porta a strutture ancora più potenti di PCRE (fornisce accesso ad altre grammatiche (senza contesto e sensibili al contesto) rispetto a quelle regolari e estese).

In Java (da cui deriva questo esempio), si può usare la concatenazione di stringhe per formare la regex.

Pattern re = Pattern.compile(
  "^\\s*"+
  "(?:"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #1
    ")?"+
    "(?:"+
      "([\\d]+)\\s*:\\s*"+  // Capture group #2
    ")"+
  ")?"+ // First groups match 0 or 1 times
  "([\\d]+)"+ // Capture group #3
  "(?:\\s*[.,]\\s*([0-9]+))?"+ // Capture group #4 (0 or 1 times)
  "\\s*$"
);

Certamente, questo crea molti altri "nella stringa che può portare a un po 'di confusione, può essere letto più facilmente (specialmente con l'evidenziazione della sintassi sulla maggior parte degli IDE) e documentato.

La chiave sta nel riconoscere il potere e "scrivere una volta" nella natura in cui spesso cadono le espressioni regolari. Scrivere il codice per evitarlo difensivamente in modo che l'espressione regolare rimanga chiara e comprensibile è la chiave. Formattiamo il codice Java per chiarezza: le espressioni regolari non sono diverse quando la lingua ti dà la possibilità di farlo.


13
C'è una grande differenza tra "documentare" e "aggiungere interruzioni di riga".

4
@JonofAllTrades Rendere il codice leggibile è il primo passo verso qualsiasi cosa. L'aggiunta di interruzioni di riga consente anche di aggiungere commenti per quel sottoinsieme di RE sulla stessa riga (cosa che è più difficile da fare su una singola lunga riga di testo di espressioni regolari).

2
@JonofAllTrades, non sono abbastanza d'accordo. "Documentare" e "aggiungere interruzioni di riga" non sono così diversi in quanto entrambi hanno lo stesso scopo: rendere il codice più semplice da comprendere. E per il codice mal formattato, "l'aggiunta di interruzioni di riga" serve a tale scopo molto meglio dell'aggiunta di documentazione.
Ben Lee,

2
L'aggiunta di interruzioni di riga è un inizio, ma rappresenta circa il 10% del lavoro. Altre risposte forniscono ulteriori dettagli, il che è utile.

26

La modalità "dettagliata" offerta da alcune lingue e librerie è una delle risposte a queste preoccupazioni. In questa modalità, gli spazi bianchi nella stringa regexp vengono eliminati (quindi è necessario utilizzarli \s) e sono possibili commenti. Ecco un breve esempio in Python che supporta questo per impostazione predefinita:

email_regex = re.compile(r"""
    ([\w\.\+]+) # username (captured)
    @
    \w+         # minimal viable domain part
    (?:\.w+)    # rest of the domain, after first dot
""", re.VERBOSE)

In qualsiasi lingua che non lo fa, l'implementazione di un traduttore dalla modalità verbosa a "normale" dovrebbe essere un compito semplice. Se sei preoccupato per la leggibilità dei tuoi regexps, probabilmente giustificheresti questo investimento abbastanza facilmente.


15

Ogni linguaggio che usa regex ti permette di comporli da blocchi più semplici per rendere la lettura più semplice e con qualcosa di più complicato del tuo esempio, dovresti sicuramente sfruttare questa opzione. Il problema particolare con Java e molte altre lingue è che non trattano le espressioni regolari come cittadini "di prima classe", invece richiedono loro di intrufolarsi nella lingua tramite letterali stringa. Ciò significa che molte virgolette e barre rovesciate che in realtà non fanno parte della sintassi regex e rendono le cose difficili da leggere, e anche che non si può ottenere molto più leggibile di quello senza definire efficacemente il proprio mini-linguaggio e interprete.

Il modo migliore prototipico di integrare le espressioni regolari era ovviamente Perl, con la sua opzione di spazi bianchi e gli operatori di quotazione regex. Perl 6 estende il concetto di costruire regex da parti a grammatiche ricorsive reali, che è molto meglio usare e non è affatto un paragone. La lingua potrebbe aver perso la barca della tempestività, ma il suo supporto regex era The Good Stuff (tm).


1
Per "blocchi più semplici" menzionati all'inizio della risposta, intendi solo concatenazione di stringhe o qualcosa di più avanzato?
hyde,

7
Intendevo definire sottoespressioni come letterali di stringa più brevi, assegnarle a variabili locali con nomi significativi e quindi concatenare. Trovo che i nomi siano più importanti per la leggibilità rispetto al solo miglioramento del layout.
Kilian Foth,

11

Mi piace usare Expresso: http://www.ultrapico.com/Expresso.htm

Questa applicazione gratuita ha le seguenti funzionalità che trovo utili nel tempo:

  • Puoi semplicemente copiare e incollare il tuo regex e l'applicazione lo analizzerà per te
  • Una volta che il tuo regex è stato scritto, puoi testarlo direttamente dall'applicazione (l'applicazione ti fornirà l'elenco di acquisizioni, sostituzioni ...)
  • Una volta testato, genererà il codice C # per implementarlo (nota che il codice conterrà le spiegazioni sulla tua regex).

Ad esempio, con la regex che hai appena inviato, sembrerebbe: Schermata di esempio con la regex inizialmente indicata

Certo, provarlo vale più di mille parole per descriverlo. Si noti inoltre che sono in qualche modo correlato con l'editor di questa applicazione.


4
ti dispiacerebbe spiegarlo in modo più dettagliato - come e perché risponde alla domanda posta? Le "risposte solo link" non sono del tutto benvenute allo Stack Exchange
moscerino del

5
@gnat Mi dispiace per quello. Hai assolutamente ragione. Spero che la mia risposta modificata fornisca ulteriori approfondimenti.
E. Jaep,

9

Per alcune cose, potrebbe essere utile usare solo una grammatica come BNF. Questi possono essere molto più facili da leggere rispetto alle espressioni regolari. Uno strumento come GoldParser Builder può quindi convertire la grammatica in un parser che fa il lavoro pesante per te.

Le grammatiche BNF, EBNF, ecc. Possono essere molto più facili da leggere e creare di un'espressione regolare complicata. L'ORO è uno strumento per tali cose.

Il seguente link wiki c2 ha un elenco di possibili alternative che possono essere cercate su Google, con alcune discussioni su di esse incluse. È fondamentalmente un link "vedi anche" per completare la mia raccomandazione del motore di grammatica:

Alternative alle espressioni regolari

Prendendo "alternativa" per indicare "facilità semanticamente equivalente con sintassi diversa", esistono almeno queste alternative a / con RegularExpressions:

  • Espressioni regolari di base
  • Espressioni regolari "estese"
  • Espressioni regolari compatibili con Perl
  • ... e molte altre varianti ...
  • Sintassi RE in stile SNOBOL (SnobolLanguage, IconLanguage)
  • Sintassi SRE (RE come EssExpressions)
  • diverse sintassi FSM
  • Grammatiche di intersezione a stati finiti (abbastanza espressive)
  • ParsingExpressionGrammars, come in OMetaLanguage e LuaLanguage ( http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html )
  • La modalità di analisi di RebolLanguage
  • ProbabilityBasedParsing ...

ti dispiacerebbe spiegare di più su ciò che fa questo link e a cosa serve? Le "risposte solo al collegamento" non sono del tutto benvenute allo Stack Exchange
moscerino del

1
Benvenuto in Programmers, Nick P. Per favore ignora il downvote / r, ma leggi la pagina su meta a cui @gnat si collegava.
Christoffer Lette,

@ Christoffer Lette Apprezzo la tua risposta. Proverò a tenerlo presente nei post futuri. @ Il commento di Paulo Scardine riflette l'intento dei miei post. Le grammatiche BNF, EBNF, ecc. Possono essere molto più facili da leggere e creare di un'espressione regolare complicata. L'ORO è uno strumento per tali cose. Il link c2 ha un elenco di possibili alternative che possono essere cercate su Google, con alcune discussioni su di esse incluse. Fondamentalmente era un link "vedi anche" per completare la mia raccomandazione sul motore di grammatica.
Nick P,

6

Questa è una vecchia domanda e non ho visto alcuna menzione delle espressioni verbali, quindi ho pensato di aggiungere queste informazioni anche per i futuri cercatori. Le espressioni verbali sono state progettate specificamente per rendere comprensibile la regex umana, senza bisogno di imparare il significato simbolico della regex. Vedi il seguente esempio. Penso che questo faccia meglio quello che stai chiedendo.

// Create an example of how to test for correctly formed URLs
var tester = VerEx()
    .startOfLine()
    .then('http')
    .maybe('s')
    .then('://')
    .maybe('www.')
    .anythingBut(' ')
    .endOfLine();

// Create an example URL
var testMe = 'https://www.google.com';

// Use RegExp object's native test() function
if (tester.test(testMe)) {
    alert('We have a correct URL '); // This output will fire}
} else {
    alert('The URL is incorrect');
}

console.log(tester); // Outputs the actual expression used: /^(http)(s)?(\:\/\/)(www\.)?([^\ ]*)$/

Questo esempio è per JavaScript, puoi trovare questa libreria ora per molti dei linguaggi di programmazione.


2
Questo e spettacolare!
Jeremy Thompson,

3

Il modo più semplice sarebbe usare ancora regex ma costruire la tua espressione componendo espressioni più semplici con nomi descrittivi, ad esempio http://www.martinfowler.com/bliki/ComposedRegex.html (e sì, questo è da string concat)

tuttavia, in alternativa, puoi anche utilizzare una libreria combinatrice di parser, ad esempio http://jparsec.codehaus.org/, che ti darà un parser decente completo ricorsivo. di nuovo il vero potere qui deriva dalla composizione (questa volta composizione funzionale).


3

Ho pensato che sarebbe stato degno di nota di logstash Grok espressioni. Grok si basa sull'idea di comporre lunghe espressioni di analisi da quelle più brevi. Permette di testare convenientemente questi elementi costitutivi ed è preconfezionato con oltre 100 modelli comunemente usati . Oltre a questi schemi, consente l'uso di tutta la sintassi delle espressioni regolari.

Il modello sopra espresso in grok è (ho testato nell'app debugger ma avrei potuto sbagliare):

"(( *%{NUMBER:a} *:)? *%{NUMBER:b} *:)? *%{NUMBER:c} *(. *%{NUMBER:d} *)?"

Le parti e gli spazi opzionali lo fanno sembrare un po 'più brutto del solito, ma sia qui che in altri casi, usare Grok può rendere la vita molto più bella.


2

In F # hai il modulo FsVerbalExpressions . Ti permette di comporre Regexes da espressioni verbali, ha anche alcune regex predefinite (come l'URL).

Uno degli esempi per questa sintassi è il seguente:

let groupName =  "GroupNumber"

VerbEx()
|> add "COD"
|> beginCaptureNamed groupName
|> any "0-9"
|> repeatPrevious 3
|> endCapture
|> then' "END"
|> capture "COD123END" groupName
|> printfn "%s"

// 123

Se non si ha familiarità con la sintassi F #, groupName è la stringa "GroupNumber".

Quindi creano un'espressione verbale (VerbEx) che costruiscono come "COD (? <GroupNumber> [0-9] {3}) END". Che poi testano sulla stringa "COD123END", dove ottengono il gruppo di acquisizione denominato "GroupNumber". Ciò si traduce in 123.

Sinceramente trovo la regex normale molto più facile da comprendere.


-2

Innanzitutto, capire che il codice che funziona semplicemente è un codice errato. Un buon codice deve inoltre segnalare accuratamente eventuali errori riscontrati.

Ad esempio, se stai scrivendo una funzione per trasferire denaro dal conto di un utente al conto di un altro utente; non dovresti semplicemente restituire un booleano "funzionato o fallito" perché ciò non dà al chiamante alcuna idea di cosa sia andato storto e non consente al chiamante di informare correttamente l'utente. Invece, potresti avere una serie di codici di errore (o una serie di eccezioni): impossibile trovare l'account di destinazione, fondi insufficienti nell'account di origine, autorizzazione negata, impossibile connettersi al database, carico eccessivo (riprovare più tardi), ecc. .

Ora pensa al tuo esempio "analizza una stringa di numeri in formato 1: 2: 3.4". Tutto ciò che regex fa è segnalare un "pass / fail" che non consente di presentare all'utente un feedback adeguato (se questo feedback è un messaggio di errore in un registro o una GUI interattiva in cui gli errori sono mostrati in rosso come tipi di utenti o quant'altro). Quali tipi di errori non riesce a descrivere correttamente? Carattere errato nel primo numero, primo numero troppo grande, due punti mancanti dopo il primo numero, ecc.

Per convertire "cattivo codice che funziona semplicemente" in "buon codice che fornisce errori sufficientemente descrittivi" devi suddividere la regex in molte regex più piccole (in genere, regex così piccole che è più facile farlo senza regex in primo luogo ).

Rendere il codice leggibile / mantenibile è solo una conseguenza accidentale di rendere il codice buono.


6
Probabilmente non è un buon presupposto. Il mio è perché A) Questo non affronta la domanda ( Come renderlo leggibile?), B) La corrispondenza delle espressioni regolari è pass / fail, e se la interrompi al punto in cui puoi dire esattamente perché non è riuscita, tu perdere molta potenza e velocità e aumentare la complessità, C) Non c'è alcuna indicazione dalla domanda che ci sia anche la possibilità che la partita fallisca - è semplicemente una questione di rendere leggibile il Regex. Quando hai il controllo dei dati che entrano e / o li convalidi in anticipo, puoi presumere che siano validi.
Bobson,

A) Spezzarlo in pezzi più piccoli lo rende più leggibile (come conseguenza del renderlo buono). C) Laddove stringhe sconosciute / non convalidate entrano in un software, uno sviluppatore sano analizzerebbe (con la segnalazione degli errori) a quel punto e convertirà i dati in un modulo che non necessita di riprogrammazione, dopo di che regex non è necessario. B) è una sciocchezza che si applica solo al codice errato (fare riferimento ai punti A e C).
Brendan,

Passando dal tuo C: cosa succede se questa è la sua logica di validazione? Il codice del PO potrebbe essere esattamente quello che stai suggerendo: convalidare l'input, riportare se non è valido e convertirlo in un modulo utilizzabile (tramite le acquisizioni). Tutto ciò che abbiamo è l'espressione stessa. Come consiglieresti di analizzarlo se non con una regex? Se aggiungi del codice di esempio che otterrà lo stesso risultato, rimuoverò il mio downvote.
Bobson,

Se si tratta di "C: Convalida (con segnalazione errori)", si tratta di un codice errato perché la segnalazione errori è errata. Se fallisce; era perché la stringa era NULL o perché il primo numero aveva troppe cifre o perché il primo separatore non lo era :? Immagina un compilatore con un solo messaggio di errore ("ERRORE") troppo stupido per dire all'utente quale sia il problema. Ora immagina migliaia di siti Web altrettanto stupidi e che mostrano (ad esempio) "Indirizzo e-mail errato" e niente di più.
Brendan,

Inoltre, immagina un operatore dell'help desk mezzo addestrato che riceve una segnalazione di bug da un utente non addestrato che dice: il software ha smesso di funzionare - l'ultima riga nel registro del software è "ERRORE: impossibile estrarre il numero di versione minore dalla stringa di versione '1: 2-3.4 "(due punti previsti dopo il secondo numero)"
Brendan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.