Ich benutze Python 3.5.2
Ich habe zwei Listen
Ich muss also 750.000 Sätze durchlaufen und ungefähr 20.000 Ersetzungen durchführen aber NUR, wenn meine Wörter tatsächlich "Wörter" sind und nicht Teil einer größeren Zeichenfolge.
Ich mache das durch Vorkompilieren meine Wörter, so dass sie von dem Metazeichen \b
Flankiert werden
compiled_words = [re.compile(r'\b' + Word + r'\b') for Word in my20000words]
Dann gehe ich meine "Sätze" durch
import re
for sentence in sentences:
for Word in compiled_words:
sentence = re.sub(Word, "", sentence)
# put sentence into a growing list
Diese verschachtelte Schleife verarbeitet ungefähr 50 Sätze pro Sekunde, was nett ist, aber es dauert immer noch mehrere Stunden, um alle meine Sätze zu verarbeiten.
Gibt es eine Möglichkeit, die Methode str.replace
(Die meiner Meinung nach schneller ist) zu verwenden, die Ersetzungen jedoch immer noch nur bei Wortgrenzen erforderlich macht?
Gibt es eine Möglichkeit, die Methode re.sub
Zu beschleunigen? Ich habe die Geschwindigkeit bereits geringfügig verbessert, indem ich re.sub
Übersprungen habe, wenn die Länge meines Wortes> ist als die Länge meines Satzes, aber es ist keine große Verbesserung.
Vielen Dank für alle Vorschläge.
Sie können versuchen, ein einziges Muster wie "\b(Word1|Word2|Word3)\b"
zu kompilieren.
Da re
für den tatsächlichen Abgleich auf C-Code angewiesen ist, können die Einsparungen erheblich sein.
Wie @pvg in den Kommentaren ausführte, profitiert es auch vom Single-Pass-Matching.
Wenn Ihre Worte nicht regulär sind, ist Eric's Antwort schneller.
Verwenden Sie diese Methode (mit festgelegter Suche), wenn Sie die schnellste Lösung wünschen. Bei einem Datensatz, der den OPs ähnlich ist, ist er ungefähr 2000-mal schneller als die akzeptierte Antwort.
Wenn Sie darauf bestehen, einen regulären Ausdruck für die Suche zu verwenden, verwenden Sie diese trie-basierte Version , das immer noch 1000-mal schneller als eine reguläre Ausdrucksverbindung ist.
Wenn Ihre Sätze keine gewaltigen Zeichenfolgen sind, können wahrscheinlich mehr als 50 Sätze pro Sekunde verarbeitet werden.
Wenn Sie alle gesperrten Wörter in einem Satz speichern, können Sie sehr schnell überprüfen, ob ein anderes Wort in diesem Satz enthalten ist.
Packe die Logik in eine Funktion, gib diese Funktion als Argument an re.sub
Und du bist fertig!
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)
Umgewandelte Sätze sind:
' . !
.
GiraffeElephantBoat
sfgsdg sdwerha aswertwe
Beachten Sie, dass:
lower()
)""
ersetzen, bleiben möglicherweise zwei Leerzeichen (wie in Ihrem Code).\w+
Auch mit Akzenten (z. B. "ångström"
) Überein.Es gibt eine Million Sätze, banned_words
Hat fast 100000 Wörter und das Skript läuft in weniger als 7 Sekunden.
Im Vergleich dazu benötigte Liteyes Antwort 160s für zehntausend Sätze.
Da n
die Gesamtzahl der Wörter und m
die Anzahl der gesperrten Wörter ist, ist der OP- und Liteye-Code O(n*m)
.
Im Vergleich sollte mein Code in O(n+m)
laufen. Wenn man bedenkt, dass es viel mehr Sätze als verbotene Wörter gibt, wird der Algorithmus zu O(n)
.
Wie komplex ist eine Regex-Suche mit einem '\b(Word1|Word2|...|wordN)\b'
-Muster? Ist es O(N)
oder O(1)
?
Es ist ziemlich schwer zu verstehen, wie die Regex-Engine funktioniert, also schreiben wir einen einfachen Test.
Dieser Code extrahiert 10**i
Zufällige englische Wörter in eine Liste. Es erstellt die entsprechende Regex-Union und testet sie mit verschiedenen Worten:
#
)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))
Es gibt aus:
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
So sieht es aus wie die Suche nach einem einzelnen Wort mit einem '\b(Word1|Word2|...|wordN)\b'
Muster:
O(1)
bester FallO(n/2)
durchschnittlicher Fall, der noch O(n)
istO(n)
schlimmster FallDiese Ergebnisse stimmen mit einer einfachen Schleifensuche überein.
Eine viel schnellere Alternative zu einer Regex-Union ist das Erstellen des Regex-Musters aus einem Trie .
Verwenden Sie diese Methode, wenn Sie die schnellste auf Regex basierende Lösung wünschen. Bei einem Datensatz, der den OPs ähnlich ist, ist er ungefähr 1000-mal schneller als die akzeptierte Antwort.
Wenn Sie sich nicht für Regex interessieren, verwenden Sie diese satzbasierte Version , das 2000-mal schneller ist als eine Regex-Union.
Eine einfache Regex-Vereinigung Annäherung wird mit vielen verbotenen Wörtern langsam, weil die Regex-Engine macht keine sehr gute Arbeit das Muster zu optimieren.
Es ist möglich, ein Trie mit allen gesperrten Wörtern zu erstellen und den entsprechenden regulären Ausdruck zu schreiben. Die resultierenden Ausdrücke oder regulären Ausdrücke sind nicht wirklich für den Menschen lesbar, ermöglichen jedoch ein sehr schnelles Nachschlagen und Abgleichen.
['foobar', 'foobah', 'fooxar', 'foozap', 'fooza']
Die Liste wird in einen Versuch umgewandelt:
{
'f': {
'o': {
'o': {
'x': {
'a': {
'r': {
'': 1
}
}
},
'b': {
'a': {
'r': {
'': 1
},
'h': {
'': 1
}
}
},
'z': {
'a': {
'': 1,
'p': {
'': 1
}
}
}
}
}
}
}
Und dann zu diesem Regex-Muster:
r"\bfoo(?:ba[hr]|xar|zap?)\b"
Der große Vorteil ist, dass zum Testen, ob Zoo
passt, die Regex-Engine nur muss das erste Zeichen vergleichen (passt nicht) statt versucht die 5 Wörter . Es ist ein Übermaß an Vorprozessen für 5 Wörter, zeigt jedoch vielversprechende Ergebnisse für viele tausend Wörter.
Beachten Sie, dass (?:)
Nicht erfassende Gruppen verwendet werden, weil:
foobar|baz
Würde mit foobar
oder baz
, aber nicht mit foobaz
übereinstimmenfoo(bar|baz)
speichert nicht benötigte Informationen in einer Erfassungsgruppe .Hier ist ein leicht modifiziertes Gist , das wir als trie.py
- Bibliothek verwenden können:
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())
Hier ist ein kleiner Test (der gleiche wie dieser ):
# 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))
Es gibt aus:
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
Zur Info, der reguläre Ausdruck beginnt folgendermaßen:
(?: 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))?) | 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))?) | oder (?: (?: \' 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 | oder (?: (?: \' 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))? | 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)))?) | 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))? | pt ich...
Es ist wirklich unlesbar, aber für eine Liste von 100000 verbotenen Wörtern ist dieser Trie-Regex 1000-mal schneller als eine einfache Regex-Union!
Hier ist ein Diagramm der vollständigen Trie, exportiert mit trie-python-graphviz und graphviz twopi
:
Vielleicht möchten Sie versuchen, die Sätze vorab zu verarbeiten, um die Wortgrenzen zu kodieren. Verwandeln Sie jeden Satz in eine Wortliste, indem Sie ihn an den Wortgrenzen aufteilen.
Dies sollte schneller sein, denn um einen Satz zu verarbeiten, müssen Sie nur die einzelnen Wörter durchgehen und prüfen, ob es eine Übereinstimmung ist.
Derzeit muss die Regex-Suche jedes Mal die gesamte Zeichenfolge erneut durchsuchen, nach Wortgrenzen suchen und das Ergebnis dieser Arbeit vor dem nächsten Durchgang "verwerfen".
Hier ist eine schnelle und einfache Lösung mit Testsatz.
Gewinnstrategie:
re ("\ w +", repl, satz) sucht nach Wörtern.
"repl" kann ein aufrufbares sein. Ich habe eine Funktion verwendet, die ein Diktat-Lookup ausführt, und das Diktat enthält die Wörter, die gesucht und ersetzt werden müssen.
Dies ist die einfachste und schnellste Lösung (siehe Funktion replace4 im folgenden Beispielcode).
Zweitbester
Die Idee ist, die Sätze mit re.split in Wörter aufzuteilen, während die Separatoren erhalten bleiben, um die Sätze später zu rekonstruieren. Das Ersetzen erfolgt dann mit einem einfachen Diktat-Lookup.
(Siehe Funktion replace3 im folgenden Beispielcode).
Timings für Beispielfunktionen:
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)
... und 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 )
Vielleicht ist Python hier nicht das richtige Tool. Hier ist eines mit der Unix-Toolchain
sed G file |
tr ' ' '\n' |
grep -vf blacklist |
awk -v RS= -v OFS=' ' '{$1=$1}1'
angenommen, Ihre Blacklist-Datei wird mit den hinzugefügten Wortgrenzen vorverarbeitet. Die Schritte sind: Konvertieren Sie die Datei in doppelte Abstände, teilen Sie jeden Satz in ein Wort pro Zeile, löschen Sie die Blacklist-Wörter massenweise aus der Datei und führen Sie die Zeilen wieder zusammen.
Dies sollte mindestens eine Größenordnung schneller laufen.
Zur Vorverarbeitung der Blacklist-Datei aus Wörtern (ein Wort pro Zeile)
sed 's/.*/\\b&\\b/' words > blacklist
Wie wäre es damit:
#!/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)
Diese Lösungen teilen sich an Wortgrenzen auf und suchen jedes Wort in einem Satz. Sie sollten schneller sein als re.sub von Word-Alternativen (Liteyes-Lösung), da diese Lösungen O(n)
sind, wobei n die Größe der Eingabe aufgrund der amortized O(1)
festgelegten Suche während der Verwendung ist Regex-Alternativen führen dazu, dass die Regex-Engine nicht nur an Wortgrenzen, sondern an allen Zeichen nach Word-Übereinstimmungen suchen muss. Bei meiner Lösung wird besonders darauf geachtet, die im Originaltext verwendeten Leerzeichen beizubehalten (dh Leerzeichen werden nicht komprimiert und Tabulatoren, Zeilenumbrüche und andere Leerzeichen bleiben erhalten) sollte ziemlich einfach sein, um sie aus der Ausgabe zu entfernen.
Ich habe corpus.txt getestet, eine Verknüpfung mehrerer eBooks, die vom Gutenberg-Projekt heruntergeladen wurden, und banned_words.txt besteht aus 20000 Wörtern, die zufällig aus Ubuntus Wortliste (/ usr/share/dict/american-english) ausgewählt wurden. Die Verarbeitung von 862462 Sätzen dauert ca. 30 Sekunden (und die Hälfte davon bei PyPy). Ich habe Sätze als alles definiert, was durch "." Getrennt ist.
$ # 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 profitiert besonders vom zweiten Ansatz, während CPython beim ersten Ansatz besser abschneidet. Der obige Code sollte auf Python 2 und 3 funktionieren.
Eine unten beschriebene Lösung benötigt viel Speicher, um den gesamten Text in derselben Zeichenfolge zu speichern und die Komplexität zu verringern. Wenn RAM ist ein Problem, überlegen Sie es sich zweimal, bevor Sie es verwenden. =
Mit join
/split
Tricks können Sie Schleifen vermeiden, die den Algorithmus beschleunigen sollen.
merged_sentences = ' * '.join(sentences)
|
"Oder" regex statement einen einzelnen regulären Ausdruck für alle Wörter, die Sie aus den Sätzen entfernen müssen: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
Komplexität ist O (n). Das ist ziemlich intuitiv, aber es gibt trotzdem ein verkürztes Zitat aus einer Quelle:
for (i = 0; i < seqlen; i++) {
[...]
sz += PyUnicode_GET_LENGTH(item);
Daher haben Sie mit join/split
O(words) + 2 * O (Sätze), was immer noch eine lineare Komplexität gegenüber 2 * O (N2) mit dem anfänglichen Ansatz.
Übrigens: Verwenden Sie kein Multithreading. GIL blockiert jede Operation, da Ihre Task streng an die CPU gebunden ist, sodass GIL keine Chance hat, freigegeben zu werden. Jeder Thread sendet jedoch gleichzeitig Ticks, die zusätzlichen Aufwand verursachen und die Operation sogar ins Unendliche führen.
Verketten Sie alle Ihre Sätze in einem Dokument. Verwenden Sie eine beliebige Implementierung des Aho-Corasick-Algorithmus ( hier ist einer ), um alle Ihre "schlechten" Wörter zu lokalisieren. Durchsuche die Datei, ersetze jedes fehlerhafte Wort, aktualisiere die Offsets der gefundenen Wörter, die folgen usw.