Buscar y reemplazar palabras en Python con FlashText

En este tutorial, veremos cómo encontrar y reemplazar palabras en un archivo de texto, usando Python y FlashText, de una manera extremadamente eficiente, sin usar expresiones regulares (regex).

Introducción

En este tutorial, explicaremos cómo reemplazar palabras en secuencias de texto, con Python utilizando el módulo FlashText, que proporciona una de las formas más eficientes de reemplazar un gran conjunto de palabras en un documento de texto.

¿Cómo funciona el algoritmo FlashText?

El módulo FlashText se basa en su propio algoritmo, el Algoritmo FlashText. En esencia, se basa en una implementación de Python del algoritmo Aho-Corasick.

La conclusión fundamental del algoritmo es reducir el tiempo dedicado a encontrar una gran cantidad de palabras clave en el texto, al minimizar la cantidad de veces que se escanea el texto.

Un algoritmo ingenuo consiste en comparar cada palabra clave con cada palabra del texto completo, similar a la búsqueda lineal. Ese enfoque es extremadamente ineficiente, porque necesita escanear el documento de texto completo n veces, donde n es la cantidad de palabras clave.

Por otro lado, FlashText opta por un enfoque diferente, con la intención de escanear el texto una sola vez.

La clave de la eficiencia del algoritmo FlashText es que almacena todas las palabras clave, junto con las palabras de reemplazo correspondientes en un diccionario. Luego, en lugar de escanear el texto una vez por cada palabra clave en el diccionario, escanea el texto solo una vez. En ese escaneo sobre el texto, las palabras se comparan con las claves del diccionario y, si están presentes, se reemplazan con el valor de la clave.

Cómo instalar FlashText

Instalar FlashText es bastante sencillo, a través de pip:

1
pip install flashtext

Cómo usar FlashText

Primero echemos un vistazo a la API FlashText y algunas de las clases clave dentro de ella.

La clase KeywordProcessor

La clase principal con la que trabajaremos, que se ocupa del procesamiento de palabras clave, es la clase KeywordProcessor. Vamos a importarlo directamente desde FlashText e inicializarlo:

1
2
from flashtext import KeywordProcessor
keyword_processor = KeywordProcessor()

La línea anterior crea el objeto KeywordProcessor que funcionará en el modo que no distingue entre mayúsculas y minúsculas.

El modo que no distingue entre mayúsculas y minúsculas hará coincidir abc con ABC, así como con aBc, ABc, etc.

El modo que distingue entre mayúsculas y minúsculas solo coincidirá con aaa con exactamente la misma cadena, aaa.

Alternativamente, podemos crear una instancia de KeywordProcessor en el modo sensible a mayúsculas y minúsculas:

1
keyword_processor= KeywordProcessor(case_sensitive=True)

Definición del Diccionario de palabras clave

En el módulo FlashText, usamos palabras clave para definir palabras que necesitan ser reemplazadas. El objeto KeywordProcessor contiene un diccionario que contiene todas las palabras clave definidas.

Hay dos formas de agregar palabras clave al diccionario: a granel o una por una.

En primer lugar, echemos un vistazo a cómo agregar palabras clave una por una:

1
keyword_processor.add_keyword(<keyword>, <replacementWord>)

<PalabraReemplazo> es un argumento opcional. Cuando no se especifica, solo se agrega <palabra clave> al diccionario y no hay forma de reemplazarlo con otra palabra de reemplazo.

Entonces, si desea usar FlashText para reemplazar palabras en un texto, le recomendamos que especifique el argumento <replacementWord>.

Si tenemos más de un par de palabras clave, agregarlas una por una puede llevar un poco de tiempo. Una alternativa, mucho más utilizada incluso para pequeñas listas de palabras clave, es añadir palabras clave de forma masiva:

1
2
3
4
5
6
7
8
keyword_dictionary = {
    'replacementWord1': ['list', 'of', 'keywords', 'for', 'replacementWord1'],
    'replacementWord2': ['list', 'of', 'keywords', 'for', 'replacementWord2'],
    ...
    'replacementWordN': ['list', 'of', 'keywords', 'for', 'replacementWordN']
}

keyword_processor.add_keywords_from_dict(keyword_dictionary )

Cada clave en el diccionario es una palabra clave de cadena. Cada valor debe ser una lista. Alternativamente, puede proporcionar palabras clave a través de una “Lista”:

1
keyword_processor.add_keywords_from_list(['list', 'of', 'keywords'])

