Accelera milioni di sostituzioni regex in Python 3


127

Sto usando Python 3.5.2

Ho due liste

  • un elenco di circa 750.000 "frasi" (stringhe lunghe)
  • un elenco di circa 20.000 "parole" che vorrei eliminare dalle mie 750.000 frasi

Quindi, devo scorrere 750.000 frasi ed eseguire circa 20.000 sostituzioni, ma SOLO se le mie parole sono in realtà "parole" e non fanno parte di una più ampia serie di caratteri.

Lo sto facendo precompilando le mie parole in modo che siano affiancate dal \bmetacarattere

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

Quindi giro tra le mie "frasi"

import re

for sentence in sentences:
  for word in compiled_words:
    sentence = re.sub(word, "", sentence)
  # put sentence into a growing list

Questo ciclo nidificato elabora circa 50 frasi al secondo , il che è carino, ma ci vogliono ancora diverse ore per elaborare tutte le mie frasi.

  • Esiste un modo per utilizzare il str.replacemetodo (che credo sia più veloce), ma che richiede comunque che le sostituzioni avvengano solo ai confini delle parole ?

  • In alternativa, c'è un modo per accelerare il re.submetodo? Ho già migliorato marginalmente la velocità saltando re.subse la lunghezza della mia parola è> della lunghezza della mia frase, ma non è molto un miglioramento.

Grazie per eventuali suggerimenti.


1
La prima risposta qui ha un buon codice di esempio: stackoverflow.com/questions/2846653/… basta dividere l'array di frasi per il numero di core della CPU in cui sono stati eseguiti quei thread
Mohammad Ali

4
Puoi anche provare un'implementazione non regex: attraversa i tuoi input parola per parola e abbina ciascuno con un set. Questo è un passaggio singolo e le ricerche di hash sono piuttosto rapide.
pvg

2
Quanto durano queste frasi, per inciso? 750k linee non sembrano un set di dati che dovrebbe richiedere ore per l'elaborazione.
pvg

2
@MohammadAli: non preoccuparti di quell'esempio per il lavoro associato alla CPU. Python ha un grande blocco necessario per eseguire bytecode (Global Interpreter Lock), quindi non puoi trarre vantaggio dai thread per il lavoro della CPU. Dovresti usare multiprocessing(cioè più processi Python).
Kevin

1
Hai bisogno di uno strumento di forza industriale per farlo. Un trie regex viene generato da un albero ternario di un elenco di stringhe. Non ci sono mai più di 5 passaggi per fallire rendendo questo il metodo più veloce per fare questo tipo di corrispondenza. Esempi: dizionario di 175.000 parole o simile al tuo elenco vietato solo le 20.000 parole S
x15

Risposte:


123

Una cosa che puoi provare è compilare un singolo modello come "\b(word1|word2|word3)\b".

Poiché resi basa sul codice C per eseguire la corrispondenza effettiva, i risparmi possono essere notevoli.

Come sottolineato da @pvg nei commenti, beneficia anche della corrispondenza a passaggio singolo.

Se le tue parole non sono regex, la risposta di Eric è più veloce.


4
Non è solo la C impl (che fa una grande differenza) ma stai anche abbinando con un singolo passaggio. Varianti di questa domanda si presentano abbastanza spesso, è un po 'strano che non ci sia (o forse c'è, nascondersi da qualche parte?) Una risposta SO canonica per questo con questa idea abbastanza sensata.
pvg

40
@Liteye il tuo suggerimento ha trasformato un lavoro di 4 ore in un lavoro di 4 minuti! Sono stato in grado di unire tutte le oltre 20.000 regex in un unico regex gigantesco e il mio laptop non ha battuto ciglio. Grazie ancora.
pdanese,

2
@Bakuriu: s/They actually use/They actually could in theory sometimes use/. Hai qualche motivo per credere che l'implementazione di Python stia facendo qualcosa di diverso da un ciclo qui?
user541686

2
@Bakuriu: Sarei davvero interessato a sapere se è così, ma non credo che la soluzione regex richieda tempo lineare. Se non crea un Trie fuori dal sindacato, non vedo come possa accadere.
Eric Duminil,

2
@Bakuriu: non è un motivo. Mi è stato chiesto se si ha un motivo per credere che la realizzazione in realtà si comporta in questo modo, non è se si dispone di una ragione di credere che potrebbe comportarsi in quel modo. Personalmente devo ancora imbattermi in un'implementazione regex di un singolo linguaggio di programmazione tradizionale che funziona in tempo lineare nello stesso modo in cui ti aspetteresti da una regex classica, quindi se sai che Python lo fa, dovresti mostrare alcune prove.
user541686

