IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Python Discussion :

regex compléxité, optimisation [Python 3.X]


Sujet :

Python

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre éprouvé
    Homme Profil pro
    Vagabong étudiant en annalyse du signal.
    Inscrit en
    Avril 2019
    Messages
    130
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Vagabong étudiant en annalyse du signal.
    Secteur : High Tech - Multimédia et Internet

    Informations forums :
    Inscription : Avril 2019
    Messages : 130
    Par défaut regex compléxité, optimisation
    Bonjour, j'ai écrit la regex suivante pour reconnaître un nombre en python:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    model = r"((?:0b(?:[01]+(?:_[01]+)*)+)|(?:0o(?:[0-7]+(?:_[0-7]+)*)+)|(?:0x(?:[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)+)|(?:(?:\.(?:[0-9]+(?:_[0-9]+)*)+)|(?:(?:[0-9]+(?:_[0-9]+)*)+\.(?:[0-9]+(?:_[0-9]+)*)*))(?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*|(?:(?:[0-9]+(?:_[0-9]+)*)+(?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*))j?"
    recherche = re.match(model, chaine[start_rank:].lower())
    Cela fonctionne très bien sauf pour des nombres grand, par exemple 121623452345213467809856784, met une dizaine de secondes à être compris...
    Il suffit de rajouter une décimal, et là le temps n'est plus acceptable!

    Comment faire pour l'optimiser?

  2. #2
    Expert éminent
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 762
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 762
    Par défaut
    Salut,

    Citation Envoyé par robinechuca Voir le message
    Comment faire pour l'optimiser?
    Faire une seule regex pour parser un nombre qu'il soit binaire, octal, hexadécimal ou décimal, çà fait juste un pattern illisible où vous allez devoir encoder des conditions, mettre des lookahead.

    Déjà si vous testiez les deux premiers caractères pour trouver la base puis faire bosser int serait plus de la programmation Python (que l'optimisation de pattern de regexp qui n'a rien de trop spécifique à Python).

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  3. #3
    Membre chevronné
    Homme Profil pro
    BTS SN IR
    Inscrit en
    Mai 2017
    Messages
    514
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 26
    Localisation : France, Saône et Loire (Bourgogne)

    Informations professionnelles :
    Activité : BTS SN IR

    Informations forums :
    Inscription : Mai 2017
    Messages : 514
    Par défaut
    Est ce que le regex est obligatoire?
    Si non une boucle avec try int except et voilà (int à un argument facultatif base)

  4. #4
    Expert confirmé Avatar de disedorgue
    Homme Profil pro
    Ingénieur intégration
    Inscrit en
    Décembre 2012
    Messages
    4 362
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France

    Informations professionnelles :
    Activité : Ingénieur intégration
    Secteur : High Tech - Opérateur de télécommunications

    Informations forums :
    Inscription : Décembre 2012
    Messages : 4 362
    Par défaut
    Tu es sur que ta regex peut différencier tous les cas, comme par exemple "0x123450b111000" ?
    Ici, c'est que de l'hexa ou une partie hexa et une partie binaire ?
    Ce qui nous manque ici, c'est un sample pour savoir ce que peut contenir une chaine en entrée.

    D'une manière générale et quelque soit le langage, une optimisation de regex se fait en connaissant la forme de la chaine que l'on parse.

  5. #5
    Expert confirmé Avatar de CosmoKnacki
    Homme Profil pro
    Justicier interdimensionnel
    Inscrit en
    Mars 2009
    Messages
    2 986
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Charente Maritime (Poitou Charente)

    Informations professionnelles :
    Activité : Justicier interdimensionnel

    Informations forums :
    Inscription : Mars 2009
    Messages : 2 986
    Par défaut
    Dans l'état actuelle des choses, ta pattern est illisible. On va donc commencer par utiliser le flag re.VERBOSE qui permet d'ignorer les espacements et d'ajouter des commentaires inline tout en indentant pour faire apparaître la structure de la pattern. Je vais utiliser la méthode re.compile non pas pour influer sur les performances (car ça ne change rien), mais juste pour que la pattern et les flags soient au même endroit:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    pat = re.compile(r'''
    (   # binary
        (?:0b(?:[01]+(?:_[01]+)*)+)
      | # octal
        (?:0o(?:[0-7]+(?:_[0-7]+)*)+)
      | # hexadecimal
        (?:0x(?:[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)+)
      | # decimal
        (?:
            (?:\.(?:[0-9]+(?:_[0-9]+)*)+)
          |
            (?:(?:[0-9]+(?:_[0-9]+)*)+\.(?:[0-9]+(?:_[0-9]+)*)*)
        )
           # exponant (optional)
        (?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*
      | # integer
        (?:
            (?:[0-9]+(?:_[0-9]+)*)+
            # exponant (optional)
            (?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*
        )
    )j?''', re.VERBOSE)
    Ensuite on enlève ce qui est inutile dont des groupes, des échappements qui n'ont pas lieu d'être, les lettres majuscules:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    pat = re.compile(r'''
    (   # binary
        0b (?: [01]+ (?:_[01]+)* )+
      | # octal
        0o (?: [0-7]+ (?:_[0-7]+)* )+
      | # hexadecimal
        0x (?: [0-9a-f]+ (?:_[0-9a-f]+)* )+
      | # decimal
        (?:
            \. (?: [0-9]+ (?:_[0-9]+)* )+
          |
            (?: [0-9]+ (?:_[0-9]+)* )+
            \. (?: [0-9]+ (?:_[0-9]+)* )*
        )
           # exponant (optional)
        (?: e [+-]? (?: [0-9]+ (?:_[0-9]+)* )+ )*
      | # integer
        (?: [0-9]+ (?:_[0-9]+)* )+
            # exponant (optional)
        (?: e[+-]? (?: [0-9]+ (?:_[0-9]+)* )+ )*
    )j?''', re.VERBOSE)
    On y voit déjà plus clair, et c'est là qu'on remarque qu'il y a des constructions bizarres répétées un peu partout avec un mésusage des quantificateurs + (1 ou plus) et * (0 ou plus). Prenons par exemple la première branche, celle des nombres binaires: 0b (?: [01]+ (?:_[01]+)* )+
    Tu utilises le groupe répété (?: ... )+ pour t'assurer qu'il y a au moins un chiffre après le b. C'est inutile, [01]+ le garantit déjà. Donc on peut supprimer ce groupe répété et écrire simplement 0b [01]+ (?:_[01]+)*
    C'est cette construction qui est la principale cause de la lenteur de ta pattern, car elle s'apparente à la pattern pathologique (a*)*b (qui en l'absence de b va essayer tous les découpages possibles et imaginables du groupe répété avant d'échouer). (Faire des recherches sur le mécanisme de Backtracking).

    Même type d'erreur pour la partie décimale des nombres ou l'exposant qui sont optionnels avec (?: ... )*. * signifie 0 ou plus, or tu ne vas pas répéter une partie décimale ou un exposant 25 fois! Le quantificateur approprié est plutôt ? (0 ou 1) ou encore {0,1}:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    pat = re.compile(r'''
    (   # binary
        0b [01]+ (?:_[01]+)*
      | # octal
        0o [0-7]+ (?:_[0-7]+)*
      | # hexadecimal
        0x [0-9a-f]+ (?:_[0-9a-f]+)*
      | # decimal
        (?:
            \. [0-9]+ (?:_[0-9]+)*
          |
            [0-9]+ (?:_[0-9]+)*
            \. (?: [0-9]+ (?:_[0-9]+)* )?
        )
           # exponant (optional)
        (?: e [+-]? [0-9]+ (?:_[0-9]+)* )?
      | # integer
        [0-9]+ (?:_[0-9]+)*
            # exponant (optional)
        (?: e [+-]? [0-9]+ (?:_[0-9]+)* )?
    )j?''', re.VERBOSE)
    Occupons-nous maintenant des branches.

    Tu as trois branches (entiers et nombres décimaux mis à part) qui commencent toutes par un 0. Dans l'état actuel de ta pattern ça signifie que si la chaîne ne commence pas par 0, ces trois branches vont échouer et le 0 sera testé 3 fois pour rien. En mettant ce 0 en facteur on réduit le nombre d'échecs à 1.

    Idem avec les décimaux et les entiers, en rendant la partie décimale optionnelle, on peut se passer de la branche des entiers.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    pat = re.compile(r'''
    (
        0
        (?: # binary
            b [01]+ (?:_[01]+)*
          | # octal
            o [0-7]+ (?:_[0-7]+)*
          | # hexadecimal
            x [0-9a-f]+ (?:_[0-9a-f]+)*
        )
      | # decimal
        (?:
            \. [0-9]+ (?:_[0-9]+)*
          |
            [0-9]+ (?:_[0-9]+)*
            (?: \. (?: [0-9]+ (?:_[0-9]+)* )? )?
        )
           # exponant
        (?: e [+-]? [0-9]+ (?:_[0-9]+)* )?
    )j?''', re.VERBOSE)
    Avec ces factorisations on fait passer le nombre de points d'entrée de 6 à 3. À contrario, une factorisation après plusieurs branches, si elle réduit la taille de la pattern, ne change pas le nombre de points d'entrée, comme c'est le cas avec l'exposant. On peut le distribuer pour gagner un niveau d'imbrication:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pat = re.compile(r'''
    (   #     binary                octal                   hexadecimal
        0 (?: b [01]+ (?:_[01]+)* | o [0-7]+ (?:_[0-7]+)* | x [0-9a-f]+ (?:_[0-9a-f]+)* )
      | # decimal
        \. [0-9]+ (?:_[0-9]+)*
        (?: e [+-]? [0-9]+ (?:_[0-9]+)* )?
      |
        [0-9]+ (?:_[0-9]+)* \.? (?: [0-9]+ (?:_[0-9]+)* )?
        (?: e [+-]? [0-9]+ (?:_[0-9]+)* )?
    )j?''', re.VERBOSE)
     
    recherche = pat.match(chaine[start_rank:].lower())
    Voilà qui devrait réduire considérablement le temps d'exécution de la pattern et la rendre lisible. Néanmoins, mon petit doigt me dit qu'elle ne doit pas être seule en cause.

    NB: si tu choisis d'utiliser re.compile comme dans l'exemple, assures-toi que celui-ci n'est exécuté qu'une seule fois pour tout le script (donc pas dans une boucle ni dans une fonction). Dans le cas contraire, n'oublie pas de passer le flag re.VERBOSE en paramètre de re.match.

  6. #6
    Expert confirmé
    Avatar de fred1599
    Homme Profil pro
    Lead Dev Python
    Inscrit en
    Juillet 2006
    Messages
    4 080
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Meurthe et Moselle (Lorraine)

    Informations professionnelles :
    Activité : Lead Dev Python
    Secteur : Arts - Culture

    Informations forums :
    Inscription : Juillet 2006
    Messages : 4 080
    Par défaut
    Citation Envoyé par robinechuca Voir le message
    Bonjour, j'ai écrit la regex suivante pour reconnaître un nombre en python:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     
    model = r"((?:0b(?:[01]+(?:_[01]+)*)+)|(?:0o(?:[0-7]+(?:_[0-7]+)*)+)|(?:0x(?:[0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)+)|(?:(?:\.(?:[0-9]+(?:_[0-9]+)*)+)|(?:(?:[0-9]+(?:_[0-9]+)*)+\.(?:[0-9]+(?:_[0-9]+)*)*))(?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*|(?:(?:[0-9]+(?:_[0-9]+)*)+(?:e[\+\-]?(?:[0-9]+(?:_[0-9]+)*)+)*))j?"
    recherche = re.match(model, chaine[start_rank:].lower())
    Cela fonctionne très bien sauf pour des nombres grand, par exemple 121623452345213467809856784, met une dizaine de secondes à être compris...
    Bonjour,

    Peut-on avoir l'ensemble des tests ?

    Merci de définir un peu plus le contexte, d'où vient chaîne ? Ont-elles été traitées préalablement par d'autres outils, fonctions, etc... ?

    C'est beaucoup trop flou pour rendre quelque chose de générique, d'ailleurs ce qui serait intéressant, c'est que ça ne réponde qu'aux tests que vous avez défini avant de coder.

    @CosmoKnacki

    +1 pour le taf !

  7. #7
    Expert confirmé
    Avatar de tyrtamos
    Homme Profil pro
    Retraité
    Inscrit en
    Décembre 2007
    Messages
    4 486
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Var (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Retraité

    Informations forums :
    Inscription : Décembre 2007
    Messages : 4 486
    Billets dans le blog
    6
    Par défaut
    Bonjour,

    @CosmoKnacki => bravo pour le boulot! Je trouve aussi que "re.VERBOSE" est pratique, voire indispensable, pour mettre au point les motifs regex complexes.

  8. #8
    Expert éminent
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 762
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 762
    Par défaut
    Salut,

    @CosmoKnacki: jolie démonstration. Il faut juste ajouter des ^ et des $ pour que çà ne matche pas des "0b1012". En Python, çà pourrait s'écrire:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def is_number(s):
        bases = { '0b':2, '0o':8, '0x':16 }
        if (base := bases.get(s[:2])):
            f = lambda: int(s[2:], base)
        else:
            f = lambda: (float if '.' in n else int)(s)
     
        try:
            f()
        except:
            return False
        return True
    un poil moins rapide (çà fabrique un entier ou un float pour le jeter).

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

  9. #9
    Expert confirmé Avatar de CosmoKnacki
    Homme Profil pro
    Justicier interdimensionnel
    Inscrit en
    Mars 2009
    Messages
    2 986
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Charente Maritime (Poitou Charente)

    Informations professionnelles :
    Activité : Justicier interdimensionnel

    Informations forums :
    Inscription : Mars 2009
    Messages : 2 986
    Par défaut
    @tyrtamos: C'est pareil et plus ça va plus j'ai tendance à l'utiliser même sur des patterns courtes. À noter que c'est devenu le mode par défaut en Perl 6.

    @Wiztricks: Je ne l'ai pas ajouté car je ne sais pas trop ce qu'il veut en faire (validation ou extraction) et parce que la pattern est déjà ancrée au début (utilisation de la méthode re.match sur une sous-chaîne) et a un groupe de capture (ce qui me fait plutôt pencher vers une extraction).
    S'il s'agit d'une validation de chaîne complète il faut utiliser re.fullmatch (si disponible) ou juste ajouter l'ancre de fin $ à la pattern, dans tous les cas l'ancre de début de chaîne est inutile.

  10. #10
    Expert éminent
    Homme Profil pro
    Architecte technique retraité
    Inscrit en
    Juin 2008
    Messages
    21 762
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Manche (Basse Normandie)

    Informations professionnelles :
    Activité : Architecte technique retraité
    Secteur : Industrie

    Informations forums :
    Inscription : Juin 2008
    Messages : 21 762
    Par défaut
    Salut,

    Citation Envoyé par CosmoKnacki Voir le message
    @Wiztricks: Je ne l'ai pas ajouté car je ne sais pas trop ce qu'il veut en faire (validation ou extraction) et parce que la pattern est déjà ancrée au début (utilisation de la méthode re.match sur une sous-chaîne) et a un groupe de capture (ce qui me fait plutôt pencher vers une extraction).
    Moi je dis çà parce que j'ai "testé" avant de mesurer les performances des 2 moutures et que j'ai eu cette surprise...

    - W
    Architectures post-modernes.
    Python sur DVP c'est aussi des FAQs, des cours et tutoriels

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. [Python 3.X] Optimisation : liste de regex dynamique
    Par Shampra dans le forum Général Python
    Réponses: 11
    Dernier message: 26/01/2016, 19h21
  2. [RegEx] Optimiser/"factoriser" un masque regex
    Par Invité dans le forum Langage
    Réponses: 4
    Dernier message: 02/10/2013, 14h36
  3. Un souci d'optimisation avec une "petite" Regex
    Par Sehnsucht dans le forum Général Dotnet
    Réponses: 0
    Dernier message: 17/05/2010, 18h29
  4. Réponses: 5
    Dernier message: 06/01/2010, 12h02
  5. [Regex] exemple de l'API optimisable ?
    Par Celeborn dans le forum Collection et Stream
    Réponses: 6
    Dernier message: 10/12/2005, 23h52

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo