regulares ignorecase expresiones ejemplos all python regex string performance replace

ignorecase - python string expresiones regulares



Acelera millones de reemplazos de expresiones regulares en Python 3 (9)

TLDR

Use este método (con búsqueda establecida) si desea la solución más rápida. Para un conjunto de datos similar a los OP, es aproximadamente 2000 veces más rápido que la respuesta aceptada.

Si insiste en usar una expresión regular para la búsqueda, use esta versión basada en trie , que aún es 1000 veces más rápida que una unión de expresión regular.

Teoría

Si sus oraciones no son cadenas enormes, probablemente sea factible procesar muchas más de 50 por segundo.

Si guarda todas las palabras prohibidas en un conjunto, será muy rápido verificar si se incluye otra palabra en ese conjunto.

Empaque la lógica en una función, re.sub esta función como argumento para re.sub y listo.

Código

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)

Las oraciones convertidas son:

'' . ! . GiraffeElephantBoat sfgsdg sdwerha aswertwe

Tenga en cuenta que:

  • la búsqueda no distingue entre mayúsculas y minúsculas (gracias a lower() )
  • reemplazar una palabra con "" podría dejar dos espacios (como en su código)
  • Con python3, /w+ también coincide con los caracteres acentuados (por ejemplo, "ångström" ).
  • Cualquier carácter que no sea de palabra (tabulación, espacio, nueva línea, marcas, ...) permanecerá intacto.

Actuación

Hay un millón de oraciones, banned_words tiene casi 100000 palabras y el script se ejecuta en menos de 7 segundos.

En comparación, la answer Liteye necesitaba 160 para 10 mil oraciones.

Con n siendo la cantidad total de palabras m la cantidad de palabras prohibidas, los códigos de OP y Liteye son O(n*m) .

En comparación, mi código debería ejecutarse en O(n+m) . Teniendo en cuenta que hay muchas más oraciones que palabras prohibidas, el algoritmo se convierte en O(n) .

Prueba de unión de expresiones regulares

¿Cuál es la complejidad de una búsqueda de ''/b(word1|word2|...|wordN)/b'' regulares con un ''/b(word1|word2|...|wordN)/b'' ? ¿Es O(N) u O(1) ?

Es bastante difícil comprender la forma en que funciona el motor regex, así que escribamos una prueba simple.

Este código extrae 10**i palabras aleatorias en inglés en una lista. Crea la unión regex correspondiente y la prueba con diferentes palabras:

  • uno claramente no es una palabra (comienza con # )
  • una es la primera palabra en la lista
  • una es la última palabra en la lista
  • uno parece una palabra pero no lo es


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

Produce:

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

Por lo tanto, parece que la búsqueda de una sola palabra con un ''/b(word1|word2|...|wordN)/b'' tiene:

  • O(1) mejor caso
  • O(n/2) caso promedio, que sigue siendo O(n)
  • O(n) peor de los casos

Estos resultados son consistentes con una simple búsqueda de bucle.

Una alternativa mucho más rápida a una unión regex es crear el patrón regex a partir de un trie .

Estoy usando Python 3.5.2

Tengo dos listas

  • una lista de aproximadamente 750,000 "oraciones" (cadenas largas)
  • una lista de aproximadamente 20,000 "palabras" que me gustaría eliminar de mis 750,000 oraciones

Entonces, tengo que recorrer 750,000 oraciones y realizar alrededor de 20,000 reemplazos, pero SOLO si mis palabras son en realidad "palabras" y no son parte de una cadena de caracteres más grande.

Estoy haciendo esto al precompilar mis palabras para que estén flanqueadas por el metacarácter /b

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

Luego recorro mis "oraciones"

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

Este bucle anidado procesa alrededor de 50 oraciones por segundo , lo cual es bueno, pero aún tarda varias horas en procesar todas mis oraciones.

  • ¿Hay alguna manera de usar el método str.replace (que creo que es más rápido), pero que aún requiere que los reemplazos solo sucedan en los límites de las palabras ?

  • Alternativamente, ¿hay alguna manera de acelerar el método re.sub ? Ya he mejorado la velocidad marginalmente saltando sobre re.sub si la longitud de mi palabra es> que la longitud de mi oración, pero no es una gran mejora.

Gracias por cualquier sugerencia


TLDR

Use este método si desea la solución más rápida basada en expresiones regulares. Para un conjunto de datos similar a los OP, es aproximadamente 1000 veces más rápido que la respuesta aceptada.

Si no le importa la expresión regular, use esta versión basada en conjuntos , que es 2000 veces más rápida que una unión de expresión regular.

Regex optimizado con Trie

Un enfoque answer se vuelve lento con muchas palabras prohibidas, porque el motor de expresiones regulares no hace un muy buen trabajo al optimizar el patrón.

Es posible crear un Trie con todas las palabras prohibidas y escribir la expresión regular correspondiente. El trie o regex resultante no son realmente legibles para los humanos, pero permiten una búsqueda y coincidencia muy rápidas.

Ejemplo

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

La lista se convierte en un trie:

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

Y luego a este patrón regex:

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

La gran ventaja es que para probar si el zoo coincide, el motor de expresiones regulares solo necesita comparar el primer carácter (no coincide), en lugar de probar las 5 palabras . Es una exageración previa al proceso para 5 palabras, pero muestra resultados prometedores para muchos miles de palabras.

Tenga en cuenta que (?:) los grupos sin captura se utilizan porque:

Código

Aquí hay una gist ligeramente modificada, que podemos usar como una biblioteca 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())