123

TLDR

Utilizzare questo metodo (con la ricerca impostata) se si desidera la soluzione più veloce. Per un set di dati simile ai PO, è circa 2000 volte più veloce della risposta accettata.

Se si insiste sull'uso di un regex per la ricerca, utilizzare questa versione basata su trie , che è ancora 1000 volte più veloce di un'unione regex.

Teoria

Se le tue frasi non sono stringhe gigantesche, è probabilmente possibile elaborare molte più di 50 al secondo.

Se salvi tutte le parole vietate in un set, sarà molto veloce controllare se un'altra parola è inclusa in quel set.

Comprimere la logica in una funzione, assegnare questa funzione come argomento re.sube il gioco è fatto!

Codice

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)

Le frasi convertite sono:

' .  !
  .
GiraffeElephantBoat
sfgsdg sdwerha aswertwe

Nota che:

  • la ricerca non fa distinzione tra maiuscole e minuscole (grazie a lower())
  • la sostituzione di una parola con ""potrebbe lasciare due spazi (come nel tuo codice)
  • Con python3, \w+corrisponde anche a caratteri accentati (ad es "ångström".).
  • Qualsiasi carattere non verbale (tab, spazio, nuova riga, segni, ...) rimarrà intatto.

Prestazione

Ci sono un milione di frasi, banned_wordsha quasi 100000 parole e lo script viene eseguito in meno di 7 secondi.

In confronto, la risposta di Liteye aveva bisogno di 160 secondi per 10 mila frasi.

Con nl'ammontare totale di parole e mla quantità di parole vietate, sono OP e il codice di Liteye O(n*m).

In confronto, il mio codice dovrebbe essere eseguito O(n+m). Considerando che ci sono molte più frasi delle parole vietate, l'algoritmo diventa O(n).

Test di unione Regex

Qual è la complessità di una ricerca regex con uno '\b(word1|word2|...|wordN)\b'schema? È O(N)o O(1)?

È piuttosto difficile capire come funziona il motore regex, quindi scriviamo un semplice test.

