web-dev-qa-db-fra.com

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

J'utilise Python 3.5.2

J'ai deux listes

  • une liste d'environ 750 000 "phrases" (chaînes longues)
  • une liste d'environ 20 000 "mots" que je voudrais supprimer de mes 750 000 phrases

Donc, je dois parcourir 750 000 phrases et effectuer environ 20 000 remplacements, NIQUEMENT si mes mots sont réellement des "mots" et ne font pas partie d'une plus grande chaîne de caractères.

Je le fais en pré-compilant mes mots pour qu’ils soient flanqués du métacaractère \b

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

Puis je boucle mes "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 traite environ 50 phrases par seconde, ce qui est Nice, mais le traitement de toutes mes phrases prend encore plusieurs heures.

  • Existe-t-il un moyen d’utiliser la méthode str.replace (qui, à mon avis, est plus rapide), tout en exigeant que les remplacements ne se produisent qu’à limites de mots?

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

Merci pour vos suggestions.

112
pdanese

Une chose que vous pouvez essayer est de compiler un seul modèle comme "\b(Word1|Word2|Word3)\b".

Étant donné que re repose 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, Eric answer est plus rapide.

109
Liteye

TLDR

Utilisez cette méthode (avec set lookup) si vous voulez la solution la plus rapide. Pour un jeu de données similaire aux PO, il est environ 2000 fois plus rapide que la réponse acceptée.

Si vous insistez pour utiliser une expression rationnelle pour la recherche, utilisez cette version basée sur trie , qui est toujours 1000 fois plus rapide qu'une union regex.

Théorie

Si vos phrases ne sont pas énormes, il est probablement possible d'en traiter plus de 50 par seconde.

Si vous enregistrez tous les mots interdits dans un ensemble, il sera très rapide de vérifier si un autre mot est inclus dans cet ensemble.

Compressez la logique dans une fonction, donnez cette fonction comme argument à 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)

Les phrases converties sont:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Notez que:

  • la recherche est insensible à la casse (grâce à lower())
  • le remplacement d'un mot par "" peut laisser deux espaces (comme dans votre code)
  • Avec python3, \w+ correspond également aux caractères accentués (par exemple "ångström").
  • Tout caractère autre que Word (tabulation, espace, nouvelle ligne, marques, ...) reste inchangé.

Performance

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

En comparaison, la réponse de Liteye avait besoin de 160 secondes pour 10 000 phrases.

Avec n étant le nombre total de mots et m le nombre de mots interdits, les codes OP et Liteye sont O(n*m).

En comparaison, mon code devrait être exécuté dans O(n+m). Considérant qu'il y a beaucoup plus de phrases que de mots interdits, l'algorithme devient O(n).

Test de l'union regex

Quelle est la complexité d'une recherche regex avec un motif '\b(Word1|Word2|...|wordN)\b'? Est-ce que O(N) ou O(1)?

Il est assez difficile de comprendre le fonctionnement du moteur de regex, écrivons donc un test simple.

Ce code extrait 10**i _ mots anglais aléatoires dans une liste. Il crée l'union regex correspondante et le teste avec différents mots:

  • on n'est clairement pas un mot (ça commence par #)
  • l'un est le premier mot de la liste
  • l'un est le dernier mot de la liste
  • on ressemble à un mot mais n'est 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))

Il produit:

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

Il semble donc que la recherche d'un seul mot avec un modèle '\b(Word1|Word2|...|wordN)\b' a:

  • O(1) meilleur des cas
  • O(n/2) cas moyen, qui est toujours O(n)
  • O(n) pire des cas

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

Une alternative beaucoup plus rapide à une union de regex consiste à créer le motif de regex à partir d'un trie .

105
Eric Duminil

TLDR

Utilisez cette méthode si vous voulez la solution la plus rapide basée sur regex. Pour un jeu de données similaire aux PO, il est environ 1000 fois plus rapide que la réponse acceptée.

Si vous ne vous souciez pas de regex, utilisez cette version basée sur un ensemble , qui est 2000 fois plus rapide qu'une union de regex.

