Espressioni regolari leggibili senza perdere il loro potere?

75

Molti programmatori conoscono la gioia di scatenare un'espressione regolare veloce, in questi giorni spesso con l'aiuto di alcuni servizi Web, o più tradizionalmente al prompt interattivo, o magari scrivendo un piccolo script che ha l'espressione regolare in fase di sviluppo, e una raccolta dei casi di test. In entrambi i casi il processo è iterativo e abbastanza veloce: continua a hackerare la stringa dall'aspetto enigmatico finché non trova corrispondenze e cattura ciò che vuoi e rifiuterà ciò che non vuoi.

Per un semplice caso il risultato potrebbe essere qualcosa di simile a questo, come un'espressione regolare in Java:

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

Molti programmatori conoscono anche il dolore di dover modificare un'espressione regolare o semplicemente codificare attorno a un'espressione regolare in una base di codice legacy. Con un po 'di editing per dividerlo, sopra regexp è ancora molto facile da comprendere per chiunque abbia ragionevolmente familiarità con espressioni regolari, e un veterano delle espressioni regolari dovrebbe vedere subito cosa fa (risposta alla fine del post, nel caso qualcuno voglia l'esercizio di capire da soli).

Tuttavia, non è necessario che le cose diventino molto più complesse perché una regexp diventi veramente qualcosa di sola scrittura, e anche con una documentazione diligente (che tutti naturalmente fanno per tutte le espressioni regolari complesse che scrivono ...), la modifica delle espressioni regolari diventa un compito scoraggiante. Può anche essere un compito molto pericoloso, se regexp non viene attentamente testato (ma tutti ovviamente hanno test unitari completi per tutte le loro espressioni regolari complesse, sia positive che negative ...).

Quindi, per farla breve, c'è una soluzione / alternativa di lettura / scrittura per le espressioni regolari senza perdere il loro potere? Come apparirebbe la precedente regexp con un approccio alternativo? Qualsiasi linguaggio va bene, anche se una soluzione multi-lingua sarebbe la migliore, nella misura in cui le espressioni regolari sono multi-lingua.

E poi, ciò che fa la regexp precedente è questa: analizza una stringa di numeri in formato 1:2:3.4 , catturando ciascun numero, dove sono consentiti spazi e solo 3 è richiesto.

    
posta hyde 15.04.2013 - 14:44
fonte

11 risposte

80

Un certo numero di persone ha menzionato la composizione da 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 number sarebbe @"([\d]+)";

    
risposta data 15.04.2013 - 17:04
fonte
42

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

All'interno di perl l'operatore /x alla fine dell'espressione regolare sopprime lo spazio bianco che consente di documentare l'espressione regolare .

L'espressione regolare precedente diventerebbe quindi:

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

Sì, è un po 'consumante di spazi bianchi verticali, anche se uno potrebbe accorciarlo senza sacrificare troppa leggibilità.

And then, what the earlier regexp does is this: parse a string of numbers in format 1:2:3.4, capturing each number, where spaces are allowed and only 3 is required.

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

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

