Python >> Tutoriel Python >  >> Python Tag >> RegEx

Compilation d'expressions régulières Python

La méthode re.compile(pattern) renvoie un objet d'expression régulière à partir du pattern qui fournit des méthodes regex de base telles que pattern.search(string) , pattern.match(string) , et pattern.findall(string) . L'approche explicite en deux étapes consistant à (1) compiler et (2) rechercher le modèle est plus efficace que d'appeler, par exemple, search(pattern, string) à la fois, si vous faites correspondre le même modèle plusieurs fois, car cela évite les compilations redondantes du même modèle.

Pourquoi les expressions régulières ont-elles survécu à sept décennies de bouleversement technologique ? Parce que les codeurs qui comprennent les expressions régulières avoir un énorme avantage lorsque vous travaillez avec des données textuelles . Ils peuvent écrire en une seule ligne de code ce qui en prend des dizaines à d'autres !

Cet article concerne le re.compile(pattern) méthode de Python re bibliothèque. Avant de plonger dans re.compile() , voyons un aperçu des quatre méthodes associées que vous devez comprendre :

  • Le findall(pattern, string) la méthode renvoie une liste de correspondances de chaînes . Pour en savoir plus, consultez le didacticiel de notre blog.
  • Le search(pattern, string) la méthode renvoie un objet de correspondance de la première correspondance . En savoir plus dans notre tutoriel de blog.
  • Le match(pattern, string) la méthode renvoie un objet de correspondance si l'expression régulière correspond au début de la chaîne . Pour en savoir plus, consultez le didacticiel de notre blog.
  • Le fullmatch(pattern, string) la méthode renvoie un objet de correspondance si l'expression régulière correspond à la chaîne entière . En savoir plus dans notre tutoriel de blog.

Article connexe : Python Regex Superpower - Le guide ultime

Fort de cet aperçu rapide des méthodes regex les plus critiques, répondons à la question suivante :

Comment fonctionne re.compile() en Python ?

Le re.compile(pattern) La méthode renvoie un objet d'expression régulière. Vous utilisez ensuite l'objet pour appeler des méthodes regex importantes telles que search(string) , match(string) , fullmatch(string) , et findall(string) .

En bref :vous compilez d'abord le modèle. Vous recherchez le motif en une seconde de chaîne.

Cette approche en deux étapes est plus efficace que d'appeler, par exemple, search(pattern, string) immediatement. Autrement dit, SI vous appelez le search() méthode plusieurs fois sur le même motif . Pourquoi? Parce que vous pouvez réutiliser le modèle compilé plusieurs fois.

Voici un exemple :

import re

# These two lines ...
regex = re.compile('Py...n')
match = regex.search('Python is great')

# ... are equivalent to ...
match = re.search('Py...n', 'Python is great')

Dans les deux cas, la variable match contient l'objet match suivant :

<re.Match object; span=(0, 6), match='Python'>

Mais dans le premier cas, on peut trouver le motif non seulement dans la chaîne 'Python is great ' mais aussi dans d'autres chaînes, sans aucun travail redondant consistant à compiler le modèle encore et encore.

Spécification :

re.compile(pattern, flags=0)

La méthode a jusqu'à deux arguments.

  • pattern  :le modèle d'expression régulière que vous souhaitez faire correspondre.
  • flags (argument optionnel) :un modificateur plus avancé qui permet de personnaliser le comportement de la fonction. Vous voulez savoir comment utiliser ces drapeaux ? Consultez cet article détaillé sur le blog Finxter.

Nous explorerons ces arguments plus en détail plus tard.

Valeur de retour :

Le re.compile(patterns, flags) La méthode renvoie un objet d'expression régulière. Vous pouvez demander (et à juste titre) :

Qu'est-ce qu'un objet d'expression régulière ?

Python crée en interne un objet d'expression régulière (à partir du Pattern class) pour préparer le processus de correspondance de modèle. Vous pouvez appeler les méthodes suivantes sur l'objet regex :