Questo codice estrae 10**iparole inglesi casuali in un elenco. Crea l'unione regex corrispondente e la verifica con parole diverse:

  • uno chiaramente non è una parola (inizia con #)
  • una è la prima parola nell'elenco
  • una è l'ultima parola nell'elenco
  • uno sembra una parola ma non lo è


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

Emette:

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

Quindi sembra che la ricerca di una sola parola con un '\b(word1|word2|...|wordN)\b'modello abbia:

  • O(1) caso migliore
  • O(n/2) caso medio, che è ancora O(n)
  • O(n) caso peggiore

Questi risultati sono coerenti con una semplice ricerca in loop.

Un'alternativa molto più rapida a un'unione regex è quella di creare il modello regex da un trie .


1
Avevi ragione. Il mio rientro era sbagliato. L'ho risolto nella domanda originale. Per quanto riguarda il commento che 50 frasi / secondo è lento, tutto quello che posso dire è che sto fornendo un esempio semplificato. Il vero set di dati è più complicato di quello che sto descrivendo, ma non sembrava rilevante. Inoltre, la concatenazione delle mie "parole" in una sola regex ha migliorato notevolmente la velocità. Inoltre, sto "spremendo" i doppi spazi dopo le sostituzioni.
pdanese,

1
@ user36476 Grazie per il feedback, ho rimosso la parte corrispondente. Potresti provare il mio suggerimento, per favore? Oserei dire che è molto più veloce della risposta accettata.
Eric Duminil,

1
Da quando hai rimosso tale O(1)affermazione fuorviante , la tua risposta merita sicuramente un voto positivo.
idmean

1
@idmean: vero, non era molto chiaro. Si riferiva solo alla ricerca: "È una parola vietata?".
Eric Duminil,

1
@EricDuminil: ottimo lavoro! Vorrei poter votare una seconda volta.
Matthieu M.

105

TLDR

Utilizzare questo metodo se si desidera la soluzione basata su regex più veloce. Per un set di dati simile ai PO, è circa 1000 volte più veloce della risposta accettata.

Se non ti interessa regex, usa questa versione basata su set , che è 2000 volte più veloce di un'unione regex.

Regex ottimizzato con Trie

Un semplice approccio sindacale Regex diventa lento con molte parole vietate, perché il motore regex non fa un ottimo lavoro di ottimizzazione del modello.

È possibile creare un Trie con tutte le parole vietate e scrivere la regex corrispondente. Il trie o regex risultante non sono realmente leggibili dall'uomo, ma consentono una ricerca e una corrispondenza molto veloci.

Esempio

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

Unione Regex

L'elenco viene convertito in un trie:

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

E poi a questo schema regex:

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

Regex trie

L'enorme vantaggio è che per verificare se le zoocorrispondenze, il motore regex deve solo confrontare il primo carattere (non corrisponde), invece di provare le 5 parole . È un overkill di preprocesso per 5 parole, ma mostra risultati promettenti per molte migliaia di parole.

Si noti che (?:)i gruppi non acquisiti vengono utilizzati perché:

Codice

Ecco un riassunto leggermente modificato , che possiamo usare come trie.pylibreria:

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

Test

Ecco un piccolo test (uguale a questo ):

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

Emette:

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

Per informazioni, il regex inizia in questo modo:

(: un (: (: \ 's | a (?: \'???? s | chen | Liyah (: \ 's) | R (:?? dvark (: (:? \' S | s )) | B | su))? (:? \ 's | a (:? c (: noi (: (: \???' s | es)) | [ik]) | ft | solitario (?? : (:? \ 's | s)?) | ndon (?? :( ?: ndr | ing | ment (: \' s) |??? s)) | s (: e (:( ?:? ment: | [ds])) | h (:( ?: e [DS] | ing)) | iNG) | t ((\ 's?):?????? e (:( ?: ment ( ?: \ 's) | [ds])) | ing | toir? (:? (: \??' s | s)))) | B (:? come? (:?? id) | e (? : ss (: (:? \ 's | es)) |? y (: (: \??' |) | ot (s s)):? (:??? \ 's | t (: \ 's) | s)) | reviat?? (: e [ds] | i (:?? ng | on? (: (: \??' s | s)))) | y? (:? \' ? s) | \ e (: (:? \ 's | s)?)) | d (: ICAT (: e [ds] | i (:??? ng | on (:??? (: \ 's | s)))) | om? (: en (: (: \???' s | s)) | inale) | u (:??? ct (:( ?: ed | I (?: ng | on (: (: \?? 's | s))) | o? (: (:? \' s | s)????) | s)) | l (: \ 's)) ) | e (: (:? \ 's | am | l (: (:? \' s | ard | figlio (:??? \ 's))) | R (:?? Deen (: \ 's) | nathy? (:? \' s) | ra (:??? nt | zione (: (:? \ 's | s))??)) | t (:( ?: t (?: e (:? r (: (: \?? 's | s)) | d?) | ing | o? (:? (: \'s | s))) | s)) | Yance? (:? \ 's) | d)) | Hor (:( ?: R (:????? e (n (: ce (??? : \ 's) | t) | d) | iNG) | s)) | i (:??? d (: e [ds] | ing | gennaio (:??? \'? s)) | Gail | l (: ENE | esso (:? i | y (: \ 's))?) | j | ur ((:: ect (ly??):???? zione (: (: \?)' s | s)) | e [DS] | ing)) | l (:???? un (: tivo (: (:???? \ 's | s)) | ze) | e (:(? : st | r)) | OOM | ution? (:? (:???? \ '? s | s)) | y) | m \' s | n (: e (: gat (: e [ds] ? | i (: ng | on (:? \ 's)?) | r (?: \?)' s)) | normale (:( ?: esso? (:? i | y? (:? \' s)) | ly))) | o (?:? ard | de (: (:???? \ 's | s)) | Li (:? sh (:( ?: e [DS] | ing )) | zione? (:? (: \ '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)) | ve) | s))) | u (|: (\ 's s?):?????? nd (:( ?: ed | ing | s)) | t) | ve (: (:? \ 's | bacheca))) | R (:??? un (: cadabra (: \?' s) | d? (:?? e [DS] | iNG) | prosciutto (? : \ '? s) | m (: (:? \' s | s)?) | SI (: on (: (:??? \ 's | s)) |? ve (:( ?:?\ 'S | ly | Ness (: \?' | S)))) | est | IDG (s):??? E (:( ?: ment (: (:??? \ 'S | S)) ? | [DS])) | ment (| ing:? (:?? s \ '| s))) | o (:?? annuncio | 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)) | en (:( ?: ndr | | ing s?)? (:? ce (: (:? \ 's | s)???) | t (:( ?: e (: e ( ?:? (: \ 's | ismo (: \?' 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 (: \? '| t s)? (: (:? \' s | s))??) | d) | ing | s)) | PTI ...s | [es])) | ond (:( ?: ed | ing | s))) | en (:???? Ce (: (:??? \ 's | S)) | t (?: ? (: e (: e (: (:???? \ '? s | ismo (: \'?? 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)) | PTI .. .s | [es])) | ond (:( ?: ed | ing | s))) | en (:???? Ce (: (:??? \ 's | S)) | t (?: ? (: e (: e (: (:???? \ '? s | ismo (: \'?? 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)) | PTI .. .

È davvero illeggibile, ma per un elenco di 100000 parole vietate, questa regex di Trie è 1000 volte più veloce di una semplice unione di regex!

Ecco un diagramma del trie completo, esportato con trie-python-graphviz e graphviz twopi:

Inserisci qui la descrizione dell'immagine


Sembra che per scopi originali, non sia necessario un gruppo che non acquisisca. Almeno il significato del gruppo non catturante dovrebbe essere menzionato
Xavier Combelle,

3
@XavierCombelle: hai ragione, dovrei menzionare il gruppo di acquisizione: la risposta è stata aggiornata. |Però la vedo al contrario: le parentesi sono necessarie per l'alternanza regex ma i gruppi di acquisizione non sono necessari per il nostro scopo. Avrebbero solo rallentato il processo e utilizzato più memoria senza benefici.
Eric Duminil,

3
@EricDuminil Questo post è perfetto, grazie mille :)
Mohamed AL ANI

1
@MohamedALANI: rispetto a quale soluzione?
Eric Duminil,

1
@ PV8: dovrebbe corrispondere solo a parole complete, sì, grazie al \b( limite di parole ). Se l'elenco è ['apple', 'banana'], sostituirà le parole che sono esattamente appleo banana, ma non nana, banao pineapple.
Eric Duminil,

15

Una cosa che potresti voler provare è pre-elaborare le frasi per codificare i confini delle parole. Fondamentalmente trasforma ogni frase in un elenco di parole dividendo i confini delle parole.

Questo dovrebbe essere più veloce, perché per elaborare una frase, devi solo scorrere ciascuna delle parole e verificare se è una corrispondenza.

Attualmente la ricerca regex deve ripetere di nuovo l'intera stringa ogni volta, cercando i limiti delle parole e quindi "scartando" il risultato di questo lavoro prima del passaggio successivo.


8

Bene, ecco una soluzione semplice e veloce, con set di test.

Strategia vincente:

re.sub ("\ w +", sostitu, frase) cerca le parole.

"sostitu" può essere richiamabile. Ho usato una funzione che esegue una ricerca di dict e il dict contiene le parole da cercare e sostituire.

Questa è la soluzione più semplice e veloce (vedere la funzione di sostituzione 4 nell'esempio di codice seguente).

Il secondo migliore

L'idea è di dividere le frasi in parole, usando re.split, conservando i separatori per ricostruire le frasi in seguito. Quindi, le sostituzioni vengono eseguite con una semplice ricerca dict.

(vedere la funzione di sostituzione 3 nel seguente codice di esempio).

Tempi per funzioni di esempio:

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)

... e codice:

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

Modifica: puoi anche ignorare le lettere minuscole quando controlli se passi un elenco di frasi minuscole e modifica sostituisci

def replace4( sentences ):
pd = patterns_dict.get
def repl(m):
    w = m.group()
    return pd(w.lower(),w)

1
Upgrade per i test. replace4e il mio codice ha prestazioni simili.
Eric Duminil,

Non sono sicuro di cosa repl(m):stia facendo def e come si stia assegnando mla funzione di sostituzione 4
StatguyUser

Inoltre sto ricevendo un errore error: unbalanced parenthesisper la lineapatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
StatguyUser

Mentre la funzione di sostituzione3 e di sostituzione4 risolve il problema originale (per sostituire le parole), sostituisci1 e sostituisci2 sono più generici, poiché funzionano anche se l'ago è una frase (una sequenza di parole) e non una sola parola.
Zoltan Fedor,

7

Forse Python non è lo strumento giusto qui. Eccone uno con la toolchain Unix

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

supponendo che il file della blacklist sia preelaborato con i limiti delle parole aggiunti. I passaggi sono: convertire il file in doppia spaziatura, dividere ogni frase in una parola per riga, eliminare in massa le parole della lista nera dal file e unire nuovamente le righe.

Questo dovrebbe eseguire almeno un ordine di grandezza più velocemente.

Per preelaborare il file della lista nera dalle parole (una parola per riga)

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

4

Cosa ne pensi di questo:

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

Queste soluzioni si dividono sui confini delle parole e cercano ogni parola in un set. Dovrebbero essere più veloci di re.sub delle parole alternative (soluzione di Liteyes) poiché queste soluzioni sono O(n)dove n è la dimensione dell'input dovuto alla amortized O(1)ricerca impostata, mentre l'uso di regex alternates farebbe in modo che il motore regex debba verificare la corrispondenza delle parole su ogni personaggio piuttosto che solo sui confini delle parole. La mia soluzione presta particolare attenzione a preservare gli spazi bianchi utilizzati nel testo originale (ovvero non comprime gli spazi bianchi e conserva le schede, le nuove righe e altri caratteri degli spazi bianchi), ma se decidi che non ti interessa, dovrebbe essere abbastanza semplice per rimuoverli dall'output.

Ho testato su corpus.txt, che è una concatenazione di più eBook scaricati dal Progetto Gutenberg, e banned_words.txt sono 20000 parole scelte casualmente dalla lista di parole di Ubuntu (/ usr / share / dict / american-english). Ci vogliono circa 30 secondi per elaborare 862462 frasi (e la metà su PyPy). Ho definito le frasi come qualsiasi cosa separata da ".".

$ # 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 beneficia in particolare di più del secondo approccio, mentre CPython ha ottenuto risultati migliori nel primo approccio. Il codice sopra dovrebbe funzionare su Python 2 e 3.


Python 3 è un dato nella domanda. Ho valutato questo, ma penso che varrebbe la pena sacrificare alcuni dettagli e un'implementazione 'ottimale' in questo codice per renderlo meno prolisso.
pvg

Se lo capisco correttamente, è sostanzialmente lo stesso principio della mia risposta, ma più dettagliato? Dividere e unirsi \W+è praticamente come subsu \w+, giusto?
Eric Duminil,

Mi chiedo se la mia soluzione di seguito (funzione di sostituzione 4) sia più veloce di pypy;) Mi piacerebbe testare i tuoi file!
bobflux,

3

Approccio pratico

Una soluzione descritta di seguito utilizza molta memoria per archiviare tutto il testo nella stessa stringa e per ridurre il livello di complessità. Se la RAM è un problema, pensaci due volte prima di usarlo.

Con join/ splittrucchi puoi evitare qualsiasi loop che dovrebbe accelerare l'algoritmo.

  • Concatena una frase con un delimitatore speciale che non è contenuto nelle frasi:
  • merged_sentences = ' * '.join(sentences)

  • Compila una singola regex per tutte le parole che ti servono per liberarti dalle frasi usando |"o" l'istruzione regex:
  • regex = re.compile(r'\b({})\b'.format('|'.join(words)), re.I) # re.I is a case insensitive flag

  • Sottoscrivi le parole con la regex compilata e dividila per lo speciale carattere delimitatore in frasi separate:
  • clean_sentences = re.sub(regex, "", merged_sentences).split(' * ')

    Prestazione

    "".joinla complessità è O (n). Questo è piuttosto intuitivo ma comunque c'è una citazione abbreviata da una fonte:

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

    Quindi con join/splitte hai O (parole) + 2 * O (frasi) che è ancora complessità lineare rispetto a 2 * O (N 2 ) con l'approccio iniziale.


    tra l'altro non usare il multithreading. GIL bloccherà ogni operazione perché l'attività è strettamente legata alla CPU, quindi GIL non ha alcuna possibilità di essere rilasciata, ma ogni thread invierà tick simultaneamente che causano ulteriore sforzo e portano persino l'operazione all'infinito.


    Nel caso in cui le frasi siano (erano) memorizzate in un file di testo, sono già separate da una nuova riga. Quindi l'intero file potrebbe essere letto come una grande stringa (o buffer), le parole rimosse e poi riscritte (o questo potrebbe essere fatto direttamente nel file usando la mappatura della memoria). Otoh, per rimuovere una parola, il resto della stringa deve essere spostato indietro per riempire il vuoto, quindi sarebbe un problema con una stringa molto grande. Un'alternativa sarebbe quella di riscrivere le parti tra le parole in un'altra stringa o file (che includerebbe le nuove righe) - o semplicemente spostare quelle parti in un file mmapped (1) ..
    Danny_ds

    .. L'ultimo approccio (spostare / scrivere le parti tra le parole) combinato con la ricerca sul set di Eric Duminil potrebbe essere davvero veloce, forse senza nemmeno usare regex. (2)
    Danny_ds

    .. O forse regex è già ottimizzato per spostare solo quelle parti quando si sostituiscono più parole, non lo so.
    Danny_ds,

    0

    Concatena tutte le tue frasi in un unico documento. Usa qualsiasi implementazione dell'algoritmo Aho-Corasick ( eccone uno ) per localizzare tutte le tue parole "cattive". Attraversa il file, sostituendo ogni parolaccia, aggiornando gli offset delle parole trovate che seguono ecc.

    Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
    Licensed under cc by-sa 3.0 with attribution required.