192 votes

Accélérez le remplacement de millions de regex dans Python 3

Je suis à l'aide de Python 3.5.2

J'ai deux listes

  • une liste d'environ 750 000 "phrases" (chaînes longues)
  • une liste de près de 20.000 "mots" que je voudrais effacer de ma de 750 000 phrases

Donc, je dois faire une boucle par de 750 000 phrases et d'effectuer environ 20 000 remplacements, mais SEULEMENT si mes mots sont en fait des "mots" et ne font pas partie d'une grande chaîne de caractères.

Je fais cela par la pré-compilation de mes mots, de sorte qu'ils sont flanqués par l' \b métacaractère

compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]

Ensuite je boucle mon "phrases"

import re

for sentence in sentences:
  for word in compiled_words:
    sentence = re.sub(word, "", sentence)
  # put sentence into a growing list

Cette boucle imbriquée est le traitement d'environ 50 phrases par seconde, ce qui est agréable, mais il faut encore plusieurs heures de traiter l'ensemble de mes phrases.

  • Est-il possible à l'aide de l' str.replace méthode (qui je crois est le plus rapide), mais toujours exigeant que les remplacements ne se produisent dans les limites de mot?

  • Sinon, est-il un moyen d'accélérer l' re.sub méthode? J'ai déjà amélioré la vitesse légèrement en sautant sur re.sub si la longueur de ma parole est > que la longueur de ma phrase, mais ce n'est pas une amélioration.

Merci pour toutes les suggestions.

191voto

Eric Duminil Points 38857

TLDR

Utilisez cette méthode si vous voulez le plus rapide regex à base de solution. Pour un jeu de données similaires à l'OP, c'est environ 1000 fois plus rapide que l'on a accepté la réponse.

Si vous n'avez pas de soins sur les regex, utilisez cette version, qui est 2000 fois plus rapide qu'un regex de l'union.

Optimisé Regex avec Trie

Une simple Regex union approche devient lent avec de nombreuses interdit les mots, parce que le moteur d'expressions régulières ne pas faire un très bon travail d'optimisation du modèle.

Il est possible de créer un Trie avec tous les interdits de mots et d'écrire les regex. Le résultant trie ou regex ne sont pas vraiment lisible par l'homme, mais ils ne permettent très vite de recherche et de correspondance.

Exemple

['foobar', 'foobah', 'fooxar', 'foozap', 'fooza']

Regex union

La liste est convertie à un trie:

{'f': {'o': {'o': {'x': {'a': {'r': {'': 1}}}, 'b': {'a': {'r': {'': 1}, 'h': {'': 1}}}, 'z': {'a': {'': 1, 'p': {'': 1}}}}}}}

Et alors, à ce pattern regex:

r"\bfoo(?:ba[hr]|xar|zap?)\b"

Regex trie

L'énorme avantage est que pour tester si zoo correspond à, le moteur d'expressions régulières seulement besoin de comparer le premier caractère (il ne correspond pas), au lieu de tenter les 5 mots. C'est un prétraitement de trop pour le 5 mots, mais il montre des résultats prometteurs pour plusieurs milliers de mots.

Notez que (?:) non-capture de groupes sont utilisés parce que:

Code

Voici un légèrement modifiée, pour l'essentiel, que l'on peut utiliser en tant que trie.py bibliothèque:

import re


class Trie():
    """Regex::Trie in Python. Creates a Trie out of a list of words. The trie can be exported to a Regex pattern.
    The corresponding Regex should match much faster than a simple Regex union."""

    def __init__(self):
        self.data = {}

    def add(self, word):
        ref = self.data
        for char in word:
            ref[char] = char in ref and ref[char] or {}
            ref = ref[char]
        ref[''] = 1

    def dump(self):
        return self.data

    def quote(self, char):
        return re.escape(char)

    def _pattern(self, pData):
        data = pData
        if "" in data and len(data.keys()) == 1:
            return None

        alt = []
        cc = []
        q = 0
        for char in sorted(data.keys()):
            if isinstance(data[char], dict):
                try:
                    recurse = self._pattern(data[char])
                    alt.append(self.quote(char) + recurse)
                except:
                    cc.append(self.quote(char))
            else:
                q = 1
        cconly = not len(alt) > 0

        if len(cc) > 0:
            if len(cc) == 1:
                alt.append(cc[0])
            else:
                alt.append('[' + ''.join(cc) + ']')

        if len(alt) == 1:
            result = alt[0]
        else:
            result = "(?:" + "|".join(alt) + ")"

        if q:
            if cconly:
                result += "?"
            else:
                result = "(?:%s)?" % result
        return result

    def pattern(self):
        return self._pattern(self.dump())