Perl6 (l'esempio precedente era per perl5) lo fa ulteriormente con il concetto di regole che porta a ancora più potente strutture rispetto al PCRE (fornisce accesso ad altre grammatiche (context free e context sensitive) piuttosto che normali ed estese regolari).

In Java (da cui viene estratto questo esempio), è possibile utilizzare la concatenazione di stringhe per formare l'espressione regolare.

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 molto più " nella stringa che potrebbe portare ad una certa confusione in questo, può essere più facilmente letto (specialmente con l'evidenziazione della sintassi sulla maggior parte degli IDE) e documentato.

La chiave è riconoscere la potenza e "scrivere una volta" la natura in cui spesso cadono le espressioni regolari. Scrivere il codice per evitare in modo difensivo questo in modo che l'espressione regolare rimanga chiara e comprensibile sia la chiave. Formiamo il codice Java per chiarezza: le espressioni regolari non sono diverse quando la lingua ti dà la possibilità di farlo.

    
risposta data 15.04.2013 - 16:54
fonte
26

La modalità "verbose" offerta da alcune lingue e librerie è una delle risposte a queste preoccupazioni. In questa modalità, lo spazio bianco nella stringa regexp viene rimosso (quindi è necessario utilizzare \s ) e i commenti sono possibili. 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 è, l'implementazione di un traduttore dalla modalità dettagliata a "normale" dovrebbe essere un compito semplice. Se sei preoccupato della leggibilità delle espressioni regolari, probabilmente giustificherai abbastanza facilmente questo investimento temporale.

    
risposta data 15.04.2013 - 16:28
fonte
15

Ogni linguaggio che usa espressioni regolari consente di comporli da blocchi più semplici per facilitare la lettura, e con qualcosa di più complicato del (o complicato come) esempio, dovresti sicuramente approfittare di questa opzione. Il guaio particolare con Java e molti altri linguaggi è che non considerano le espressioni regolari come cittadini di "prima classe", ma richiedono loro di intrufolarsi nella lingua tramite stringhe letterali. Ciò significa che molte delle virgolette e dei backslash che non fanno parte della sintassi regex e rendono le cose difficili da leggere, e significa anche che non puoi ottenere molto più leggibile di quello senza definire efficacemente il tuo mini-linguaggio e interprete.

Il modo migliore per integrare prototipicamente le espressioni regolari era naturalmente Perl, con la sua opzione di spazi bianchi e operatori di regex-quoting. Perl 6 estende il concetto di reisex da parti a grammatiche reali ricorsive, il che è molto meglio usarlo, in realtà non c'è paragone. La lingua potrebbe aver perso la barca della tempestività, ma il supporto per le espressioni regolari era The Good Stuff (tm).

    
risposta data 15.04.2013 - 14:52
fonte
11

Mi piace usare Expresso: link

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

  • Puoi semplicemente copiare e incollare la tua regex e l'applicazione la analizzerà per te
  • Una volta scritta la tua espressione regolare, puoi testarla direttamente dall'applicazione (l'applicazione ti fornirà l'elenco di catture, sostituzioni ...)
  • Una volta testato, genererà il codice C # per implementarlo (si noti che il codice conterrà le spiegazioni sulla regex).

Ad esempio, con la regex appena inviata, sembrerebbe:

Naturalmente, provarlo vale più di mille parole per descriverlo. Si prega di notare che io sono legato in qualche modo con l'editor di questa applicazione.

    
risposta data 15.04.2013 - 16:10
fonte
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 esegue il sollevamento pesante per te.

Le grammatiche BNF, EBNF, ecc. possono essere molto più facili da leggere e creare di una normale espressione complicata. GOLD è uno strumento per queste cose.

Il link wiki c2 sotto ha una lista di possibili alternative che possono essere consultate, con alcune discussioni su di esse incluse. È fondamentalmente un link "vedi anche" per completare la mia raccomandazione sul motore di grammatica:

Alternative alle espressioni regolari

Taking "alternative" to mean "semantically equivalent facility with different syntax", there are at least these alternatives to/with RegularExpressions:

  • Basic regular expressions
  • "Extended" regular expressions
  • Perl-compatible regular expressions
  • ... and many other variants...
  • SNOBOL-style RE syntax (SnobolLanguage, IconLanguage)
  • SRE syntax (RE's as EssExpressions)
  • different FSM syntaces
  • Finite-state intersection grammars (quite expressive)
  • ParsingExpressionGrammars, as in OMetaLanguage and LuaLanguage (http://www.inf.puc-rio.br/~roberto/lpeg/lpeg.html)
  • The parse mode of RebolLanguage
  • ProbabilityBasedParsing...
    
risposta data 15.04.2013 - 20:06
fonte
4

Questa è una vecchia domanda e non ho visto alcuna menzione di espressioni verbali quindi ho pensato di aggiungere le informazioni qui come bene per i futuri ricercatori. Le espressioni verbali sono state progettate specificamente per rendere comprensibile l'espressione regolare, senza la necessità di imparare il significato simbolico della regex. Vedi il seguente esempio. Penso che questo faccia al meglio quello che chiedi.

// 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, ora puoi trovare questa libreria per molti linguaggi di programmazione.

    
risposta data 13.10.2016 - 21:15
fonte
3

Il modo più semplice sarebbe quello di usare ancora espressioni regolari, ma costruire la tua espressione dalla composizione di espressioni più semplici con nomi descrittivi, ad es. link (e sì, questo è da string concat)

tuttavia come alternativa potresti anche usare una libreria combinatore di parser, ad es. link che ti darà un parser completo ricorsivo e decente. di nuovo il vero potere qui viene dalla composizione (questa volta composizione funzionale).

    
risposta data 15.04.2013 - 16:23
fonte
3

Ho pensato che valesse la pena menzionare le grok espressioni logstash di logstash. Grok si basa sull'idea di comporre lunghe espressioni di parsing da quelle più brevi. Permette di testare convenientemente questi elementi costitutivi e viene fornito con più di 100 modelli comunemente utilizzati . Oltre a questi pattern, consente l'uso di tutte le sintassi delle espressioni regolari.

Il modello sopra espresso in grok è (ho provato nell'applicazione debugger ma potrebbe essere stato erroneamente incastrato):

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

Le parti e gli spazi opzionali lo rendono un po 'più brutto del solito, ma sia qui che in altri casi, l'utilizzo di grok può rendere la vita molto più bella.

    
risposta data 17.04.2013 - 23:02
fonte
2

In F # hai il modulo FsVerbalExpressions . Ti permette di comporre Regexes dalle espressioni verbali, ha anche alcune espressioni regolari pre-costruite (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 hai 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 cattura denominato "GroupNumber". Questo risulta in 123.

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

    
risposta data 08.02.2017 - 13:26
fonte
-2

Innanzitutto, capisci che il codice che funziona è solo un codice errato. Il buon codice ha anche bisogno di riportare accuratamente ogni errore riscontrato.

Ad esempio, se stai scrivendo una funzione per trasferire denaro dall'account di un utente all'account di un altro utente; non si restituisce semplicemente un valore booleano "lavorato o non riuscito" poiché ciò non dà al chiamante alcuna idea di cosa è andato storto e non consente al chiamante di informare l'utente correttamente. Invece, potresti avere una serie di codici di errore (o un insieme di eccezioni): impossibile trovare l'account di destinazione, fondi insufficienti nell'account sorgente, autorizzazione negata, impossibile connettersi al database, troppo carico (riprova più tardi), ecc. .

Ora pensa al tuo esempio "analizza una stringa di numeri in formato 1: 2: 3.4". Tutta la regex riporta un "pass / fail" che non consente all'utente di presentare un feedback adeguato (se questo feedback è un messaggio di errore in un log o una GUI interattiva in cui gli errori vengono visualizzati in rosso come tipi di utente, o qualsiasi altra cosa). 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 "codice erroneo che funziona semplicemente" in "codice buono che fornisce errori adeguatamente descrittivi" devi interrompere l'espressione regolare in molte espressioni regex più piccole (in genere, espressioni regex talmente piccole che è più facile farlo senza espressioni regex in il primo posto).

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

    
risposta data 18.04.2013 - 02:02
fonte

Leggi altre domande sui tag