Méthode Description
Pattern.search(string[, pos[, endpos]]) Recherche l'expression régulière n'importe où dans la chaîne et renvoie un objet match ou None . Vous pouvez définir les positions de début et de fin de la recherche.
Pattern.match(string[, pos[, endpos]]) Recherche la regex au début de la chaîne et renvoie un objet match ou None . Vous pouvez définir les positions de début et de fin de la recherche.
Pattern.fullmatch(string[, pos[, endpos]]) Faire correspondre l'expression régulière avec la chaîne entière et renvoie un objet match ou None . Vous pouvez définir les positions de début et de fin de la recherche.
Pattern.split(string, maxsplit=0) Divise la chaîne en une liste de sous-chaînes. La regex est le délimiteur. Vous pouvez définir un nombre maximum de divisions.
Pattern.findall(string[, pos[, endpos]]) Recherche l'expression régulière n'importe où dans la chaîne et renvoie une liste des sous-chaînes correspondantes . Vous pouvez définir les positions de début et de fin de la recherche.
Pattern.finditer(string[, pos[, endpos]]) Renvoie un itérateur qui couvre toutes les correspondances de l'expression régulière dans la chaîne (renvoie un objet match après l'autre). Vous pouvez définir les positions de début et de fin de la recherche.
Pattern.sub(repl, string, count=0) Renvoie une nouvelle chaîne en remplaçant le premier count occurrences de la regex dans la chaîne (de gauche à droite) avec la chaîne de remplacement repl .
Pattern.subn(repl, string, count=0) Renvoie une nouvelle chaîne en remplaçant le premier count occurrences de la regex dans la chaîne (de gauche à droite) avec la chaîne de remplacement repl . Cependant, il renvoie un tuple avec la chaîne remplacée comme première valeur et le nombre de remplacements réussis comme deuxième valeur de tuple.

Si vous connaissez les méthodes regex les plus élémentaires, vous vous rendrez compte qu'elles apparaissent toutes dans ce tableau. Mais il y a une distinction :vous n'avez pas à définir le modèle comme un argument. Par exemple, la méthode regex re.search(pattern, string) compilera en interne un objet regex p puis appelez le p.search(string) .

Vous pouvez voir ce fait dans l'implémentation officielle du re.search(pattern, string) méthode :

def search(pattern, string, flags=0):
    """Scan through string looking for a match to the pattern, returning
    a Match object, or None if no match was found."""
    return _compile(pattern, flags).search(string)

(Source :référentiel GitHub du package re)

Le re.search(pattern, string ) est un simple wrapper pour compiler le modèle en premier et appeler le p.search(string) fonction sur l'objet regex compilé p .

Voulez-vous maîtriser la superpuissance des regex ? Découvrez mon nouveau livre La façon la plus intelligente d'apprendre les expressions régulières en Python avec l'approche innovante en 3 étapes pour un apprentissage actif :(1) étudiez un chapitre de livre, (2) résolvez un puzzle de code et (3) regardez une vidéo de chapitre éducatif.

Vaut-il la peine d'utiliser la méthode re.compile() de Python ?

Non, dans la grande majorité des cas, cela ne vaut pas la ligne supplémentaire.

Prenons l'exemple suivant :

import re

# These two lines ...
regex = re.compile('Py...n')
match = regex.search('Python is great')

# ... are equivalent to ...
match = re.search('Py...n', 'Python is great')

Ne vous méprenez pas. Compiler un modèle une fois et l'utiliser plusieurs fois dans votre code (par exemple, dans une boucle) offre un grand avantage en termes de performances. Dans certains cas anecdotiques, la compilation du modèle a d'abord entraîné une accélération de 10 à 50 fois par rapport à la compilation répétée.

Mais la raison pour laquelle cela ne vaut pas la ligne supplémentaire est que la bibliothèque re de Python est livrée avec un cache interne. Au moment d'écrire ces lignes, le cache a une limite de 512 objets regex compilés. Ainsi, pour les 512 premières fois, vous pouvez être sûr lorsque vous appelez le re.search(pattern, string) que le cache contient déjà le modèle compilé.

Voici l'extrait de code pertinent du référentiel GitHub de re :

# – ------------------------------------------------------------------
# internals

_cache = {}  # ordered!