Test

Voici un petit test (le même que ce seul):

# Encoding: utf-8
import re
import timeit
import random
from trie import Trie

with open('/usr/share/dict/american-english') as wordbook:
    banned_words = [word.strip().lower() for word in wordbook]
    random.shuffle(banned_words)

test_words = [
    ("Surely not a word", "#surely_NöTäWORD_so_regex_engine_can_return_fast"),
    ("First word", banned_words[0]),
    ("Last word", banned_words[-1]),
    ("Almost a word", "couldbeaword")
]

def trie_regex_from_words(words):
    trie = Trie()
    for word in words:
        trie.add(word)
    return re.compile(r"\b" + trie.pattern() + r"\b", re.IGNORECASE)

def find(word):
    def fun():
        return union.match(word)
    return fun

for exp in range(1, 6):
    print("\nTrieRegex of %d words" % 10**exp)
    union = trie_regex_from_words(banned_words[:10**exp])
    for description, test_word in test_words:
        time = timeit.timeit(find(test_word), number=1000) * 1000
        print("  %s : %.1fms" % (description, time))

C'sorties:

TrieRegex of 10 words
  Surely not a word : 0.3ms
  First word : 0.4ms
  Last word : 0.5ms
  Almost a word : 0.5ms

TrieRegex of 100 words
  Surely not a word : 0.3ms
  First word : 0.5ms
  Last word : 0.9ms
  Almost a word : 0.6ms

TrieRegex of 1000 words
  Surely not a word : 0.3ms
  First word : 0.7ms
  Last word : 0.9ms
  Almost a word : 1.1ms

TrieRegex of 10000 words
  Surely not a word : 0.1ms
  First word : 1.0ms
  Last word : 1.2ms
  Almost a word : 1.2ms

TrieRegex of 100000 words
  Surely not a word : 0.3ms
  First word : 1.2ms
  Last word : 0.9ms
  Almost a word : 1.6ms

Pour info, la regex commence comme ceci:

(?:a(?:(?:\'s|a(?:\'s|chen|liyah(?:\'s)?|r(?:dvark(?:(?:\'s|s))?|on))|b(?:\'s|a(?:c(?:us(?:(?:\'s|es))?|[ik])|ft|lone(?:(?:\'s|s))?|ndon(?:(?:ed|ing|ment(?:\'s)?|s))?|s(?:e(?:(?:ment(?:\'s)?|[ds]))?|h(?:(?:e[ds]|ing))?|ing)|t(?:e(?:(?:ment(?:\'s)?|[ds]))?|ing|toir(?:(?:\'s|s))?))|b(?:as(?:id)?|e(?:ss(?:(?:\'s|es))?|y(?:(?:\'s|s))?)|ot(?:(?:\'s|t(?:\'s)?|s))?|reviat(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?))|y(?:\'s)?|\é(?:(?:\'s|s))?)|d(?:icat(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?))|om(?:en(?:(?:\'s|s))?|inal)|u(?:ct(?:(?:ed|i(?:ng|on(?:(?:\'s|s))?)|or(?:(?:\'s|s))?|s))?|l(?:\'s)?))|e(?:(?:\'s|am|l(?:(?:\'s|ard|son(?:\'s)?))?|r(?:deen(?:\'s)?|nathy(?:\'s)?|ra(?:nt|tion(?:(?:\'s|s))?))|t(?:(?:t(?:e(?:r(?:(?:\'s|s))?|d)|ing|or(?:(?:\'s|s))?)|s))?|yance(?:\'s)?|d))?|hor(?:(?:r(?:e(?:n(?:ce(?:\'s)?|t)|d)|ing)|s))?|i(?:d(?:e[ds]?|ing|jan(?:\'s)?)|gail|l(?:ene|it(?:ies|y(?:\'s)?)))|j(?:ect(?:ly)?|ur(?:ation(?:(?:\'s|s))?|e[ds]?|ing))|l(?:a(?:tive(?:(?:\'s|s))?|ze)|e(?:(?:st|r))?|oom|ution(?:(?:\'s|s))?|y)|m\'s|n(?:e(?:gat(?:e[ds]?|i(?:ng|on(?:\'s)?))|r(?:\'s)?)|ormal(?:(?:it(?:ies|y(?:\'s)?)|ly))?)|o(?:ard|de(?:(?:\'s|s))?|li(?:sh(?:(?:e[ds]|ing))?|tion(?:(?:\'s|ist(?:(?:\'s|s))?))?)|mina(?:bl[ey]|t(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?)))|r(?:igin(?:al(?:(?:\'s|s))?|e(?:(?:\'s|s))?)|t(?:(?:ed|i(?:ng|on(?:(?:\'s|ist(?:(?:\'s|s))?|s))?|ve)|s))?)|u(?:nd(?:(?:ed|ing|s))?|t)|ve(?:(?:\'s|board))?)|r(?:a(?:cadabra(?:\'s)?|d(?:e[ds]?|ing)|ham(?:\'s)?|m(?:(?:\'s|s))?|si(?:on(?:(?:\'s|s))?|ve(?:(?:\'s|ly|ness(?:\'s)?|s))?))|east|idg(?:e(?:(?:ment(?:(?:\'s|s))?|[ds]))?|ing|ment(?:(?:\'s|s))?)|o(?:ad|gat(?:e[ds]?|i(?:ng|on(?:(?:\'s|s))?)))|upt(?:(?:e(?:st|r)|ly|ness(?:\'s)?))?)|s(?:alom|c(?:ess(?:(?:\'s|e[ds]|ing))?|issa(?:(?:\'s|[es]))?|ond(?:(?:ed|ing|s))?)|en(?:ce(?:(?:\'s|s))?|t(?:(?:e(?:e(?:(?:\'s|ism(?:\'s)?|s))?|d)|ing|ly|s))?)|inth(?:(?:\'s|e(?:\'s)?))?|o(?:l(?:ut(?:e(?:(?:\'s|ly|st?))?|i(?:on(?:\'s)?|sm(?:\'s)?))|v(?:e[ds]?|ing))|r(?:b(?:(?:e(?:n(?:cy(?:\'s)?|t(?:(?:\'s|s))?)|d)|ing|s))?|pti...

C'est vraiment illisible, mais pour une liste de 100000 interdit les mots, ce Trie regex est 1000 fois plus rapide qu'une simple regex union!

Voici un schéma complet de la trie, exporté à trie-python-graphviz et graphviz twopi:

Enter image description here

168voto

Eric Duminil Points 38857

TLDR

L'utilisation de cette méthode (avec recherche) si vous voulez la solution la plus rapide. Pour un jeu de données similaires à l'OP, c'est environ 2000 fois plus rapide que l'on a accepté la réponse.

Si vous insistez sur l'utilisation d'une expression régulière pour la recherche, l'utilisation de cette trie-en fonction de la version, qui est toujours 1000 fois plus rapide qu'un regex de l'union.

La théorie de l'

Si vos phrases ne sont pas faramineux des chaînes, il est probablement possible de traiter beaucoup plus de 50 par seconde.

Si vous enregistrez tous les interdits mots dans un ensemble, il va être très rapide pour vérifier si un mot est inclus dans cet ensemble.

Le Pack de la logique dans une fonction, de donner à cette fonction comme argument d' re.sub et vous avez terminé!

Code

import re
with open('/usr/share/dict/american-english') as wordbook:
    banned_words = set(word.strip().lower() for word in wordbook)


def delete_banned_words(matchobj):
    word = matchobj.group(0)
    if word.lower() in banned_words:
        return ""
    else:
        return word

sentences = ["I'm eric. Welcome here!", "Another boring sentence.",
             "GiraffeElephantBoat", "sfgsdg sdwerha aswertwe"] * 250000

word_pattern = re.compile('\w+')

for sentence in sentences:
    sentence = word_pattern.sub(delete_banned_words, sentence)

Converti phrases sont:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Notez que:

  • la recherche est insensible à la casse (grâce à l' lower())
  • le remplacement d'un mot avec "" pourrait laisser deux espaces (comme dans votre code)
  • Avec python3, \w+ correspond à des caractères accentués (par exemple, "ångström").
  • Tout non-caractère de mot (tab, espace, retour à la ligne, marques, ...) restera intacte.

Performance

Il y a un million de phrases, banned_words a près de 100000 mots et le script s'exécute en moins de 7s.

En comparaison, Liteye de réponse nécessaires 160s pour 10 mille phrases.

Avec n étant le total de la quantité de mots et d' m le montant de l'interdit des mots, des OP et des Liteye du code O(n*m).

En comparaison, mon code doit s'exécuter en O(n+m). Considérant qu'il ya beaucoup plus de phrases que d'interdire des mots, l'algorithme devient O(n).

Regex union test

Quelle est la complexité d'un regex de recherche avec un '\b(word1|word2|...|wordN)\b' modèle? Est-il O(N) ou O(1)?

Il est assez difficile de saisir la façon dont le moteur de regex fonctionne, nous allons donc écrire un test simple.

Ce code extrait 10**i aléatoire de mots anglais dans une liste. Il crée le correspondant de la regex de l'union, et des tests avec des mots différents :

  • l'un est clairement pas un mot (il commence par #)
  • l'un est le premier mot dans la liste
  • l'un est le dernier mot dans la liste
  • on ressemble à un mot, mais n'est-ce pas


import re
import timeit
import random

with open('/usr/share/dict/american-english') as wordbook:
    english_words = [word.strip().lower() for word in wordbook]
    random.shuffle(english_words)

print("First 10 words :")
print(english_words[:10])

test_words = [
    ("Surely not a word", "#surely_NöTäWORD_so_regex_engine_can_return_fast"),
    ("First word", english_words[0]),
    ("Last word", english_words[-1]),
    ("Almost a word", "couldbeaword")
]


def find(word):
    def fun():
        return union.match(word)
    return fun

for exp in range(1, 6):
    print("\nUnion of %d words" % 10**exp)
    union = re.compile(r"\b(%s)\b" % '|'.join(english_words[:10**exp]))
    for description, test_word in test_words:
        time = timeit.timeit(find(test_word), number=1000) * 1000
        print("  %-17s : %.1fms" % (description, time))

C'sorties:

First 10 words :
["geritol's", "sunstroke's", 'fib', 'fergus', 'charms', 'canning', 'supervisor', 'fallaciously', "heritage's", 'pastime']

Union of 10 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 0.7ms
  Almost a word     : 0.7ms

Union of 100 words
  Surely not a word : 0.7ms
  First word        : 1.1ms
  Last word         : 1.2ms
  Almost a word     : 1.2ms

Union of 1000 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 9.6ms
  Almost a word     : 10.1ms

Union of 10000 words
  Surely not a word : 1.4ms
  First word        : 1.8ms
  Last word         : 96.3ms
  Almost a word     : 116.6ms

Union of 100000 words
  Surely not a word : 0.7ms
  First word        : 0.8ms
  Last word         : 1227.1ms
  Almost a word     : 1404.1ms

Donc il semble que la recherche d'un mot avec un '\b(word1|word2|...|wordN)\b' modèle a:

  • O(1) dans le meilleur des cas
  • O(n/2) moyenne de cas, qui est encore O(n)
  • O(n) pire des cas

Ces résultats sont cohérents avec une simple boucle de recherche.

Beaucoup plus rapide, alternative à une regex de l'union est de créer de l' expression régulière pattern à partir d'un trie.

140voto

Liteye Points 2069

Une chose que vous pouvez essayer est de compiler un modèle unique comme "\b(word1|word2|word3)\b" .

Étant donné que re s'appuie sur le code C pour effectuer la correspondance réelle, les économies peuvent être considérables.

Comme @pvg l'a souligné dans les commentaires, il bénéficie également de la correspondance en un seul passage.

Si vos mots ne sont pas regex, la réponse d'Eric est plus rapide.

17voto

Denziloe Points 2374

Une chose que vous pourriez vouloir essayer est le pré-traitement, les peines pour coder les frontières de mot. Fondamentalement, faire de chaque phrase en une liste de mots par fractionnement sur les limites de word.

Cela devrait être rapide, car le traitement d'une phrase, il vous suffit de faire défiler les mots et vérifier si c'est un match.

Actuellement, la regex de recherche est d'avoir à passer par l'ensemble de la chaîne de nouveau à chaque fois, à la recherche de limites de mots, puis de "rejet" le résultat de ce travail avant le prochain passage.

14voto

peufeu Points 4758

Eh bien, voici une solution rapide et facile, avec l'ensemble de test.

Stratégie gagnante:

re.sub("\w+",repl,phrase) des recherches pour les mots.

"repl" peut être un appelable. J'ai utilisé une fonction qui effectue un dict de recherche, et le dict contient les mots à rechercher et remplacer.

C'est le plus simple et le plus rapide de la solution (voir la fonction replace4 dans l'exemple de code ci-dessous).

Deuxième meilleur

L'idée est de diviser les phrases en mots, à l'aide de ré.split, tout en conservant les séparateurs de reconstituer les phrases plus tard. Ensuite, les remplacements sont effectués avec un simple dict de recherche.

(voir la fonction replace3 dans l'exemple de code ci-dessous).

Les horaires pour exemple les fonctions de:

replace1: 0.62 sentences/s
replace2: 7.43 sentences/s
replace3: 48498.03 sentences/s
replace4: 61374.97 sentences/s (...and 240.000/s with PyPy)

...et le code:

#! /bin/env python3
# -*- coding: utf-8

import time, random, re

def replace1( sentences ):
    for n, sentence in enumerate( sentences ):
        for search, repl in patterns:
            sentence = re.sub( "\\b"+search+"\\b", repl, sentence )

def replace2( sentences ):
    for n, sentence in enumerate( sentences ):
        for search, repl in patterns_comp:
            sentence = re.sub( search, repl, sentence )

def replace3( sentences ):
    pd = patterns_dict.get
    for n, sentence in enumerate( sentences ):
        #~ print( n, sentence )
        # Split the sentence on non-word characters.
        # Note: () in split patterns ensure the non-word characters ARE kept
        # and returned in the result list, so we don't mangle the sentence.
        # If ALL separators are spaces, use string.split instead or something.
        # Example:
        #~ >>> re.split(r"([^\w]+)", "ab céé? . d2eéf")
        #~ ['ab', ' ', 'céé', '? . ', 'd2eéf']
        words = re.split(r"([^\w]+)", sentence)

        # and... done.
        sentence = "".join( pd(w,w) for w in words )

        #~ print( n, sentence )

def replace4( sentences ):
    pd = patterns_dict.get
    def repl(m):
        w = m.group()
        return pd(w,w)

    for n, sentence in enumerate( sentences ):
        sentence = re.sub(r"\w+", repl, sentence)



# Build test set
test_words = [ ("word%d" % _) for _ in range(50000) ]
test_sentences = [ " ".join( random.sample( test_words, 10 )) for _ in range(1000) ]

# Create search and replace patterns
patterns = [ (("word%d" % _), ("repl%d" % _)) for _ in range(20000) ]
patterns_dict = dict( patterns )
patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]


def test( func, num ):
    t = time.time()
    func( test_sentences[:num] )
    print( "%30s: %.02f sentences/s" % (func.__name__, num/(time.time()-t)))

print( "Sentences", len(test_sentences) )
print( "Words    ", len(test_words) )

test( replace1, 1 )
test( replace2, 10 )
test( replace3, 1000 )
test( replace4, 1000 )

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