118 votes

Motif Regex à faire correspondre, sauf quand... / Sauf entre

--Édition-- Les réponses actuelles ont des idées utiles mais je veux quelque chose de plus complet que je peux comprendre à 100% et réutiliser ; c'est pourquoi j'ai mis une prime. De plus, les idées qui fonctionnent partout sont meilleures pour moi que la syntaxe non standard comme \K

Cette question porte sur la façon dont je peux faire correspondre un motif sauf dans certaines situations s1 s2 s3. Je donne un exemple spécifique pour montrer ce que je veux dire mais je préfère une réponse générale que je peux comprendre à 100% pour pouvoir la réutiliser dans d'autres situations.

Exemple

Je veux faire correspondre cinq chiffres en utilisant \b\d{5}\b mais pas dans trois situations s1 s2 s3 :

s1 : Pas sur une ligne qui se termine par un point comme cette phrase.

s2 : Pas n'importe où à l'intérieur de parenthèses.

s3 : Pas à l'intérieur d'un bloc qui commence par if( et se termine par //endif

Je sais comment résoudre l'une des situations s1 s2 s3 avec un lookahead et un lookbehind, en particulier en C# lookbehind ou \K en PHP.

Par exemple

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

s3 avec C# lookbehind (?

``

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

Mais le mélange de conditions ensemble me fait exploser la tête. Encore plus de mauvaises nouvelles, c'est que je pourrais avoir besoin d'ajouter d'autres conditions s4 s5 à un autre moment.

La bonne nouvelle, c'est que je me moque de savoir si je traite les fichiers en utilisant les langages les plus courants comme PHP, C#, Python ou la machine à laver de mon voisin. :) Je suis assez débutant en Python & Java mais intéressé à apprendre s'il existe une solution.

Je suis donc venu ici pour voir si quelqu'un peut penser à une recette flexible.

Les indices sont bienvenus : vous n'avez pas besoin de me donner le code complet. :)

Merci.

``

1 votes

\K n'est pas une syntaxe spéciale en php. Veuillez élaborer et clarifier ce que vous voulez dire. Si vous voulez nous dire que vous n'avez pas besoin d'une solution "compliquée", vous devez préciser ce qui est compliqué pour vous et pourquoi.

0 votes

@hakre Vous voulez dire parce que ruby l'utilise maintenant et que cela a commencé en perl?

1 votes

Non, car ce n'est pas le PCRE qui est PHP (ni Ruby). Perl est différent, cependant PCRE vise à être compatible avec Perl Regex.

11voto

Yawar Points 2393

Faites trois matchs différents et gérez la combinaison des trois situations en utilisant une logique conditionnelle dans le programme. Vous n'avez pas besoin de tout gérer dans un énorme regex.

EDIT : laissez-moi développer un peu car la question est devenue plus intéressante :-)

L'idée générale que vous essayez de capturer ici est de correspondre à un certain motif regex, mais pas quand il y a certains autres motifs (qui pourraient être n'importe lequel) présents dans la chaîne de test. Heureusement, vous pouvez profiter de votre langage de programmation : gardez les regex simples et utilisez simplement une condition composée. Une bonne pratique serait de capturer cette idée dans un composant réutilisable, donc créons une classe et une méthode qui l'implémentent :

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();

    // Configuration de l'objet de mise en correspondance.
    mwe.SearchString = @"\b\d{5}\b";
    mwe.ExceptionStrings = new string[] {
      @"\.$"
    , @"\(.*" + mwe.SearchString + @".*\)"
    , @"if\(.*" + mwe.SearchString + @".*//endif"
    };

    var testStrs = new string[] {
      "1." // Faux
    , "11111." // Faux
    , "(11111)" // Faux
    , "if(11111//endif" // Faux
    , "if(11111" // Vrai
    , "11111" // Vrai
    };

    // Effectuer les tests.
    foreach (var ts in testStrs) {
      System.Console.WriteLine(mwe.IsMatch(ts));
    }
  }
}

Ainsi, ci-dessus, nous configurons la chaîne de recherche (les cinq chiffres), plusieurs chaînes d'exceptions (vos s1, s2 et s3), puis essayons de faire correspondre plusieurs chaînes de test. Les résultats imprimés devraient être tels que montrés dans les commentaires à côté de chaque chaîne de test.

2 votes