Sin embargo, con este enfoque, solo agrega las palabras clave sin palabras de reemplazo. O, si un archivo de texto contiene pares clave-valor siguiendo una sintaxis clave=>valor:

1
2
keyword1=>value1
keyword2=>value2

Podemos importarlos a través de la función keywords_from_file():

1
keyword_processor.add_keywords_from_file('keyword_list.txt')

Un enfoque popular, que le permite la mayor flexibilidad y una gran legibilidad, es usar un diccionario. También es la coincidencia más natural para el algoritmo, dado que finalmente todo termina en un diccionario.

Ahora, echemos un vistazo a un ejemplo rápido. Imagine que tenemos un documento textual y queremos minimizar el uso de sinónimos para estandarizar el vocabulario utilizado. En esencia, queremos reemplazar todas las apariciones de palabras como horrible, terrible y horrible (lista de palabras clave) con la palabra malo (palabra de reemplazo), y todas las apariciones de palabras como bien, excelente y genial , con la palabra bien.

Agregaríamos esas palabras clave y palabras_de_reemplazo al keyword_dictionary:

1
2
3
4
keyword_dictionary = {
    "bad": ["awful", "terrible", "horrible"],
    "good": ["fine", "excellent", "great"]
}

Y, finalmente, agregue keyword_dictionary al objeto keyword_processor:

1
keyword_processor.add_keywords_from_dict(keyword_dictionary)

Reemplazar palabras clave con palabras de reemplazo

Una vez que hayamos cargado las palabras clave y sus respectivas palabras de reemplazo en la instancia KeywordProcessor, podemos ejecutar la función replace_keywords(), que escanea el texto proporcionado y ejecuta el reemplazo:

1
new_text = keywordProcessor.replace_keywords("Passed String")

Analiza el texto proporcionado, reemplaza todas las palabras clave dentro de él con sus valores coincidentes y devuelve una nueva cadena.

Ahora, normalmente no trabajamos aquí con cadenas literales, sino con documentos. Querremos abrir un documento, leer las líneas dentro de él y pasarlas como una cadena a la función replace_keywords().

Note: For really long files, that might not fit into your local machine's memory - you might want to consider leer un archivo línea por línea.

En cualquier caso, carguemos un archivo de texto y ejecutemos la función replace_keywords() en el contenido:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Open the long textual document `data.txt`
with open('data.txt', 'r+') as file:
    # Load the content from `data.txt` to a variable as a string
    content = file.read()
    # Replace all desired keywords from `data.txt` and store it in the new variable
    new_content = keyword_processor.replace_keywords(content)
    # Replace the old content
    file.seek(0)
    file.truncate()
    # Write the alternated content to the original file 
    file.write(new_content)

Entonces, si alimentamos un archivo de texto como text.txt:

1
The breakfast was terrific! I really loved the eggs, you're a great cook.

Con las siguientes palabras clave y palabras de reemplazo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from flashtext import KeywordProcessor
keyword_processor = KeywordProcessor()

keyword_dictionary = {
    "good": ["terrific", "great"],
    "eggs": ["hash browns"]
}

keyword_processor.add_keywords_from_dict(keyword_dictionary)

with open('data.txt', 'r+') as file:
    content = file.read()
    new_content = keyword_processor.replace_keywords(content)
    file.seek(0)
    file.truncate()
    file.write(new_content)

Daría como resultado un archivo text.txt alterado:

1
The breakfast was good! I really loved the hash browns, you're a good cook.

Otras funcionalidades útiles del módulo FlashText

Hagamos un ficticio keyword_processor y keyword_dictionary para ilustrar algunas de las otras funcionalidades útiles del módulo FlashText:

1
2
3
4
5
6
keywordProcessor = KeywordProcessor()
keywordDictionary = {
    "bad": ["awful", "terrible", "horrible"],
    "good": ["fine", "excellent", "great"]
}
keywordProcessor.add_keywords_from_dict(keywordDictionary)

Para obtener una lista de todas las palabras clave en la instancia de KeywordProcessor, usamos la función get_all_keywords():

1
2
# List all added keywords
print(keywordProcessor.get_all_keywords())

Lo que resulta en:

1
{'awful': 'bad', 'terrible': 'bad', 'horrible': 'bad', 'fine': 'good', 'excellent': 'good', 'great': 'good'}

Para verificar si una palabra clave está presente en KeywordProcessor, podemos usar el operador in:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
'bad' in keywordProcessor
# Output: true
# keyword `bad` is PRESENT in the keywordProcessor

