Ich benutze Python 3.5.2
Ich habe zwei Listen
- eine Liste von ungefähr 750.000 "Sätzen" (lange Zeichenketten)
- eine Liste von ungefähr 20.000 "Wörtern", die ich aus meinen 750.000 Sätzen löschen möchte
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 sind.
Ich dies tun pre-Kompilierung meine Worte , so dass sie durch die flankiert sind \b
metacharacter
compiled_words = [re.compile(r'\b' + word + r'\b') for word in my20000words]
Dann durchlaufe ich meine "Sätze"
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
str.replace
Methode zu verwenden (die meiner Meinung nach schneller ist), aber dennoch zu verlangen, dass Ersetzungen nur an Wortgrenzen erfolgen ?Gibt es alternativ eine Möglichkeit, die
re.sub
Methode zu beschleunigen ? Ich habe die Geschwindigkeit bereits geringfügig verbessert, indem ich übersprungen habe,re.sub
wenn die Länge meines Wortes> als die Länge meines Satzes ist, aber es ist keine große Verbesserung.
Vielen Dank für Anregungen.
multiprocessing
(dh mehrere Python-Prozesse).Antworten:
Eine Sache, die Sie versuchen können, ist, ein einzelnes Muster wie zu kompilieren
"\b(word1|word2|word3)\b"
.Da
re
der eigentliche Abgleich auf C-Code basiert, können die Einsparungen dramatisch sein.Wie @pvg in den Kommentaren hervorhob, profitiert es auch vom Single-Pass-Matching.
Wenn Ihre Worte nicht regulär sind, ist Erics Antwort schneller.
quelle
s/They actually use/They actually could in theory sometimes use/
. Haben Sie Grund zu der Annahme, dass die Implementierung von Python hier etwas anderes als eine Schleife ausführt?TLDR
Verwenden Sie diese Methode (mit Set-Lookup), 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 , die immer noch 1000-mal schneller ist als eine reguläre Vereinigung.
Theorie
Wenn Ihre Sätze keine riesigen Zeichenfolgen sind, ist es wahrscheinlich möglich, viel mehr als 50 pro Sekunde zu verarbeiten.
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.
Packen Sie die Logik in eine Funktion, geben Sie diese Funktion als Argument an
re.sub
und Sie sind fertig!Code
Konvertierte Sätze sind:
Beachten Sie, dass:
lower()
).""
möglicherweise zwei Leerzeichen (wie in Ihrem Code).\w+
auch mit Akzentzeichen überein (z"ångström"
. B. ).Performance
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 160 Sekunden für zehntausend Sätze.
Mit
n
der Gesamt amound von Wörtern zu sein undm
die Menge der verbotenen Wörter, OPs und Liteye Code sindO(n*m)
.Im Vergleich dazu sollte mein Code in laufen
O(n+m)
. Wenn man bedenkt, dass es viel mehr Sätze als verbotene Wörter gibt, wird der AlgorithmusO(n)
.Regex Union Test
Wie komplex ist eine Regex-Suche mit einem
'\b(word1|word2|...|wordN)\b'
Muster? Ist esO(N)
oderO(1)
?Es ist ziemlich schwer zu verstehen, wie die Regex-Engine funktioniert. Schreiben wir also 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:#
)Es gibt aus:
Es sieht also so aus, als hätte die Suche nach einem einzelnen Wort mit einem
'\b(word1|word2|...|wordN)\b'
Muster Folgendes:O(1)
I'm besten fallO(n/2)
Durchschnittsfall, der noch istO(n)
O(n)
schlimmsten FallDiese Ergebnisse stimmen mit einer einfachen Schleifensuche überein.
Eine viel schnellere Alternative zu einer Regex-Vereinigung besteht darin, das Regex-Muster aus einem Versuch zu erstellen .
quelle
O(1)
Behauptung entfernt haben, verdient Ihre Antwort definitiv eine positive Abstimmung.TLDR
Verwenden Sie diese Methode, wenn Sie die schnellste Regex-basierte 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 , die 2000-mal schneller ist als eine Regex-Union.
Optimierter Regex mit Trie
Ein einfacher Regex-Union- Ansatz wird mit vielen gesperrten Wörtern langsam, da die Regex-Engine das Muster nicht sehr gut optimiert.
Es ist möglich, einen Trie mit allen gesperrten Wörtern zu erstellen und den entsprechenden regulären Ausdruck zu schreiben. Der resultierende Trie oder Regex ist nicht wirklich lesbar, ermöglicht jedoch eine sehr schnelle Suche und Übereinstimmung.
Beispiel
Die Liste wird in einen Versuch konvertiert:
Und dann zu diesem Regex-Muster:
Der große Vorteil ist, dass
zoo
die Regex-Engine zum Testen von Übereinstimmungen nur das erste Zeichen vergleichen muss (es stimmt nicht überein), anstatt die 5 Wörter auszuprobieren . Es ist ein Vorverarbeitungs-Overkill für 5 Wörter, aber es zeigt vielversprechende Ergebnisse für viele tausend Wörter.Beachten Sie, dass
(?:)
nicht erfassende Gruppen verwendet werden, weil:foobar|baz
würde passenfoobar
oderbaz
, aber nichtfoobaz
foo(bar|baz)
würde nicht benötigte Informationen in einer Erfassungsgruppe speichern .Code
Hier ist ein leicht modifizierter Kern , den wir als
trie.py
Bibliothek verwenden können:Prüfung
Hier ist ein kleiner Test (der gleiche wie dieser ):
Es gibt aus:
Zur Information, der Regex beginnt wie folgt:
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-Vereinigung!
Hier ist ein Diagramm des gesamten Tries, das mit Trie-Python-Graphviz und Graphviz exportiert wurde
twopi
:quelle
|
aber das Erfassen von Gruppen wird für unseren Zweck überhaupt nicht benötigt. Sie würden nur den Prozess verlangsamen und mehr Speicher ohne Nutzen verwenden.\b
( Wortgrenze ). Wenn die Liste ist['apple', 'banana']
, werden Wörter ersetzt, die genauapple
oderbanana
, aber nichtnana
,bana
oder sindpineapple
.Eine Sache, die Sie versuchen möchten, ist die Vorverarbeitung der Sätze, um die Wortgrenzen zu codieren. Verwandeln Sie jeden Satz in eine Liste von Wörtern, indem Sie die Wortgrenzen aufteilen.
Dies sollte schneller sein, da Sie zum Verarbeiten eines Satzes nur jedes der Wörter durchgehen und prüfen müssen, ob es übereinstimmt.
Derzeit muss die Regex-Suche jedes Mal die gesamte Zeichenfolge erneut durchlaufen, nach Wortgrenzen suchen und dann das Ergebnis dieser Arbeit vor dem nächsten Durchgang "verwerfen".
quelle
Hier ist eine schnelle und einfache Lösung mit Testsatz.
Gewinnstrategie:
re ("\ w +", repl, satz) sucht nach Wörtern.
"repl" kann aufrufbar sein. Ich habe eine Funktion verwendet, die eine Diktatsuche durchführt, und das Diktat enthält die Wörter, die gesucht und ersetzt werden sollen.
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 und dabei die Trennzeichen beizubehalten, um die Sätze später zu rekonstruieren. Das Ersetzen erfolgt dann mit einer einfachen Diktatsuche.
(siehe Funktion replace3 im folgenden Beispielcode).
Timings zum Beispiel Funktionen:
... und Code:
Bearbeiten: Sie können auch Kleinbuchstaben ignorieren, wenn Sie prüfen, ob Sie eine Kleinbuchstabenliste mit Sätzen übergeben und repl bearbeiten
quelle
replace4
und mein Code haben ähnliche Leistungen.repl(m):
tut und wie Siem
in der Funktion replace4error: unbalanced parenthesis
patterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
Vielleicht ist Python hier nicht das richtige Werkzeug. Hier ist eine mit der Unix-Toolchain
Angenommen, Ihre Blacklist-Datei ist 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)
quelle
Wie wäre es damit:
Diese Lösungen teilen sich an Wortgrenzen auf und suchen jedes Wort in einer Menge. Sie sollten schneller sein als die Wiederholung von Wortalternativen (Liteyes-Lösung), da bei diesen Lösungen
O(n)
n die Größe der Eingabe aufgrund von istamortized O(1)
festgelegten Suche ist, während die Verwendung von Regex-Alternativen dazu führen würde, dass die Regex-Engine nach auf alle Zeichen und nicht nur auf Wortgrenzen. Meine Lösung achtet besonders darauf, die im Originaltext verwendeten Leerzeichen beizubehalten (dh es werden keine Leerzeichen komprimiert und Tabulatoren, Zeilenumbrüche und andere Leerzeichen werden beibehalten), aber wenn Sie entscheiden, dass Sie sich nicht darum kümmern, ist dies der Fall sollte ziemlich einfach sein, um sie aus der Ausgabe zu entfernen.Ich habe auf corpus.txt getestet, einer Verkettung mehrerer eBooks, die aus dem Gutenberg-Projekt heruntergeladen wurden. Banned_words.txt enthält 20000 Wörter, die zufällig aus Ubuntus Wortliste (/ usr / share / dict / american-english) ausgewählt wurden. Die Verarbeitung von 862462 Sätzen dauert ungefähr 30 Sekunden (und die Hälfte davon auf PyPy). Ich habe Sätze als alles definiert, was durch "." Getrennt ist.
PyPy profitiert besonders mehr vom zweiten Ansatz, während CPython beim ersten Ansatz besser abschnitt. Der obige Code sollte sowohl auf Python 2 als auch auf Python 3 funktionieren.
quelle
\W+
ist im Grunde wiesub
an\w+
, oder?Praktischer Ansatz
Eine unten beschriebene Lösung verwendet viel Speicher, um den gesamten Text in derselben Zeichenfolge zu speichern und die Komplexität zu verringern. Wenn RAM ein Problem ist, überlegen Sie es sich zweimal, bevor Sie es verwenden.
Mit
join
/split
Tricks können Sie Schleifen vermeiden, die den Algorithmus beschleunigen sollten.|
entfernen müssen, indem Sie die Anweisung "oder" verwenden:Performance
"".join
Komplexität ist O (n). Das ist ziemlich intuitiv, aber es gibt trotzdem ein verkürztes Zitat aus einer Quelle:Daher
join/split
haben Sie mit O (Wörter) + 2 * O (Sätze) immer noch eine lineare Komplexität gegenüber 2 * O (N 2 ) mit dem anfänglichen Ansatz.Übrigens verwenden Sie kein Multithreading. GIL blockiert jede Operation, da Ihre Aufgabe streng an die CPU gebunden ist, sodass GIL keine Chance hat, freigegeben zu werden, aber jeder Thread sendet gleichzeitig Ticks, die zusätzlichen Aufwand verursachen und sogar die Operation bis ins Unendliche führen.
quelle
Verketten Sie alle Ihre Sätze zu einem Dokument. Verwenden Sie eine beliebige Implementierung des Aho-Corasick-Algorithmus ( hier eine ), um alle Ihre "schlechten" Wörter zu lokalisieren. Durchlaufen Sie die Datei, ersetzen Sie jedes fehlerhafte Wort, aktualisieren Sie die Offsets der folgenden gefundenen Wörter usw.
quelle