Tu veux peut-être dire comme faire correspondre trois regex à la suite ? Regex 1 élimine la situation 1 (peut-être juste supprimer le mauvais chiffre), r2 supprime s2, r3 supprime s3 et correspond aux chiffres restants ? C'est une idée intéressante.

0 votes

Ha, bien sûr, c'est pourquoi je t'ai donné un vote positif. :) Ne te méprends pas, je pense toujours que dans ce cas particulier, ma réponse est plus efficace et plus facile à maintenir. As-tu vu la version avec espacement libre que j'ai ajoutée hier ? C'est en une seule passe et exceptionnellement facile à lire et à maintenir. Mais j'apprécie ton travail et ta réponse détaillée. Désolé, je ne peux pas donner un autre vote positif, sinon je le ferais. :)

2voto

Avinash Raj Points 23485

Le même que @zx81's (*SKIP)(*F) mais en utilisant une assertion de recherche en arrière négative.

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

DEMO

En python, je ferais facilement comme ceci,

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'):                                  # Diviser l'entrée en fonction du caractère `\n` et puis itérer sur les parties.
    if not line.endswith('.'):                                   # Ne pas considérer la partie qui se termine par un point.
        for i in re.split(r'\([^()]*\)|if\(.*?//endif', line):   # Encore diviser la partie par des parenthèses ou une condition if qui se termine par `//endif` et puis itérer sur les parties internes.
            for j in re.findall(r'\b\d+\b', i):                  # Ensuite, trouver tous les chiffres qui sont présents à l'intérieur des parties internes et puis boucler à travers les chiffres récupérés.
                print(j)                                         # Imprime les numéros un par un.

Résultat:

000
111
222
333

2voto

U Mad Points 1456

Votre exigence selon laquelle ce n'est pas entre parenthèses est impossible à satisfaire pour tous les cas. À savoir, si vous pouvez trouver d'une manière ou d'une autre un ( à gauche et un ) à droite, cela ne signifie pas toujours que vous êtes entre parenthèses. Par exemple :

(....) + 55555 + (.....) - pas entre parenthèses alors qu'il y a ( et ) à gauche et à droite

Vous pourriez vous considérer astucieux et chercher un ( à gauche seulement si vous n'en rencontrez pas un ) auparavant et vice versa à droite. Cela ne fonctionnera pas pour ce cas :

((.....) + 55555 + (.....)) - entre parenthèses même s'il y a des ) et des ( à gauche et à droite.

Il est impossible de déterminer si vous êtes entre parenthèses en utilisant des expressions régulières, car les expressions régulières ne peuvent pas compter combien de parenthèses ont été ouvertes et combien ont été fermées.

Considérez cette tâche plus facile : en utilisant des expressions régulières, déterminez si toutes les parenthèses (éventuellement imbriquées) dans une chaîne sont fermées, c'est-à-dire pour chaque ( vous devez trouver ). Vous découvrirez que c'est impossible à résoudre et si vous ne pouvez pas résoudre cela avec des expressions régulières, alors vous ne pourrez pas non plus déterminer si un mot est entre parenthèses pour tous les cas, car vous ne pourrez pas déterminer à une certaine position dans la chaîne si tous les parenthèses précédentes ont une correspondance avec ).

2 votes

Personne n'a mentionné de parenthèses imbriquées, et votre cas n°1 est bien géré par la réponse de zx81.

0 votes

Merci pour votre gentille pensée :) mais les parenthèses imbriquées ne me préoccupent pas pour cette question, c'est plus l'idée de mauvaises situations s1 s2 s3

0 votes

Bien sûr que ce n'est pas impossible! C'est exactement pourquoi vous devez suivre le niveau de parenthèses dans lequel vous êtes actuellement en train d'analyser.

2voto

Tiago Points 2425

Hans si ça ne te dérange pas, j'ai utilisé la machine à laver de ton voisin appelée perl :)

Modifié : Ci-dessous un pseudo-code :

  boucler à travers l'entrée
  si la ligne contient 'if(' définir skip=true
        si skip= vrai ne rien faire
        sinon
           si la ligne correspond à '\b\d{5}\b' définir s0=true
           si la ligne ne correspond pas à la condition s1 définir s1=true
           si la ligne ne correspond pas à la condition s2 définir s2=true
           si s0, s1, s2 sont vrais afficher la ligne 
  si la ligne contient '//endif' définir skip=false

