web-dev-qa-db-de.com

Beschleunigen Sie Millionen von Regex-Ersetzungen in Python 3

Ich benutze Python 3.5.2

Ich habe zwei Listen

  • eine Liste von ca. 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.

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.

112
pdanese

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.

109
Liteye

TLDR

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.

Theorie

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!

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)

Umgewandelte Sätze sind:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Beachten Sie, dass:

  • die Suche unterscheidet nicht zwischen Groß- und Kleinschreibung (dank lower())
  • wenn Sie ein Wort durch "" ersetzen, bleiben möglicherweise zwei Leerzeichen (wie in Ihrem Code).
  • Mit python3 stimmt \w+ Auch mit Akzenten (z. B. "ångström") Überein.
  • Alle Nicht-Word-Zeichen (Tabulator, Leerzeichen, Zeilenumbrüche, Markierungen, ...) bleiben unberührt.

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

Regex Union Test

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:

  • man ist eindeutig kein Wort (es beginnt mit #)
  • eins ist das erste Wort in der Liste
  • eins ist das letzte Wort in der Liste
  • man sieht aus wie ein Wort, ist es aber nicht


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 Fall
  • O(n/2) durchschnittlicher Fall, der noch O(n) ist
  • O(n) schlimmster Fall

Diese Ergebnisse stimmen mit einer einfachen Schleifensuche überein.

Eine viel schnellere Alternative zu einer Regex-Union ist das Erstellen des Regex-Musters aus einem Trie .

105
Eric Duminil

TLDR

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.

Optimierter Regex mit Trie

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.

Beispiel

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

Regex union

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"

Regex trie

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:

Code

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

Prüfung

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 :

Enter image description here

88
Eric Duminil

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".

12
Denziloe

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

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
6
karakfa

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.

4
Lie Ryan

Praktischer Ansatz

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.

  • Verketten Sie Sätze mit einem speziellen Trennzeichen, das in den Sätzen nicht enthalten ist:
  • merged_sentences = ' * '.join(sentences)
    
  • Kompilieren Sie mit | "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
    
  • Indizieren Sie die Wörter mit dem kompilierten regulären Ausdruck und teilen Sie sie durch das spezielle Trennzeichen wieder in getrennte Sätze auf:
  • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')
    

    Performance

    "".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.

    3
    I159

    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.

    0
    Edi Bice