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 \b
metacarácter
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
str.replace
método (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
re.sub
método? Ya he mejorado la velocidad marginalmente saltandore.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
multiprocessing
(es decir, múltiples procesos de Python).Respuestas:
Una cosa que puedes probar es compilar un patrón único como
"\b(word1|word2|word3)\b"
.Debido a que se
re
basa en el 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 respuesta de Eric es más rápida.
fuente
s/They actually use/They actually could in theory sometimes use/
. ¿Tiene alguna razón para creer que la implementación de Python está haciendo algo más que un bucle aquí?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, asigne esta función como argumento para
re.sub
y listo!Código
Las oraciones convertidas son:
Tenga en cuenta que:
lower()
)""
podría dejar dos espacios (como en su código)\w+
también coincide con los caracteres acentuados (p. Ej."ångström"
. .).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 respuesta de Liteye necesitaba 160 para 10 mil oraciones.
Al
n
ser la cantidad total de palabras ym
la cantidad de palabras prohibidas, los códigos de OP y Liteye sonO(n*m)
.En comparación, mi código debería ejecutarse
O(n+m)
. Teniendo en cuenta que hay muchas más oraciones que palabras prohibidas, el algoritmo se convierte enO(n)
.Prueba de unión de expresiones regulares
¿Cuál es la complejidad de una búsqueda de expresiones regulares con un
'\b(word1|word2|...|wordN)\b'
patrón? EsO(N)
oO(1)
?Es bastante difícil comprender la forma en que funciona el motor de expresiones regulares, así que escribamos una prueba simple.
Este código extrae
10**i
palabras inglesas al azar en una lista. Crea la unión regex correspondiente y la prueba con diferentes palabras:#
)Produce:
Parece que la búsqueda de una sola palabra con un
'\b(word1|word2|...|wordN)\b'
patrón tiene:O(1)
mejor casoO(n/2)
caso promedio, que todavía esO(n)
O(n)
peor de los casosEstos 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 .
fuente
O(1)
afirmación engañosa , su respuesta definitivamente merece un voto positivo.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 simple de unión de Regex 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
La lista se convierte en un trie:
Y luego a este patrón regex:
La gran ventaja es que para probar si
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 de 5 palabras, pero muestra resultados prometedores para muchos miles de palabras.Tenga en cuenta que
(?:)
los grupos sin captura se utilizan porque:foobar|baz
coincidiríafoobar
obaz
, pero nofoobaz
foo(bar|baz)
guardaría información innecesaria en un grupo de captura .Código
Aquí hay una esencia ligeramente modificada , que podemos usar como
trie.py
biblioteca:Prueba
Aquí hay una pequeña prueba (la misma que esta ):
Produce:
Para información, la expresión regular comienza así:
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
:fuente
|
pero no se necesitan grupos de captura para nuestro propósito. Simplemente ralentizarían el proceso y usarían más memoria sin beneficio.\b
( límite de palabras ). Si la lista es['apple', 'banana']
, reemplazará las palabras que son exactamenteapple
obanana
, pero nonana
,bana
opineapple
.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 tienes 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 toda la cadena nuevamente cada vez, buscando límites de palabras y luego "descartando" el resultado de este trabajo antes del próximo paso.
fuente
Bueno, aquí hay una solución rápida y fácil, con un conjunto de pruebas.
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:
... y código:
Editar: también puede ignorar las minúsculas cuando verifica si pasa una lista minúscula de oraciones y edita las respuestas
fuente
replace4
y mi código tiene desempeños similares.repl(m):
está haciendo def y cómo está asignandom
en la función replace4error: unbalanced parenthesis
de la líneapatterns_comp = [ (re.compile("\\b"+search+"\\b"), repl) for search, repl in patterns ]
Quizás Python no es la herramienta correcta aquí. Aquí hay uno con la cadena de herramientas Unix
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)
fuente
Qué tal esto:
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 los sustitutos de la segunda palabra (solución de Liteyes) ya que estas soluciones son
O(n)
donde n es el tamaño de la entrada debido aamortized O(1)
búsqueda establecida, mientras que el uso de alternativas de expresiones regulares causaría que el motor de expresiones regulares tenga que verificar la coincidencia de palabras en todos los caracteres 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 usaron 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 es de 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 ".".
PyPy se beneficia 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.
fuente
\W+
es básicamente igual quesub
en\w+
, ¿verdad?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
join
/split
tricks puedes evitar los bucles que deberían acelerar el algoritmo.|
declaración "o" expresión regular:Actuación
"".join
la complejidad es O (n). Esto es bastante intuitivo, pero de todos modos hay una cita abreviada de una fuente:Por lo tanto, con
join/split
usted tiene O (palabras) + 2 * O (oraciones), que sigue siendo una complejidad lineal frente a 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.
fuente
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 las compensaciones de las palabras encontradas que siguen, etc.
fuente