J'utilise Python 3.5.2
J'ai deux listes
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.
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.
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.
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é!
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:
lower()
)""
peut laisser deux espaces (comme dans votre code)\w+
correspond également aux caractères accentués (par exemple "ångström"
).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)
.
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:
#
)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 casO(n/2)
cas moyen, qui est toujours O(n)
O(n)
pire des casCes 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 .
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.
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.
['foobar', 'foobah', 'fooxar', 'foozap', 'fooza']
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"
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:
foobar|baz
correspondrait à foobar
ou baz
, mais pas foobaz
foo(bar|baz)
enregistrerait les informations inutiles sur un groupe de capture .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())
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
:
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.
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 )
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
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.
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.
merged_sentences = ' * '.join(sentences)
|
"ou" instruction regex:regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag
clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')
"".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.
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.