_MAXCACHE = 512
def _compile(pattern, flags):
    # internal: compile pattern
    if isinstance(flags, RegexFlag):
        flags = flags.value
    try:
        return _cache[type(pattern), pattern, flags]
    except KeyError:
        pass
    if isinstance(pattern, Pattern):
        if flags:
            raise ValueError(
                "cannot process flags argument with a compiled pattern")
        return pattern
    if not sre_compile.isstring(pattern):
        raise TypeError("first argument must be string or compiled pattern")
    p = sre_compile.compile(pattern, flags)
    if not (flags & DEBUG):
        if len(_cache) >= _MAXCACHE:
            # Drop the oldest item
            try:
                del _cache[next(iter(_cache))]
            except (StopIteration, RuntimeError, KeyError):
                pass
        _cache[type(pattern), pattern, flags] = p
    return p

Pouvez-vous trouver les endroits où le cache est initialisé et utilisé ?

Alors que dans la plupart des cas, vous n'avez pas besoin de compiler un modèle, dans certains cas, vous devriez le faire. Celles-ci découlent directement de l'implémentation précédente :

  • Vous avez plus de MAXCACHE modèles dans votre code.
  • Vous avez plus de MAXCACHE différent modèles entre deux même instances de modèle. Seulement dans ce cas, vous verrez des "échecs de cache" où le cache a déjà vidé les instances de modèle apparemment obsolètes pour faire de la place pour les plus récentes.
  • Vous réutilisez le modèle plusieurs fois. Parce que si vous ne le faites pas, cela n'aura aucun sens d'utiliser la mémoire sparse pour les enregistrer dans votre mémoire.
  • (Même dans ce cas, cela ne peut être utile que si les modèles sont relativement compliqués. Sinon, vous ne constaterez pas beaucoup d'avantages en termes de performances dans la pratique.)

Pour résumer, compiler d'abord le modèle et stocker le modèle compilé dans une variable pour une utilisation ultérieure n'est souvent rien d'autre qu'une "optimisation prématurée" - l'un des péchés capitaux des programmeurs débutants et intermédiaires.

Que fait vraiment re.compile() ?

Cela ne semble pas beaucoup, n'est-ce pas? Mon intuition était que le vrai travail consiste à trouver le modèle dans le texte, ce qui se produit après la compilation. Et, bien sûr, faire correspondre le modèle est la partie difficile. Mais une compilation sensée aide beaucoup à préparer le modèle à faire correspondre efficacement par le moteur de regex - un travail qui aurait autrement été effectué par le moteur de regex.

compile() de Regex la méthode fait beaucoup de choses telles que :

  • Combinez deux caractères suivants dans l'expression régulière s'ils indiquent ensemble un symbole spécial tel que certains symboles grecs.
  • Préparez l'expression régulière pour ignorer les majuscules et les minuscules.
  • Recherchez certains modèles (plus petits) dans l'expression régulière.
  • Analysez les groupes correspondants dans l'expression régulière entre parenthèses.

Voici l'implémentation du compile() méthode—ça a l'air plus compliqué que prévu, non ?

def _compile(code, pattern, flags):
    # internal: compile a (sub)pattern
    emit = code.append
    _len = len
    LITERAL_CODES = _LITERAL_CODES
    REPEATING_CODES = _REPEATING_CODES
    SUCCESS_CODES = _SUCCESS_CODES
    ASSERT_CODES = _ASSERT_CODES
    iscased = None
    tolower = None
    fixes = None
    if flags & SRE_FLAG_IGNORECASE and not flags & SRE_FLAG_LOCALE:
        if flags & SRE_FLAG_UNICODE:
            iscased = _sre.unicode_iscased
            tolower = _sre.unicode_tolower
            fixes = _ignorecase_fixes
        else:
            iscased = _sre.ascii_iscased
            tolower = _sre.ascii_tolower
    for op, av in pattern:
        if op in LITERAL_CODES:
            if not flags & SRE_FLAG_IGNORECASE:
                emit(op)
                emit(av)
            elif flags & SRE_FLAG_LOCALE:
                emit(OP_LOCALE_IGNORE[op])
                emit(av)
            elif not iscased(av):
                emit(op)
                emit(av)
            else:
                lo = tolower(av)
                if not fixes:  # ascii
                    emit(OP_IGNORE[op])
                    emit(lo)
                elif lo not in fixes:
                    emit(OP_UNICODE_IGNORE[op])
                    emit(lo)
                else:
                    emit(IN_UNI_IGNORE)
                    skip = _len(code); emit(0)
                    if op is NOT_LITERAL:
                        emit(NEGATE)
                    for k in (lo,) + fixes[lo]:
                        emit(LITERAL)
                        emit(k)
                    emit(FAILURE)
                    code[skip] = _len(code) - skip
        elif op is IN:
            charset, hascased = _optimize_charset(av, iscased, tolower, fixes)
            if flags & SRE_FLAG_IGNORECASE and flags & SRE_FLAG_LOCALE:
                emit(IN_LOC_IGNORE)
            elif not hascased:
                emit(IN)
            elif not fixes:  # ascii
                emit(IN_IGNORE)
            else:
                emit(IN_UNI_IGNORE)
            skip = _len(code); emit(0)
            _compile_charset(charset, flags, code)
            code[skip] = _len(code) - skip
        elif op is ANY:
            if flags & SRE_FLAG_DOTALL:
                emit(ANY_ALL)
            else:
                emit(ANY)
        elif op in REPEATING_CODES:
            if flags & SRE_FLAG_TEMPLATE:
                raise error("internal: unsupported template operator %r" % (op,))
            if _simple(av[2]):
                if op is MAX_REPEAT:
                    emit(REPEAT_ONE)
                else:
                    emit(MIN_REPEAT_ONE)
                skip = _len(code); emit(0)
                emit(av[0])
                emit(av[1])
                _compile(code, av[2], flags)
                emit(SUCCESS)
                code[skip] = _len(code) - skip
            else:
                emit(REPEAT)
                skip = _len(code); emit(0)
                emit(av[0])
                emit(av[1])
                _compile(code, av[2], flags)
                code[skip] = _len(code) - skip
                if op is MAX_REPEAT:
                    emit(MAX_UNTIL)
                else:
                    emit(MIN_UNTIL)
        elif op is SUBPATTERN:
            group, add_flags, del_flags, p = av
            if group:
                emit(MARK)
                emit((group-1)*2)
            # _compile_info(code, p, _combine_flags(flags, add_flags, del_flags))
            _compile(code, p, _combine_flags(flags, add_flags, del_flags))
            if group:
                emit(MARK)
                emit((group-1)*2+1)
        elif op in SUCCESS_CODES:
            emit(op)
        elif op in ASSERT_CODES:
            emit(op)
            skip = _len(code); emit(0)
            if av[0] >= 0:
                emit(0) # look ahead
            else:
                lo, hi = av[1].getwidth()
                if lo != hi:
                    raise error("look-behind requires fixed-width pattern")
                emit(lo) # look behind
            _compile(code, av[1], flags)
            emit(SUCCESS)
            code[skip] = _len(code) - skip
        elif op is CALL:
            emit(op)
            skip = _len(code); emit(0)
            _compile(code, av, flags)
            emit(SUCCESS)
            code[skip] = _len(code) - skip
        elif op is AT:
            emit(op)
            if flags & SRE_FLAG_MULTILINE:
                av = AT_MULTILINE.get(av, av)
            if flags & SRE_FLAG_LOCALE:
                av = AT_LOCALE.get(av, av)
            elif flags & SRE_FLAG_UNICODE:
                av = AT_UNICODE.get(av, av)
            emit(av)
        elif op is BRANCH:
            emit(op)
            tail = []
            tailappend = tail.append
            for av in av[1]:
                skip = _len(code); emit(0)
                # _compile_info(code, av, flags)
                _compile(code, av, flags)
                emit(JUMP)
                tailappend(_len(code)); emit(0)
                code[skip] = _len(code) - skip
            emit(FAILURE) # end of branch
            for tail in tail:
                code[tail] = _len(code) - tail
        elif op is CATEGORY:
            emit(op)
            if flags & SRE_FLAG_LOCALE:
                av = CH_LOCALE[av]
            elif flags & SRE_FLAG_UNICODE:
                av = CH_UNICODE[av]
            emit(av)
        elif op is GROUPREF:
            if not flags & SRE_FLAG_IGNORECASE:
                emit(op)
            elif flags & SRE_FLAG_LOCALE:
                emit(GROUPREF_LOC_IGNORE)
            elif not fixes:  # ascii
                emit(GROUPREF_IGNORE)
            else:
                emit(GROUPREF_UNI_IGNORE)
            emit(av-1)
        elif op is GROUPREF_EXISTS:
            emit(op)
            emit(av[0]-1)
            skipyes = _len(code); emit(0)
            _compile(code, av[1], flags)
            if av[2]:
                emit(JUMP)
                skipno = _len(code); emit(0)
                code[skipyes] = _len(code) - skipyes + 1
                _compile(code, av[2], flags)
                code[skipno] = _len(code) - skipno
            else:
                code[skipyes] = _len(code) - skipyes + 1
        else:
            raise error("internal: unsupported operand type %r" % (op,))

Pas besoin de tout comprendre dans ce code. Notez simplement que tout ce travail devrait être effectué par le moteur regex au « moment de l'exécution correspondant » si vous ne compilez pas le modèle en premier. Si nous ne pouvons le faire qu'une seule fois, c'est certainement un fruit à portée de main pour l'optimisation des performances, en particulier pour les longs modèles d'expressions régulières.

Comment utiliser l'argument de drapeau facultatif ?

Comme vous l'avez vu dans la spécification, le compile() la méthode est fournie avec un troisième flags facultatif argument :

re.compile(pattern, flags=0)

À quoi sert l'argument flags ?

Les drapeaux vous permettent de contrôler le moteur d'expressions régulières. Parce que les expressions régulières sont si puissantes, elles sont un moyen utile d'activer et de désactiver certaines fonctionnalités (par exemple, s'il faut ignorer les majuscules lors de la correspondance avec votre regex).

Syntaxe Signification
re.ASCII Si vous n'utilisez pas cet indicateur, les symboles spéciaux Python regex \w, \W, \b, \B, \d, \D, \s et \S correspondront aux caractères Unicode. Si vous utilisez cet indicateur, ces symboles spéciaux ne correspondront qu'aux caractères ASCII, comme leur nom l'indique.
re.A Identique à re.ASCII
re.DEBUG Si vous utilisez cet indicateur, Python affichera des informations utiles sur le shell qui vous aideront à déboguer votre regex.
re.IGNORECASE Si vous utilisez cet indicateur, le moteur regex effectuera une correspondance insensible à la casse. Donc, si vous recherchez [A-Z], cela correspondra également à [a-z].
re.I Identique à re.IGNORECASE
re.LOCALE N'utilisez jamais ce drapeau. Il est déprécié - l'idée était d'effectuer une correspondance insensible à la casse en fonction de vos paramètres régionaux actuels. Mais ce n'est pas fiable.
re.L Identique à re.LOCALE
re.MULTILINE Cet indicateur active la fonctionnalité suivante :l'expression régulière de début de chaîne « ^ » correspond au début de chaque ligne (plutôt qu'uniquement au début de la chaîne). Il en va de même pour la regex de fin de chaîne « $ » qui correspond désormais également à la fin de chaque ligne dans une chaîne multiligne.
re.M Identique à re.MULTILINE
re.DOTALL Sans utiliser cet indicateur, la regex point '.' correspond à tous les caractères sauf le caractère de nouvelle ligne 'n'. Activez ce drapeau pour vraiment faire correspondre tous les caractères, y compris le caractère de nouvelle ligne.
re.S Identique à re.DOTALL
re.VERBOSE Pour améliorer la lisibilité des expressions régulières compliquées, vous pouvez autoriser les commentaires et le formatage (multiligne) de la regex elle-même. C'est possible avec ce drapeau :tous les caractères blancs et les lignes qui commencent par le caractère '#' sont ignorés dans la regex.
re.X Identique à re.VERBOSE

Voici comment vous l'utiliseriez dans un exemple pratique :

import re

text = 'Python is great (python really is)'

regex = re.compile('Py...n', flags=re.IGNORECASE)

matches = regex.findall(text)
print(matches)
# ['Python', 'python']

Bien que votre regex 'Python' est en majuscule, nous ignorons la capitalisation en utilisant le drapeau re.IGNORECASE .


Post précédent