'red' in keywordProcessor
# Output: false
# keyword `red` is NOT PRESENT in the keywordProcessor

'awful' in keywordProcessor
# Output: false
# keyword `awful` is NOT THE KEYWORD in the keywordProcessor
# instead, it IS REPLACEMENT WORD

Y para acceder a una palabra_reemplazo basada en una cierta palabra clave:

1
2
3
4
5
6
7
8
9
keywordProcessor['fine']
# Output: 'good'

keywordProcessor['excelent']
# Output: 'good'

keywordProcessor['goood']
# Output: None
# There is no keyword `goood` in the keywordProcessor

Y finalmente, para eliminar palabras clave de un KeywordProcessor, usamos la función remove_keyword():

1
2
keyword_processor.remove_keyword('fine')
# This will remove `fine` from the keywordProcessor

Alternativamente, podemos especificar una lista o diccionario de pares de palabra clave-valor que queremos eliminar y usarlos para eliminar elementos específicos:

1
2
3
4
5
6
7
# Using a dictionary to remove keywords
keywordProcessor.remove_keywords_from_dict({"bad": ["awful", "terrible"]})
# This will remove keywords `awful` and `terrible` from the keywordProcessor

# Using a list to remove keywords
keywordProcessor.remove_keywords_from_list(["fine", "excellent"])
# This will remove keywords `fine` and `excellent` from the keywordProcessor

FlashText frente a expresiones regulares

FlashText se creó principalmente como una alternativa a las expresiones regulares, por lo que sería útil compararlas. De hecho, fue creado como respuesta a una [pregunta en StackOverflow](https://stackoverflow.com/questions/44178449/regex-replace-is-take-time-for-millions-of-documents-how-to -hazlo mas rapido/).

Al comparar la velocidad de ejecución, FlashText es el claro ganador. Se tarda aproximadamente el mismo tiempo para el mismo texto, con una pequeña y gran cantidad de palabras clave. Por otro lado, con las expresiones regulares, el tiempo de ejecución aumenta proporcionalmente a la cantidad de palabras clave que se reemplazarán.

Como autor de las notas de FlashText - para consultas grandes, las expresiones regulares pueden tardar días en ejecutarse, mientras que FlashText lo hace en 15 minutos:

replace-benchmark

[Crédito: Vikash Singh, autor de FlashText, en [FreeCodeCampamento](https://www.freecodecamp.org/news/regex-was-take-5-days-flashtext-does-it-in-15-minutes-55f04411025f /)]{.pequeña}

Los puntos de referencia han demostrado que FlashText tarda menos tiempo que RegEx en encontrar y reemplazar palabras clave cuando la cantidad de palabras clave supera las 500.

Sin embargo, cuando se trata de la coincidencia de caracteres especiales, FlashText no tiene ninguna posibilidad de vencer a las expresiones regulares. Aún más, FlashText ni siquiera tiene soporte para ese tipo de concordancia: solo puede concordar palabras clave simples sin ningún carácter especial.

La coincidencia de caracteres especiales describe las situaciones en las que algunos caracteres de la palabra tienen un significado especial y pueden coincidir con varios caracteres diferentes.

Por ejemplo, el carácter \d describe dígitos del 0 al 9, lo que significa que la palabra clave número\d coincidirá con las palabras número0, número1, número2, etc.

FlashText no está diseñado teniendo en cuenta la coincidencia de caracteres especiales, por lo que solo puede coincidir con literales de cadena iguales. La palabra number1 solo puede coincidir con la palabra clave number1 y no hay forma de usar una sola palabra clave para hacer coincidir varias palabras diferentes.

Conclusión

Como ya hemos visto, FlashText es una herramienta muy simple pero poderosa. Es bastante liviano, fácil de aprender y muy eficiente en el tiempo, sin importar la cantidad de palabras clave que se reemplacen.

Como con cualquier otra herramienta, la clave es saber cuál es el mejor escenario de caso de uso para ella. Si tiene más de 500 palabras clave para reemplazar y esas palabras clave son simples, sin ninguna coincidencia de caracteres especial, no hay razón para no usar FlashText en lugar de expresiones regulares.

Por otro lado, si tiene menos de 500 palabras clave o algún tipo de concordancia de caracteres especial, probablemente debería deshacerse de FlashText y optar por las buenas expresiones regulares dada su flexibilidad y sintaxis. optar por las buenas expresiones regulares dada su flexibilidad y sintaxis.