Étant donné le fichier input.txt :

tiago@dell:~$ cat input.txt 
c'est un texte
ça devrait correspondre à 12345
si(
ça ne devrait pas correspondre à 12345
//endif 
ça devrait correspondre à 12345
ça ne devrait pas correspondre à 12345.
ça ne devrait pas correspondre à ( blabla 12345  blablabla )
ça ne devrait pas correspondre à ( 12345 )
ça devrait correspondre à 12345

Et le script validator.pl :

tiago@dell:~$ cat validator.pl 
#! /usr/bin/perl
use warnings;
use strict;
use Data::Dumper;

sous valider_s0 {
    ma $ligne = $_[0];
    si ( $ligne =~ \d{5/ ){
        retourner "vrai";
    }
    retourner "faux";
}

sous valider_s1 {
    ma $ligne = $_[0];
    si ( $ligne =~ /\.$/ ){
        retourner "faux";
    }
    retourner "vrai";
}

sous valider_s2 {
    ma $ligne = $_[0];
    si ( $ligne =~ /.*?\(.*\d{5.*?\).*/ ){
        retourner "faux";
    }
    retourner "vrai";
}

ma $skip = "faux";
tandis que (<>){
    ma $ligne = $_;

    si( $ligne =~ /if\(/ ){
       $skip = "vrai";  
    }

    si ( $skip eq "faux" ) {
        ma $s0_statut = valider_s0 "$ligne"; 
        ma $s1_statut = valider_s1 "$ligne";
        ma $s2_statut = valider_s2 "$ligne";

        si ( $s0_statut eq "vrai"){
            si ( $s1_statut eq "vrai"){
                si ( $s2_statut eq "vrai"){
                    print "$ligne";
                }
            }
        }
    } 

    si ( $ligne =~ /\/\/endif/) {
        $skip="faux";
    }
}

Exécution :

tiago@dell:~$ cat input.txt | perl validator.pl 
ça devrait correspondre à 12345
ça devrait correspondre à 12345
ça devrait correspondre à 12345

2voto

activehigh Points 2289

Pas sûr si cela vous aiderait ou non, mais je propose une solution en tenant compte des hypothèses suivantes -

  1. Vous avez besoin d'une solution élégante pour vérifier toutes les conditions
  2. Les conditions peuvent changer à l'avenir et à tout moment.
  3. Une condition ne doit pas dépendre des autres.

Cependant j'ai également pris en compte ce qui suit -

  1. Le fichier donné a des erreurs minimales. S'il en a, alors mon code pourrait nécessiter quelques modifications pour y faire face.
  2. J'ai utilisé une pile pour garder une trace des blocs if(.

D'accord, voici la solution -

J'ai utilisé C# et avec cela MEF (Microsoft Extensibility Framework) pour implémenter les analyseurs configurables. L'idée est d'utiliser un analyseur unique pour analyser et une liste de classes de validateurs configurables pour valider la ligne et renvoyer vrai ou faux en fonction de la validation. Ensuite, vous pouvez ajouter ou supprimer n'importe quel validateur à tout moment ou en ajouter de nouveaux si vous le souhaitez. J'ai déjà implémenté jusqu'à présent pour S1, S2 et S3 que vous avez mentionnés, vérifiez les classes au point 3. Vous devrez ajouter des classes pour s4, s5 si vous en avez besoin dans le futur.

  1. Tout d'abord, Créer les Interfaces -

    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. Ensuite vient le lecteur de fichiers et le vérificateur -

    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. Ensuite vient l'implémentation des vérificateurs individuels, les noms de classe sont explicites, donc je ne pense pas qu'ils aient besoin de plus de descriptions.

    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; //s'il reste un élément dans la pile, ignorer ce bloc
            }
        }
    
        [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. Le programme -

    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();
            }
        }
    }

Pour les tests, j'ai pris le fichier d'exemple de @Tiago comme Test.txt qui contenait les lignes suivantes -

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

Donne en sortie -

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

Je ne sais pas si cela vous aidera ou non, j'ai passé un bon moment à jouer avec.... :)

La meilleure partie avec cela est que, pour ajouter une nouvelle condition, tout ce que vous avez à faire est de fournir une implémentation de IPatternMatcher, elle sera appelée automatiquement et donc sera validée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X