Prueba

Aquí hay una pequeña prueba (la misma que esta ):

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

Produce:

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

Para información, la expresión regular comienza así:

(?: a (?: (?: / ''s | a (?: /' s | chen | liyah (?: / ''s)? | r (?: dvark (?: (?: /' s | s ))? | on)) | b (?: / ''s | a (?: c (?: us (?: (?: /' s | es))? | [ik]) | ft | solitario (? : (?: / ''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 | 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))?) | o (?: (?: /' 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 | o (?: (?: /' 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 | 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) | s))?) | u (?: nd (? :( ?: ed | ing | s))? | t) | ve (?: (?: / ''s | tablero))?) | 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 yo...

Es realmente ilegible, pero para una lista de 100000 palabras prohibidas, esta expresión regular de Trie es 1000 veces más rápida que una simple unión de expresiones regulares.

Aquí hay un diagrama del trie completo, exportado con trie-python-graphviz y graphviz twopi :


Acercamiento práctico

Una solución que se describe a continuación utiliza mucha memoria para almacenar todo el texto en la misma cadena y reducir el nivel de complejidad. Si la RAM es un problema, piénselo dos veces antes de usarla.

Con los trucos de join / split , puede evitar los bucles que deberían acelerar el algoritmo.

  • Concatenar oraciones con un delimitador especial que no está contenido en las oraciones:
  • merged_sentences = '' * ''.join(sentences)

  • Recopila una expresión regular simple para todas las palabras que necesitas eliminar de las oraciones usando | "o" declaración regex:
  • regex = re.compile(r''/b({})/b''.format(''|''.join(words)), re.I) # re.I is a case insensitive flag

  • Subíndice las palabras con la expresión regular compilada y divídala por el carácter delimitador especial en oraciones separadas:
  • clean_sentences = re.sub(regex, "", merged_sentences).split('' * '')

    Actuación

    "".join complejidad "".join es O (n). Esto es bastante intuitivo, pero de todos modos hay una cita abreviada de una fuente:

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

    Por lo tanto, con join/split tienes O (palabras) + 2 * O (oraciones) que sigue siendo una complejidad lineal vs 2 * O (N 2 ) con el enfoque inicial.

    por cierto, no use multihilo. GIL bloqueará cada operación porque su tarea está estrictamente vinculada a la CPU, por lo que GIL no tiene posibilidad de ser liberada, pero cada subproceso enviará tics al mismo tiempo que causan un esfuerzo adicional e incluso llevan la operación al infinito.


    Bueno, aquí hay una solución rápida y fácil, con conjunto de prueba.

    Estrategia ganadora:

    re.sub ("/ w +", repl, oración) busca palabras.

    "repl" puede ser un invocable. Usé una función que realiza una búsqueda dict, y el dict contiene las palabras para buscar y reemplazar.

    Esta es la solución más simple y rápida (vea la función replace4 en el código de ejemplo a continuación).

    Segundo mejor

    La idea es dividir las oraciones en palabras, usando re.split, mientras conserva los separadores para reconstruir las oraciones más tarde. Luego, los reemplazos se realizan con una simple búsqueda dict.

    (vea la función replace3 en el código de ejemplo a continuación).

    Tiempos por ejemplo funciones:

    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)

    ... y código:

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


    Concatena todas tus oraciones en un solo documento. Use cualquier implementación del algoritmo Aho-Corasick ( aquí hay uno ) para localizar todas sus palabras "malas". Recorre el archivo, reemplaza cada palabra incorrecta, actualiza los desplazamientos de las palabras encontradas que siguen, etc.


    Qué tal esto:

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

    Estas soluciones se dividen en los límites de las palabras y busca cada palabra en un conjunto. Deben ser más rápidos que las alternativas de segundo subtítulo (solución de Liteyes) ya que estas soluciones son O(n) donde n es el tamaño de la entrada debido a la búsqueda de conjunto amortized O(1) , mientras que el uso de alternativas de expresiones regulares causaría motor de expresiones regulares para tener que buscar coincidencias de palabras en cada carácter en lugar de solo en los límites de las palabras. Mi solución es tener mucho cuidado para preservar los espacios en blanco que se utilizaron en el texto original (es decir, no comprime los espacios en blanco y conserva las pestañas, las nuevas líneas y otros caracteres de espacios en blanco), pero si decide que no le importa, debería ser bastante sencillo eliminarlos de la salida.

    Probé en corpus.txt, que es una concatenación de múltiples libros electrónicos descargados del Proyecto Gutenberg, y banned_words.txt son 20000 palabras elegidas al azar de la lista de palabras de Ubuntu (/ usr / share / dict / american-english). Se necesitan alrededor de 30 segundos para procesar 862462 oraciones (y la mitad de eso en PyPy). He definido oraciones como cualquier cosa separada por ".".

    $ # 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 se beneficia particularmente más del segundo enfoque, mientras que a CPython le fue mejor en el primer enfoque. El código anterior debería funcionar tanto en Python 2 como en 3.


    Quizás Python no es la herramienta correcta aquí. Aquí hay uno con la cadena de herramientas Unix

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

    suponiendo que su archivo de lista negra esté preprocesado con los límites de palabras agregados. Los pasos son: convertir el archivo a doble espacio, dividir cada oración en una palabra por línea, eliminar en masa las palabras de la lista negra del archivo y fusionar las líneas.

    Esto debería correr al menos un orden de magnitud más rápido.

    Para preprocesar el archivo de la lista negra a partir de palabras (una palabra por línea)

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


    Una cosa que puede intentar es compilar un patrón único como "/b(word1|word2|word3)/b" .

    Debido re depende del código C para hacer la coincidencia real, los ahorros pueden ser dramáticos.

    Como @pvg señaló en los comentarios, también se beneficia de la coincidencia de un solo paso.

    Si sus palabras no son expresiones regulares, la answer de Eric es más rápida.


    Una cosa que quizás desee probar es preprocesar las oraciones para codificar los límites de las palabras. Básicamente, convierta cada oración en una lista de palabras dividiéndolas en los límites de las palabras.

    Esto debería ser más rápido, porque para procesar una oración, solo tiene que pasar por cada una de las palabras y verificar si es una coincidencia.

    Actualmente, la búsqueda de expresiones regulares tiene que pasar por la cadena completa de nuevo cada vez, buscar límites de palabras y luego "descartar" el resultado de este trabajo antes del próximo paso.