Pattern Regex da abbinare, escluso quando … / Tranne tra

– Modifica– Le risposte attuali hanno alcune idee utili ma voglio qualcosa di più completo che io possa capire e riutilizzare al 100%; è per questo che ho creato una taglia. Anche le idee che funzionano ovunque sono migliori per me rispetto alla syntax standard come \K

Questa domanda riguarda come posso abbinare un pattern ad eccezione di alcune situazioni s1 s2 s3. Do un esempio specifico per mostrare il mio significato ma preferisco una risposta generale che posso capire al 100% in modo da poterla riutilizzare in altre situazioni.

Esempio

Voglio abbinare cinque cifre usando \b\d{5}\b ma non in tre situazioni s1 s2 s3:

s1: Non su una linea che termina con un punto come questa frase.

s2: non ovunque all’interno di parens.

s3: Non all’interno di un blocco che inizia con if( e termina con //endif

So come risolvere uno qualsiasi di s1 s2 s3 con lookahead e lookbehind, specialmente in C # lookbehind o \K in PHP.

Per esempio

s1 (?m)(?!\d+.*?\.$)\d+

s3 con C # lookbehind (?<!if\(\D*(?=\d+.*?//endif))\b\d+\b

s3 con PHP \ K (?:(?:if\(.*?//endif)\D*)*\K\d+

Ma il mix di condizioni mi fa esplodere la testa. Ancora più cattive notizie è che potrebbe essere necessario aggiungere altre condizioni s4 s5 in un altro momento.

La buona notizia è che non mi interessa se elaboro i file utilizzando i linguaggi più comuni come PHP, C #, Python o la lavatrice dei miei vicini. 🙂 Sono praticamente un principiante in Python e Java, ma mi interessa sapere se ha una soluzione.

Quindi sono venuto qui per vedere se qualcuno pensa a una ricetta flessibile.

I suggerimenti vanno bene: non è necessario darmi il codice completo. 🙂

Grazie.

Hans, prenderò l’esca e rimprovererò la mia risposta precedente. Hai detto che vuoi “qualcosa di più completo” quindi spero non ti dispiaccia la lunga risposta, solo cercando di compiacere. Iniziamo con un po ‘di background.

Prima di tutto, questa è una domanda eccellente. Ci sono spesso domande sulla corrispondenza di determinati modelli, tranne in determinati contesti (ad esempio, all’interno di un blocco di codice o tra parentesi). Queste domande spesso danno luogo a soluzioni abbastanza imbarazzanti. Quindi la tua domanda su più contesti è una sfida speciale.

Sorpresa

Sorprendentemente, esiste almeno una soluzione efficiente che è generale, facile da implementare e piacevole da mantenere. Funziona con tutti gli aromi regex che consentono di ispezionare i gruppi di cattura nel codice. E capita di rispondere a una serie di domande comuni che potrebbero inizialmente sembrare diverse dalle tue: “abbina tutto tranne le ciambelle”, “sostituisci tutto tranne …”, “abbina tutte le parole tranne quelle nella lista nera di mia madre”, “ignora i tag “,” corrispondono alla temperatura se non in corsivo “…

Purtroppo, la tecnica non è ben nota: stima che in venti domande SO che potrebbero usarlo, solo una ha una risposta che la menziona, il che significa forse una risposta in cinquanta o sessanta. Vedi il mio scambio con Kobi nei commenti. La tecnica è descritta in modo approfondito in questo articolo che la chiama (ottimisticamente) la “miglior regex di sempre”. Senza entrare nel dettaglio, cercherò di darti una solida comprensione di come funziona la tecnica. Per maggiori dettagli e esempi di codice in varie lingue ti incoraggio a consultare quella risorsa.

Una variante più nota

Esiste una variante che utilizza la syntax specifica per Perl e PHP che realizza lo stesso. Lo vedrai su SO nelle mani di maestri regex come CasimiretHippolyte e HamZa . Ti dirò di più su questo argomento qui sotto, ma il mio objective è la soluzione generale che funziona con tutti gli aromi regex (a patto che tu possa ispezionare i gruppi di cattura nel tuo codice).

Grazie per tutto lo sfondo, zx81 … Ma qual è la ricetta?

Fatto chiave

Il metodo restituisce la corrispondenza nell’acquisizione del gruppo 1. Non importa affatto della partita generale.

In effetti, il trucco è quello di abbinare i vari contesti che non vogliamo (concatenando questi contesti usando il | OR / alternanza) in modo da “neutralizzarli”. Dopo aver confrontato tutti i contesti indesiderati, la parte finale dell’alternanza corrisponde a ciò che vogliamo e la cattura nel Gruppo 1.

La ricetta generale è

 Not_this_context|Not_this_either|StayAway|(WhatYouWant) 

Questo corrisponderà a Not_this_context , ma in un certo senso quella corrispondenza va in un cestino della spazzatura, perché non esamineremo le corrispondenze generali: guardiamo solo alle acquisizioni del Gruppo 1.

Nel tuo caso, con le tue cifre e i tuoi tre contesti da ignorare, possiamo fare:

 s1|s2|s3|(\b\d+\b) 

Si noti che poiché in realtà corrispondiamo a s1, s2 e s3 invece di cercare di evitarli con i lookaround, le singole espressioni per s1, s2 e s3 possono rimanere chiare come giorno. (Sono le sottoespressioni su ciascun lato di un | )

L’intera espressione può essere scritta in questo modo:

 (?m)^.*\.$|\([^\)]*\)|if\(.*?//endif|(\b\d+\b) 

Guarda questa demo (ma concentrati sui gruppi di cattura nel riquadro in basso a destra).

Se provi mentalmente a suddividere questa regex a ciascuno | delimitatore, in realtà è solo una serie di quattro espressioni molto semplici.

Per i sapori che supportano la spaziatura libera, questo si legge particolarmente bene.

 (?mx) ### s1: Match line that ends with a period ### ^.*\.$ | ### OR s2: Match anything between parentheses ### \([^\)]*\) | ### OR s3: Match any if(...//endif block ### if\(.*?//endif | ### OR capture digits to Group 1 ### (\b\d+\b) 

Questo è eccezionalmente facile da leggere e mantenere.

Estendere la regex

Quando vuoi ignorare più situazioni s4 e s5, aggiungili in più alternanze a sinistra:

 s4|s5|s1|s2|s3|(\b\d+\b) 

Come funziona?

I contesti che non si desidera vengono aggiunti a un elenco di alternanze sulla sinistra: corrisponderanno, ma queste corrispondenze generali non vengono mai esaminate, quindi abbinarle è un modo per inserirle in un “cestino della spazzatura”.

Il contenuto desiderato, tuttavia, viene acquisito nel Gruppo 1. È quindi necessario controllare a livello di programmazione che il Gruppo 1 sia impostato e non vuoto. Questo è un compito di programmazione insignificante (e in seguito parleremo di come è fatto), specialmente considerando che ti lascia una semplice regex che puoi capire a colpo d’occhio e rivedere o estendere come richiesto.

Non sono sempre un fan delle visualizzazioni, ma questo fa un buon lavoro nel mostrare quanto sia semplice il metodo. Ogni “linea” corrisponde a una potenziale corrispondenza, ma solo la linea di fondo viene catturata nel Gruppo 1.

Visualizzazione dell'espressione regolare

Debuggex Demo

Variazione Perl / PCRE

In contrasto con la soluzione generale sopra, esiste una variazione per Perl e PCRE che è spesso vista su SO, almeno nelle mani di regex Gods come @CasimiretHippolyte e @HamZa. È:

 (?:s1|s2|s3)(*SKIP)(*F)|whatYouWant 

Nel tuo caso:

 (?m)(?:^.*\.$|\([^()]*\)|if\(.*?//endif)(*SKIP)(*F)|\b\d+\b 

Questa variazione è un po ‘più facile da usare perché il contenuto corrispondente nei contesti s1, s2 e s3 viene semplicemente saltato, quindi non è necessario ispezionare le acquisizioni del Gruppo 1 (notare che le parentesi sono sparite). Le partite contengono solo ciò che whatYouWant

Si noti che (*F) , (*FAIL) e (?!) Sono la stessa cosa. Se vuoi essere più oscuro, puoi usare (*SKIP)(?!)

demo per questa versione

applicazioni

Ecco alcuni problemi comuni che questa tecnica può facilmente risolvere facilmente. Noterai che la scelta delle parole può far sembrare diversi alcuni di questi problemi mentre in effetti sono praticamente identici.

  1. Come posso abbinare foo tranne che in un tag come ... ?
  2. Come posso abbinare foo tranne che in un tag o in uno snippet di javascript (più condizioni)?
  3. Come posso abbinare tutte le parole che non sono su questa lista nera?
  4. Come posso ignorare qualsiasi cosa all’interno di un blocco SUB END SUB?
  5. Come posso abbinare tutto tranne … s1 s2 s3?

Come programmare le acquisizioni del gruppo 1

Non come per il codice, ma per il completamento … Il codice per ispezionare il Gruppo 1 dipenderà ovviamente dalla tua lingua preferita. In ogni caso non dovrebbe aggiungere più di un paio di righe al codice che useresti per ispezionare le partite.

In caso di dubbi, ti consiglio di consultare la sezione relativa agli esempi di codice dell’articolo menzionato in precedenza, che presenta il codice per alcune lingue.

alternative

A seconda della complessità della domanda e del motore regex utilizzato, esistono diverse alternative. Ecco i due che possono essere applicati alla maggior parte delle situazioni, escludendo più condizioni. A mio parere, nessuno dei due è altrettanto attraente della ricetta s1|s2|s3|(whatYouWant) , se non altro perché la chiarezza vince sempre.

1. Sostituisci quindi Abbina.

Una buona soluzione che suona come un hacky ma che funziona bene in molti ambienti è lavorare in due passaggi. Una prima espressione regolare neutralizza il contesto che si desidera ignorare sostituendo le stringhe potenzialmente in conflitto. Se vuoi solo abbinare, puoi sostituirlo con una stringa vuota, quindi eseguire la tua corrispondenza nel secondo passaggio. Se si desidera sostituire, è ansible innanzitutto sostituire le stringhe da ignorare con qualcosa di distintivo, ad esempio circondando le cifre con una catena a larghezza fissa di @@@ . Dopo questa sostituzione, sei libero di sostituire ciò che realmente volevi, quindi dovrai ripristinare le tue stringhe distintive @@@ .

2. Lookaround.

Il tuo post originale ha mostrato che comprendi come escludere una singola condizione usando i lookaround. Hai detto che C # è fantastico per questo, e hai ragione, ma non è l’unica opzione. I sapori regex .NET presenti in C #, VB.NET e Visual C ++, come pure il modulo regex ancora sperimentale per sostituire re in Python, sono gli unici due motori che conosco che supportano il lookbehind infinito. Con questi strumenti, una condizione in una sola schermata può occuparsi non solo di guardare dietro, ma anche della partita e oltre la partita, evitando il bisogno di coordinarsi con un lookahead. Più condizioni? Più soluzioni.

Riciclando la regex che avevi per s3 in C #, l’intero schema sarebbe simile a questo.

 (?!.*\.)(? 

Ma ormai sai che non lo sto raccomandando, vero?

eliminazioni

@HamZa e @Jerry hanno suggerito di menzionare un trucco aggiuntivo per i casi in cui cerchi di eliminare WhatYouWant . Ti ricordi che la ricetta per abbinarsi a WhatYouWant (catturandola nel Gruppo 1) era s1|s2|s3|(WhatYouWant) , giusto? Per eliminare tutte le istanze di WhatYouWant , si modifica la regex in

 (s1|s2|s3)|WhatYouWant 

Per la stringa di sostituzione, usi $1 . Quello che succede qui è che per ogni istanza di s1|s2|s3 che è abbinata, la sostituzione $1 sostituisce quell'istanza con se stessa (referenziata da $1 ). D'altra parte, quando WhatYouWant è abbinato, viene sostituito da un gruppo vuoto e nient'altro - e quindi cancellato. Guarda questa demo , grazie a @HamZa e @Jerry per aver suggerito questa meravigliosa aggiunta.

sostituzioni

Questo ci porta a sostituzioni, su cui toccherò brevemente.

  1. Quando si sostituisce con nulla, vedere il trucco "Eliminazioni" sopra.
  2. Quando si sostituisce, se si utilizza Perl o PCRE, usare la variazione (*SKIP)(*F) menzionata sopra per far corrispondere esattamente ciò che si desidera e fare una sostituzione diretta.
  3. In altri formati, all'interno della chiamata della funzione di sostituzione, ispezionare la corrispondenza utilizzando un callback o lambda e sostituire se è impostato Gruppo 1. Se hai bisogno di aiuto con questo, l'articolo già referenziato ti darà il codice in varie lingue.

Divertiti!

No, aspetta, c'è dell'altro!

Ah, nah, lo salverò per le mie memorie in venti volumi, per essere rilasciato la prossima spring.

Fai tre diverse corrispondenze e gestisci la combinazione delle tre situazioni usando la logica condizionale in programma. Non è necessario gestire tutto in una regex gigante.

EDIT: fammi espandere un po ‘perché la domanda è diventata più interessante 🙂

L’idea generale che si sta tentando di acquisire qui è quella di confrontarsi con un certo schema di espressioni regolari, ma non quando vi sono determinati altri pattern (potrebbero essere qualsiasi numero) presenti nella stringa di test. Fortunatamente, puoi sfruttare il tuo linguaggio di programmazione: mantieni semplice la regex e usa semplicemente un composto condizionale. Una buona pratica sarebbe quella di catturare questa idea in un componente riutilizzabile, quindi creiamo una class e un metodo che la implementano:

 using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; public class MatcherWithExceptions { private string m_searchStr; private Regex m_searchRegex; private IEnumerable m_exceptionRegexes; public string SearchString { get { return m_searchStr; } set { m_searchStr = value; m_searchRegex = new Regex(value); } } public string[] ExceptionStrings { set { m_exceptionRegexes = from es in value select new Regex(es); } } public bool IsMatch(string testStr) { return ( m_searchRegex.IsMatch(testStr) && !m_exceptionRegexes.Any(er => er.IsMatch(testStr)) ); } } public class App { public static void Main() { var mwe = new MatcherWithExceptions(); // Set up the matcher object. mwe.SearchString = @"\b\d{5}\b"; mwe.ExceptionStrings = new string[] { @"\.$" , @"\(.*" + mwe.SearchString + @".*\)" , @"if\(.*" + mwe.SearchString + @".*//endif" }; var testStrs = new string[] { "1." // False , "11111." // False , "(11111)" // False , "if(11111//endif" // False , "if(11111" // True , "11111" // True }; // Perform the tests. foreach (var ts in testStrs) { System.Console.WriteLine(mwe.IsMatch(ts)); } } } 

Così sopra, impostiamo la stringa di ricerca (le cinque cifre), più stringhe di eccezioni (il tuo s1 , s2 e s3 ), e poi proviamo a confrontarci con diverse stringhe di test. I risultati stampati dovrebbero essere come mostrato nei commenti accanto a ciascuna stringa di test.

Il tuo requisito che non è all’interno di parens in imansible da satify per tutti i casi. Vale a dire, se in qualche modo riesci a trovare un ( a sinistra e ) a destra, non sempre significa che sei all’interno di un paren. Per esempio.

(....) + 55555 + (.....) – non all’interno di parents, ma ci sono ( e ) a sinistra e a destra

Ora potresti pensare di essere intelligente e cercare ( a sinistra solo se non incontri ) prima e viceversa a destra. Questo non funzionerà per questo caso:

((.....) + 55555 + (.....)) – all’interno di parate anche se ci sono in chiusura ) e ( a sinistra ea destra.

È imansible scoprire se ci si trova all’interno di parenti usando espressioni regolari, poiché l’espressione regolare non può contare quanti parenti sono stati aperti e quanti chiusi.

Considera questo compito più semplice: usando espressioni regolari, scopri se tutti i parents (possibilmente annidati) in una stringa sono chiusi, ovvero per ogni ( devi trovarli ) . Scoprirai che è imansible da risolvere e se non riesci a risolverlo con espressioni regolari, allora non puoi capire se una parola è all’interno di parenti per tutti i casi, poiché non puoi capire in una certa posizione in stringa se tutto precedente ( avere un corrispondente ) .

Hans se non ti dispiace ho usato la lavatrice del tuo vicino chiamata Perl 🙂

Modificato: sotto uno pseudo codice:

  loop through input if line contains 'if(' set skip=true if skip= true do nothing else if line match '\b\d{5}\b' set s0=true if line does not match s1 condition set s1=true if line does not match s2 condition set s2=true if s0,s1,s2 are true print line if line contains '//endif' set skip=false 

Dato il file input.txt:

 [email protected]:~$ cat input.txt this is a text it should match 12345 if( it should not match 12345 //endif it should match 12345 it should not match 12345. it should not match ( blabla 12345 blablabla ) it should not match ( 12345 ) it should match 12345 

E lo script validator.pl:

 [email protected]:~$ cat validator.pl #! /usr/bin/perl use warnings; use strict; use Data::Dumper; sub validate_s0 { my $line = $_[0]; if ( $line =~ \d{5/ ){ return "true"; } return "false"; } sub validate_s1 { my $line = $_[0]; if ( $line =~ /\.$/ ){ return "false"; } return "true"; } sub validate_s2 { my $line = $_[0]; if ( $line =~ /.*?\(.*\d{5.*?\).*/ ){ return "false"; } return "true"; } my $skip = "false"; while (<>){ my $line = $_; if( $line =~ /if\(/ ){ $skip = "true"; } if ( $skip eq "false" ) { my $s0_status = validate_s0 "$line"; my $s1_status = validate_s1 "$line"; my $s2_status = validate_s2 "$line"; if ( $s0_status eq "true"){ if ( $s1_status eq "true"){ if ( $s2_status eq "true"){ print "$line"; } } } } if ( $line =~ /\/\/endif/) { $skip="false"; } } 

Esecuzione:

 tiago @ dell: ~ $ cat input.txt |  perl validator.pl 
 dovrebbe corrispondere a 12345
 dovrebbe corrispondere a 12345
 dovrebbe corrispondere a 12345

Non sono sicuro che questo ti possa aiutare o meno, ma sto fornendo una soluzione considerando le seguenti ipotesi:

  1. Hai bisogno di una soluzione elegante per verificare tutte le condizioni
  2. Le condizioni possono cambiare in futuro e in qualsiasi momento.
  3. Una condizione non dovrebbe dipendere dagli altri.

Comunque ho considerato anche il seguente –

  1. Il file indicato ha degli errori minimi. Se è così, il mio codice potrebbe richiedere alcune modifiche per far fronte a questo.
  2. Ho usato Stack per tenere traccia di if( blocchi.

Ok ecco la soluzione –

Ho usato C # e con esso MEF (Microsoft Extensibility Framework) per implementare i parser configurabili. L’idea è, utilizzare un parser singolo per analizzare e un elenco di classi validatore configurabili per convalidare la riga e restituire vero o falso in base alla convalida. Quindi puoi aggiungere o rimuovere qualsiasi validatore in qualsiasi momento o aggiungerne di nuovi, se lo desideri. Finora ho già implementato per S1, S2 e S3 che hai menzionato, controlla le classi al punto 3. Devi aggiungere classi per s4, s5 se ne hai bisogno in futuro.

  1. Innanzitutto, crea le interfacce –

     using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo.Contracts { public interface IParser { String[] GetMatchedLines(String filename); } public interface IPatternMatcher { Boolean IsMatched(String line, Stack stack); } } 
  2. Poi arriva il lettore di file e il correttore –

     using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using FileParserDemo.Contracts; using System.ComponentModel.Composition.Hosting; using System.ComponentModel.Composition; using System.IO; using System.Collections; namespace FileParserDemo.Parsers { public class Parser : IParser { [ImportMany] IEnumerable> parsers; private CompositionContainer _container; public void ComposeParts() { var catalog = new AggregateCatalog(); catalog.Catalogs.Add(new AssemblyCatalog(typeof(IParser).Assembly)); _container = new CompositionContainer(catalog); try { this._container.ComposeParts(this); } catch { } } public String[] GetMatchedLines(String filename) { var matched = new List(); var stack = new Stack(); using (StreamReader sr = File.OpenText(filename)) { String line = ""; while (!sr.EndOfStream) { line = sr.ReadLine(); var m = true; foreach(var matcher in this.parsers){ m = m && matcher.Value.IsMatched(line, stack); } if (m) { matched.Add(line); } } } return matched.ToArray(); } } } 
  3. Poi arriva l’implementazione delle pedine individuali, i nomi delle classi sono autoesplicativi, quindi non credo che abbiano bisogno di più descrizioni.

     using FileParserDemo.Contracts; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace FileParserDemo.PatternMatchers { [Export(typeof(IPatternMatcher))] public class MatchAllNumbers : IPatternMatcher { public Boolean IsMatched(String line, Stack stack) { var regex = new Regex("\\d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveIfBlock : IPatternMatcher { public Boolean IsMatched(String line, Stack stack) { var regex = new Regex("if\\("); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { //push the if stack.Push(m.ToString()); } //ignore current line, and will validate on next line with stack return true; } regex = new Regex("//endif"); if (regex.IsMatch(line)) { foreach (var m in regex.Matches(line)) { stack.Pop(); } } return stack.Count == 0; //if stack has an item then ignoring this block } } [Export(typeof(IPatternMatcher))] public class RemoveWithEndPeriod : IPatternMatcher { public Boolean IsMatched(String line, Stack stack) { var regex = new Regex("(?m)(?!\\d+.*?\\.$)\\d+"); return regex.IsMatch(line); } } [Export(typeof(IPatternMatcher))] public class RemoveWithInParenthesis : IPatternMatcher { public Boolean IsMatched(String line, Stack stack) { var regex = new Regex("\\(.*\\d+.*\\)"); return !regex.IsMatch(line); } } } 
  4. Il programma –

     using FileParserDemo.Contracts; using FileParserDemo.Parsers; using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; namespace FileParserDemo { class Program { static void Main(string[] args) { var parser = new Parser(); parser.ComposeParts(); var matches = parser.GetMatchedLines(Path.GetFullPath("test.txt")); foreach (var s in matches) { Console.WriteLine(s); } Console.ReadLine(); } } } 

Per il test ho preso il file di esempio di @ Tiago come Test.txt che aveva le seguenti righe:

 this is a text it should match 12345 if( it should not match 12345 //endif it should match 12345 it should not match 12345. it should not match ( blabla 12345 blablabla ) it should not match ( 12345 ) it should match 12345 

Fornisce l’output –

 it should match 12345 it should match 12345 it should match 12345 

Non so se questo ti può aiutare o no, mi diverto a giocarci … 🙂

La parte migliore è che, per aggiungere una nuova condizione, tutto ciò che devi fare è fornire un’implementazione di IPatternMatcher , verrà automaticamente richiamato e quindi verrà convalidato.

Uguale a @ zx81 (*SKIP)(*F) ma con l’utilizzo di un’asserzione lookahead negativa.

 (?m)(?:if\(.*?\/\/endif|\([^()]*\))(*SKIP)(*F)|\b\d+\b(?!.*\.$) 

DEMO

In Python, farei facilmente questo,

 import re string = """cat 123 sat. I like 000 not (456) though 111 is fine 222 if( //endif if(cat==789 stuff //endif 333""" for line in string.split('\n'): # Split the input according to the `\n` character and then iterate over the parts. if not line.endswith('.'): # Don't consider the part which ends with a dot. for i in re.split(r'\([^()]*\)|if\(.*?//endif', line): # Again split the part by brackets or if condition which endswith `//endif` and then iterate over the inner parts. for j in re.findall(r'\b\d+\b', i): # Then find all the numbers which are present inside the inner parts and then loop through the fetched numbers. print(j) # Prints the number one ny one. 

Produzione:

 000 111 222 333