Regex optimisé avec Trie

Une approche nion simple de Regex devient lente avec beaucoup de mots interdits, car le moteur de regex ne fait pas un très bon travail d'optimiser le motif.

Il est possible de créer un Trie avec tous les mots interdits et d'écrire l'expression régulière correspondante. Les fichiers trie ou regex résultants ne sont pas vraiment lisibles par l'homme, mais ils permettent une recherche et une correspondance très rapides.

Exemple

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

Regex union

La liste est convertie en trie:

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

Et puis à ce motif de regex:

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

Regex trie

L’énorme avantage est que pour tester si Zoo correspond, le moteur de regex uniquement doit comparer le premier caractère (il ne correspond pas), au lieu de essayer les 5 mots . C'est un prétraitement excessif pour 5 mots, mais il montre des résultats prometteurs pour plusieurs milliers de mots.

Notez que (?:) groupes non capturés sont utilisés pour les raisons suivantes:

Code

Voici un Gist légèrement modifié, que nous pouvons utiliser comme bibliothèque trie.py:

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

Tester

Voici un petit test (identique à celui-ci ):

# 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))

Il produit:

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 (?: (?: | | | (|: | chen | liyah (?:\'s)? | r:?: dvark (?: (?: |: | s | ))? | on)) | b (?:\| s (?: c (?: us (?: (?: | | | |))? | [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 (?:\'s)? | s))? | reviat (?: e [ds]? | i (?: ng | on (?: (?: (|: | | |))?)) | y (?: \' s)? |\é (?: (?:\'s | s))?) | d (?: icat (?: e [ds]? | i (?: ng | on (?: (?:\(s))?)) | om (?: en (?: (?: (|: |))? | inal) | u (?: ct (? :(? :( ?: ed | i (?: ng | on (?: (?: (s: | s | s))?) | ou (?: (?:\s | s))? | s))? | l (?:\'s)?) ) | e (?: (?:\| s | am | l (?: (?: (|: | ard | fils (?:\'s)?)))? | r (?: deen (?:\? | nathy (?:\'s)? | ra (?: nt | tion (?: (?: (|: | s | s))?)) | t (? :(? :( ?: t (?: e (?: r (?: (?: (\: | s | s))? | d) | ing | ou (?: (?: (|: | s | s))?) | s))? | yance (?) :\'s)? | d))? | hor (? :(??: r (?: e (?: n (?: ce (?: \' s)? | t) | d) | 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 (?: (?: | | |))? | ze) | e (? :( ?:(: st | r))? | oom | ution (? :(? :\'s | s))? | y) | m\'s | n (?: e (?: gat (?: e [ds]? | i (?: ng | on (?: \' s)?))) | r (?:\' s)?) | ormal (? :( ?: il (?: 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 | s))) | | e (?: (?: (?: | s | s))?) | t (? :(?? : ed | i (?: ng | on (?: (?: (|: (|: (?: | |))? | s))? | s) | 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))? | ve (?: (?: (|: | Ly | Ness (?:\'s)? | s))?)) | east | idg (?: e (? :( ?: mentement (?: (?: (s: s | s))? | [ds]))? | ing | ment (?: (?: (?: | s | s)))? ) | o (?: ad | gat (?: e [ds]? | i (?: ng | on (?: (?: (|: | | | | s))?))) | | upt (? :(?? e (?: st | r) | ly | ness (?:\'s??))?) | s (?: alom | c (?: ess (?: (?:\| s | e [ds] | ing))? | issa (?: (?: (?: | | s |)))? | ond (? :( ?:ed | ing | s))?) | en (?: ce (? :(: ?:\'s | s))? | t (? :(?: e (?: e (?: (?: \: | | s | is (?: \' s)? | s)? | s))? | d) | ing | ly | s))) | inth (?: (?:\'s | e (?: \' s)?)) | | o (?: l (?: ut (?: e (?: : (?:\'s | ly | st?))? | i (?: sur (?: \' s)? | sm (?:\'s)?)) | v (?: e [ds] ? | ing)) | r (?: b (? :(?: e:?: n (?: cy (?:\s)? | t (?: (?:\s | s)))? ) | d) | ing | s))? | pt je...

C'est vraiment illisible, mais pour une liste de 100 000 mots interdits, cette expression rationnelle Trie est 1000 fois plus rapide qu'une simple union regex!

Voici un diagramme du tri complet, exporté avec trie-python-graphviz et graphviz twopi :

Enter image description here

88
Eric Duminil

Vous voudrez peut-être essayer de prétraiter les phrases pour coder les limites de Word. Fondamentalement, transformez chaque phrase en une liste de mots en séparant les limites des mots.

Cela devrait être plus rapide, car pour traiter une phrase, il vous suffit de parcourir chacun des mots et de vérifier s'il s'agit d'une correspondance.

Actuellement, la recherche sur les expressions rationnelles doit parcourir à nouveau la chaîne entière à chaque fois, en recherchant les limites de Word, puis en "annulant" le résultat de ce travail avant le prochain passage.

12
Denziloe

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

Stratégie gagnante:

re.sub ("\ w +", repl, phrase) recherche des mots.

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

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

Deuxième meilleur

L'idée est de scinder les phrases en mots, en utilisant re.split, tout en conservant les séparateurs pour reconstruire les phrases plus tard. Ensuite, les remplacements se font avec une simple recherche de dict.

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

Timings par exemple fonctions:

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 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 )
8
peufeu

Peut-être que Python n'est pas le bon outil ici. En voici un avec la chaine Unix

sed G file         |
tr ' ' '\n'        |
grep -vf blacklist |
awk -v RS= -v OFS=' ' '{$1=$1}1'

en supposant que votre fichier de liste noire est prétraité avec les limites de Word ajoutées. Les étapes sont les suivantes: convertir le fichier en double interligne, diviser chaque phrase en un mot par ligne, supprimer en masse les mots de la liste noire du fichier et fusionner les lignes.

Cela devrait fonctionner au moins un ordre de grandeur plus rapidement.

Pour prétraiter le fichier de liste noire à partir de mots (un mot par ligne)

sed 's/.*/\\b&\\b/' words > blacklist
6
karakfa

Que dis-tu de ça:

#!/usr/bin/env python3

from __future__ import unicode_literals, print_function
import re
import time
import io

def replace_sentences_1(sentences, banned_words):
    # faster on CPython, but does not use \b as the Word separator
    # so result is slightly different than replace_sentences_2()
    def filter_sentence(sentence):
        words = Word_SPLITTER.split(sentence)
        words_iter = iter(words)
        for Word in words_iter:
            norm_Word = Word.lower()
            if norm_Word not in banned_words:
                yield Word
            yield next(words_iter) # yield the Word separator

    Word_SPLITTER = re.compile(r'(\W+)')
    banned_words = set(banned_words)
    for sentence in sentences:
        yield ''.join(filter_sentence(sentence))


def replace_sentences_2(sentences, banned_words):
    # slower on CPython, uses \b as separator
    def filter_sentence(sentence):
        boundaries = Word_BOUNDARY.finditer(sentence)
        current_boundary = 0
        while True:
            last_Word_boundary, current_boundary = current_boundary, next(boundaries).start()
            yield sentence[last_Word_boundary:current_boundary] # yield the separators
            last_Word_boundary, current_boundary = current_boundary, next(boundaries).start()
            Word = sentence[last_Word_boundary:current_boundary]
            norm_Word = Word.lower()
            if norm_Word not in banned_words:
                yield Word

    Word_BOUNDARY = re.compile(r'\b')
    banned_words = set(banned_words)
    for sentence in sentences:
        yield ''.join(filter_sentence(sentence))


corpus = io.open('corpus2.txt').read()
banned_words = [l.lower() for l in open('banned_words.txt').read().splitlines()]
sentences = corpus.split('. ')
output = io.open('output.txt', 'wb')
print('number of sentences:', len(sentences))
start = time.time()
for sentence in replace_sentences_1(sentences, banned_words):
    output.write(sentence.encode('utf-8'))
    output.write(b' .')
print('time:', time.time() - start)

Ces solutions se divisent en limites de mots et recherchent chaque mot dans un ensemble. Elles devraient être plus rapides que re.sub de substituts Word (solution de Liteyes) car ces solutions sont O(n) où n est la taille de l'entrée due à la recherche amortized O(1), alors que l'utilisation d'alternatives regex provoquerait le moteur de regex doit vérifier les correspondances de mots sur tous les caractères plutôt que sur les limites de mots. Ma solution prend un soin particulier pour préserver les espaces blancs utilisés dans le texte original (c’est-à-dire qu’ils ne compressent pas les espaces blancs et ne conservent pas les onglets, les nouvelles lignes et d’autres caractères d’espace blanc), mais si vous décidez que vous n’y accordez aucune importance, devrait être assez simple pour les supprimer de la sortie.

J'ai testé sur corpus.txt, qui est une concaténation de plusieurs livres numériques téléchargés à partir du projet Gutenberg, et banned_words.txt, 20000 mots choisis au hasard dans la liste de mots d'Ubuntu (/ usr/share/dict/american-english). Il faut environ 30 secondes pour traiter 862462 phrases (et la moitié de celle de PyPy). J'ai défini les phrases comme n'importe quoi séparé par ".".

$ # replace_sentences_1()
$ python3 filter_words.py 
number of sentences: 862462
time: 24.46173644065857
$ pypy filter_words.py 
number of sentences: 862462
time: 15.9370770454

$ # replace_sentences_2()
$ python3 filter_words.py 
number of sentences: 862462
time: 40.2742919921875
$ pypy filter_words.py 
number of sentences: 862462
time: 13.1190629005

PyPy bénéficie plus particulièrement de la seconde approche, tandis que CPython s'en tire mieux avec la première approche. Le code ci-dessus devrait fonctionner à la fois sur Python 2 et 3.

4
Lie Ryan

Approche pratique

ne solution décrite ci-dessous utilise beaucoup de mémoire pour stocker tout le texte dans la même chaîne et pour réduire le niveau de complexité. Si RAM est un problème, réfléchissez-y à deux fois avant de l'utiliser.

Avec les astuces join/split, vous pouvez éviter les boucles qui accéléreraient l’algorithme.

  • Concaténer une phrase avec un séparateur spécial qui n’est pas contenu dans la phrase:
  • merged_sentences = ' * '.join(sentences)
    
  • Compilez une seule expression rationnelle pour tous les mots dont vous avez besoin pour éliminer les phrases à l'aide de | "ou" instruction regex:
  • regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag
    
  • Indiquez les mots avec l'expression rationnelle compilée et scindez-les en phrases séparées à l'aide du caractère de délimitation spécial:
  • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')
    

    Performance

    "".join la complexité est O (n). C'est assez intuitif, mais de toute façon, il y a une citation abrégée d'une source:

    for (i = 0; i < seqlen; i++) {
        [...]
        sz += PyUnicode_GET_LENGTH(item);
    

    Donc avec join/split vous avez O(words) + 2 * O (phrases) qui est toujours complexité linéaire vs 2 * O (N2) avec l'approche initiale.


    btw n'utilise pas le multithreading. GIL bloque chaque opération car votre tâche est strictement liée au processeur, donc GIL n'a aucune chance de se libérer mais chaque thread envoie simultanément des ticks, ce qui entraîne un effort supplémentaire et même une opération à l'infini.

    3
    I159

    Concaténer toutes vos phrases en un seul document. Utilisez n’importe quelle implémentation de l’algorithme Aho-Corasick ( en voici un ) pour localiser tous vos "mauvais" mots. Parcourez le fichier, en remplaçant chaque mauvais mot, en mettant à jour les décalages des mots trouvés qui suivent, etc.

    0
    Edi Bice