spanlp
spanlp es una librería escrita en Python para detectar, censurar y limpiar groserías,
vulgaridades, palabras de odio, racismo, xenofobia y bullying en textos escritos en Español.
## Indice
- [Indice](#indice)
- [Sobre la librería](#sobre-la-librería)
- [Casos de uso](#casos-de-uso)
- [Status Desarrollo](#status-desarrollo)
- [Instalación](#instalación)
- [Funcionamiento](#funcionamiento)
- [Uso básico](#uso-basico)
- [Uso avanzado](#uso-avanzado)
- [Pre-procesamiento de datos](#preprocesamiento-de-texto)
- [Testing](#testing)
- [Reportar un bug](#reportar-un-bug)
- [Contribuidorxs](#contribuidorxs)
- [Contacto](#contacto)
## Sobre la librería
spanlp es una librería escrita en Python para detección de groserías, vulgaridades, palabras de odio, racismo, xenofobia y bullying en textos escritos en Español. Puedes usar la librería y aplicarla a palabras de cualquiera de los más de **20 paises** de habla hispana.
Incluye:
1. Argentina 🇦🇷
2. Bolivia 🇧🇴
3. Chile 🇨🇱
4. Colombia 🇨🇴
5. Costa Rica 🇨🇷
6. Cuba 🇨🇺
7. Ecuador 🇪🇨
8. El Salvador 🇸🇻
9. España 🇪🇸
10. Guatemala 🇬🇹
11. Guinea Ecuatorial 🇬🇶
12. Honduras 🇭🇳
13. México 🇲🇽
14. Nicaragua 🇳🇮
15. Panamá 🇵🇦
16. Paraguay 🇵🇾
17. Perú 🇵🇪
18. Puerto Rico 🇵🇷
19. República Dominicana 🇩🇴
20. Uruguay 🇺🇾
21. Venezuela 🇻🇪
## Casos de uso
* Censurar vulgaridades en un texto.
* Detectar y censurar vulgaridades en una sala de chat en linea.
* Encontrar y censurar frases y palabras de odio, racismo, xenofia, bullying. (Se deben incluir como parámetros)
* Censurar comentarios groseros o insultos en algún blog o aplicación web o sitio web.
* Censurar malas palabras en un sistema de recolección de opiniones, sugerencias, quejas y reclamos.
* Limpiar textos antes de ser publicados.
* Detectar y eliminar vulgaridades en textos que serán leidos y/o vistos por niños.
* Limpiar una base de datos con mucho texto.
## Status Desarrollo
| Funcionalidad | Desarrollo | Pruebas | Release |
|-----------------------------------|------------|-------------|-----------
| Soporte de tokens con números | ✓ | ✓ | v0.0.5 |
| Estrategias de limpieza de datos | ✓ | ✓ | v0.0.5 |
| Completar dataset | ✓ | ✓ | v1.0.1 |
| Hamming | ✓ | ✓ | v1.0.2 |
| Levenstein | ✓ | ✓ | v1.1.0 |
| Bag distance | - | | - |
| Sorensen-Dice coefficient | - | | - |
| Tversky index | - | | - |
| Overlap index | - | | - |
| Tanimoto distance | - | | - |
| Ampliación datasets | Progreso | - | - |
| % de palabrotas en el texto | - | - | - |
## Instalación
Para instalar la última versión use:
```console
pip install spanlp
```
Para instalar una versión específica use (por ejemplo):
```
pip install spanlp==1.1.0
```
## Funcionamiento
Los algoritmos y modulos se personalizan de forma dinámica y muy flexible. Veamos algunos usos.
### Uso básico
Validar si una palabra o frase contiene o no una palabrota:
```python
from spanlp.palabrota import Palabrota
palabrota = Palabrota()
print(palabrota.contains_palabrota("Hola huevon cómo está?"))
# salida: True
```
```python
from spanlp.palabrota import Palabrota
palabrota = Palabrota()
print(palabrota.contains_palabrota("Hola a todos ¿cómo están?"))
# salida: False
```
Censurar una frase con los parámetros por defecto:
```python
from spanlp.palabrota import Palabrota
palabrota = Palabrota()
print(palabrota.censor("Hola huevon cómo está?"))
# salida: Hola !$%#@! cómo está?
```
Censurar la misma frase, configurando carácteres propios
```python
from spanlp.palabrota import Palabrota
palabrota = Palabrota(censor_char="*")
print(palabrota.censor("Hola huevon como está?"))
# salida: Hola ****** cómo está?
```
Censurar otra frase, configurando carácteres propios y país
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
palabrota = Palabrota(censor_char="@", countries=[Country.COLOMBIA, Country.VENEZUELA])
print(palabrota.censor("Hola huevon marico cómo está?"))
# salida: Hola @@@@@@ @@@@@@ cómo está?
```
Censuremos la misma frase pero solo con el país de Venezuela
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
palabrota = Palabrota(censor_char="@", countries=[Country.VENEZUELA])
print(palabrota.censor("Hola huevon marico cómo está?"))
# salida: Hola huevon @@@@@@ cómo está?
```
Censuremos la misma frase pero incluyendo "huevon" al vocabulario de Venezuela
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
palabrota = Palabrota(censor_char="@", countries=[Country.VENEZUELA], include=["huevon"])
print(palabrota.censor("Hola huevon marico cómo está?"))
# salida: Hola @@@@@@ @@@@@@ cómo está?
```
Censuremos la misma frase incluyendo "huevon" al vocabulario de Venezuela y excluyendo "marico"
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
palabrota = Palabrota(censor_char="@", countries=[Country.VENEZUELA], include=["huevon"], exclude=["marico"])
print(palabrota.censor("Hola huevon marico cómo está?"))
# salida: Hola huevon marico cómo está?
```
Censuremos la misma frase incluyendo ""Hola" y "huevon" al vocabulario de Venezuela y excluyendo "marico"
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
palabrota = Palabrota(censor_char="-", countries=[Country.VENEZUELA], include=["huevon"], exclude=["marico"])
print(palabrota.censor("Hola huevon marico cómo está?"))
# salida: ---- ---- marico cómo está?
```
### Uso Avanzado
El uso avanzado incluye usar metricas de distancia y similitud para encontrar, comparar, censurar palabras.
Estas son las metricas usadas a la fecha:
1. [ES-Indice de Jaccard](https://es.wikipedia.org/wiki/%C3%8Dndice_Jaccard) ([EN-Jaccard Index](https://en.wikipedia.org/wiki/Jaccard_index))
2. [ES-Similitud del coseno](https://es.wikipedia.org/wiki/Similitud_coseno) ([EN-Cosine Similarity](https://en.wikipedia.org/wiki/Cosine_similarity))
3. [ES-Levenshtein](https://es.wikipedia.org/wiki/Distancia_de_Levenshtein)([EN-Levenshtein](https://en.wikipedia.org/wiki/Levenshtein_distance))
4. [ES-Damerau-Levenshtein](https://es.wikipedia.org/wiki/Distancia_de_Damerau-Levenshtein)([EN-Damerau-Levenshtein](https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance))
Censuremos la frase usando Cosine Similarity
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import CosineSimilarity
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=CosineSimilarity())
print(palabrota.censor("Hola huevo maric cómo está?"))
# salida: Hola huevo ***** cómo está?
```
A pesar de que "maric" no está en el dataset, al algoritmo la censuró dado que por la métrica de distancia es muy similar.
Censuremos la frase usando **Cosine Similarity** manipulando los parámetros
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import CosineSimilarity
# Indicamos que tenga en cuenta palabras similares en al menos 30% y que normalice los datos (poner en minusculas, remover acentos)
cosine = CosineSimilarity(0.9, normalize=True)
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=cosine)
print(palabrota.censor("Hola huevon MARIC cómo está?"))
# salida: hola huevon ***** **** esta? => Censuró "como" porque en el dataset está "cono" y son similares en más del 90%
```
Censuremos la frase usando **JaccardIndex** con los parámetros por defecto
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import JaccardIndex
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=JaccardIndex())
print(palabrota.censor("Hola huevo maric cómo está?"))
# salida: Hola huevo ***** cómo está?
```
El indice de Jaccard usa por defecto los siguientes parámetros:
* `threshold=0.8` - Indica que censurará palabras con una similitud del 80% o más.
* `normalize=False` - False indica que no pasará el texto a minuscula y no removerá acentos.
* `n_gram=2` - Usa 2 subsecuencias de la palabra. (Ver [N-grama](https://es.wikipedia.org/wiki/N-grama))
Censuremos la frase usando **JaccardIndex** y modifiquemos los parámetros
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import JaccardIndex
jaccard = JaccardIndex(threshold=0.9, normalize=True, n_gram=1)
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=jaccard)
print(palabrota.censor("Hola huevon marica cómo vamos?"))
# salida: hola huevon ****** **** vamos?
```
Ahora, sin normalizar los datos
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import JaccardIndex
jaccard = JaccardIndex(threshold=0.9, normalize=False, n_gram=1)
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=jaccard)
print(palabrota.censor("Hola huevon marica cómo vamos?"))
# salida: Hola huevon ****** cómo vamos? => "moco" y "cómo" ahora son muy diferentes.
```
Ahora, bajemos el `threshold` a `0.6`
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import JaccardIndex
jaccard = JaccardIndex(threshold=0.6, normalize=False, n_gram=1)
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=jaccard)
print(palabrota.censor("Hola huevon marica cómo vamos?"))
# salida: Hola ****** ****** cómo vamos? => Ha censurado una palabra más.
```
Aumentemos la cantidad de `n_gram` a `3` con el `threshold=0.7` y con `normalize=False`
```python
from spanlp.palabrota import Palabrota
from spanlp.domain.countries import Country
from spanlp.domain.strategies import JaccardIndex
jaccard = JaccardIndex(threshold=0.7, normalize=False, n_gram=3)
palabrota = Palabrota(censor_char="*", countries=[Country.VENEZUELA], distance_metric=jaccard)
print(palabrota.censor("Hola huevon marica cómo vamos?"))
# salida: Hola huevon marica cómo vamos? => No censuró nada.
```
## Preprocesamiento de texto
Siempre será necesario limpiar los datos antes de empezar a trabajar. Aqui te presento la clase `Preprocessing` y las 28 estratégias de limpieza de datos.
¿Cuantas estrategias hay y cuales son?
Son 28 algoritmos y son:
1. `TextToLower` **input**: "HOLA QUE MÁS?" **output:** "hola que más?"
2. `TextToUpper` **input:** "hola ¿cómo están?" **output:** "HOLA ¿CÓMO ESTÁN?"
3. `RemoveUnicodeCharacters` **input:** "hola çcomo ªvan?" **output:** "hola como van" ([Unicode characters](https://www.rapidtables.com/code/text/unicode-characters.html))
4. `NumbersToVowelsInLowerCase` **input:** "h0l4 qu3 t4l?" **ouput:** "hola que tal?"
5. `NumbersToVowelsInUpperCase` **input:** "H0l4 c0m0 v4n?" **output:** "HOlA cOmO vAn?"
6. `NumbersToConsonantsInLowerCase` **input:** "C0m0 e574n? 8i3n?" **output:** "Como estan? bien?"
7. `NumbersToConsonantsInUpperCase` **input:** "C0m0 e574n? 8i3n?" **output:** "COMO ESTAN? BIEN?"
8. `RemoveExtraSpaces` **input:** "Hola como están? " **output:** "Hola como están?"
9. `RemoveUserMentions` **input:** "Hola @channel como van?" **output:** "Hola como van?"
10. `RemoveUrls` **input:** "Revisen este recurso https://github.com/jfreddypuentes/spanlp" **output:** "Revisen este recurso "
11. `RemoveHashtags` **input:** "Hola #equipo bienvenidos" **output:** "Hola bienvenidos"
12. `RemoveTicks` **input:** "Hola, que' más'" **output:** "Hola, que más"
13. `RemoveBackTicks` **input:** "Hola, que` más`" **output:** "Hola, que más"
14. `RemovePunctuation` **input:** "Mensaje...,con, puntos" **output:** "Mensaje con puntos"
15. `RemoveNumbers` **input:** "Hay 12 patacones y 20 yucas" **output:** "Hay patacones y yucas"
16. `RemoveAccents` **input:** "La canción es una sensación" **output:** "La cancion es una sensacion"
17. `RemoveStopWords` **input:** "La canción y la letra es buena" **output:** "canción letra buena"
18. `RemoveArticles` **input:** "La canción y la letra es buena" **output:** "canción y letra es buena"
19. `RemoveEmoticons` **input:** "Hola ;) como estás? XD" **output:** "Hola como estás?"
20. `RemovePronouns` **input:** "Yo pienso que ella debería ser como él" **output:** "pienso que debería ser como"
21. `RemoveAdverbs` **input:** "muchos años despues frente al peloton de fusilamiento lentamente recordaba..." **output:** "muchos años frente al peloton de fusilamiento recordaba..."
22. `RemoveConjunctions` **input:** "y entonces estaba programando aunque con sueño pero concentrado creando esta libreria" **output:** "entonces estaba programando con sueño concentrado creando esta libreria"
23. `RemovePrepositions` **input:** "ante todo es mejor cuidar a la naturaleza mediante buenas acciones. entre todos podemos." **output:** "todo es mejor cuidar la naturaleza buenas acciones. todos podemos."
24. `RemoveAdjectives` **input:** "la voz era tenebrosa y la noche estaba fria y oscura hasta que de pronto algo luminoso apareció y" **output:** "la voz era y la noche estaba y hasta que de pronto algo apareció y"
25. `RemoveHtmlTags` **input:** "Hola USUARIO que tal?" **output:** "Hola USUARIO que tal?"
26. `RemoveEmailAddress` **input:** "Hola Pepito, el correo es contacto@domain.com" **output:** "Hola Pepito, el correo es "
27. `ExpandAbbreviations` **input:** "pero xq tengo es3 si yo estaba bn en clase, ahora me duelen to2 los musculos" **output:** "pero por que tengo estres si yo estaba bien en clase, ahora me duelen todos los musculos"
28. `RemoveAbbreviations` **input:** "xfa pongase el tapabocas pq me da es3 verlo sin eso. to2 debemos cuidarnos. chas gracias. salu2" **output:** "pongase el tapabocas me da verlo sin eso. debemos cuidarnos. gracias."
La nueva clase `Preprocessing` implementa de manera flexible y dinámica cualquier estrategia de limpieza de una manera muy simple. Se puede aplicar dentro de una metrica de distancia como `JaccardIndex` o `CosineSimilarity` para darle más poder a la busqueda, dismunir el riesgo de no encontrar las palabras a censurar y aumentar la posibilidad de censurar las palabras que son por el hecho de estar limpias.
Veamos algunos ejemplos:
```python
from spanlp.domain.strategies import Preprocessing, TextToLower
strategies = [TextToLower()] # Defino mis estrategias de limpieza o pre-procesamiento
data = "ESTARE EN MINUSCULA" # Tengo mis datos
result = Preprocessing(data=data, clean_strategies=strategies).clean() # Invoco a Preprocessing
print(result)
# salida: estare en minuscula
```
Tambien se puede así:
```python
from spanlp.domain.strategies import Preprocessing, TextToLower
strategies = [TextToLower()]
data = "ESTARE EN MINUSCULA"
result = Preprocessing().clean(data=data, clean_strategies=strategies) # Envio los datos y las estragias al clean()
print(result)
# salida: estare en minuscula
```
Y tambien así:
```python
from spanlp.domain.strategies import Preprocessing, TextToLower
strategies = [TextToLower()]
data = "ESTARE EN MINUSCULA"
preprocessor = Preprocessing(data=data, clean_strategies=strategies)
result = preprocessor.clean()
print(result)
# salida: estare en minuscula
```
Y así:
```python
from spanlp.domain.strategies import Preprocessing, TextToLower
result = Preprocessing(data="ESTARE EN MINUSCULA", clean_strategies=[TextToLower()]).clean()
print(result)
# salida: estare en minuscula
```
**¿Cómo aplico una estrategia para que pre-preprocese mis datos dentro de la métrica de distancia?**
Aplicar la metrica de distancia con una estrategia de pre-procesado:
```python
from spanlp.domain.strategies import JaccardIndex, TextToLower
strategies = [TextToLower()]
jaccard_index = JaccardIndex(normalize=True, clean_strategies=strategies)
result = jaccard_index.calculate("HOLA", "hola")
print(result)
# salida: 1
```
Usar la métrica como interface para ejecutar la estrategia:
```python
from spanlp.domain.strategies import JaccardIndex, TextToLower
strategies = [TextToLower()]
jaccard_index = JaccardIndex(normalize=True, clean_strategies=strategies)
result = jaccard_index.normalize("HOLA ME VOY A NORMALIZAR A MINUSCULA")
print(result)
# salida: hola me voy a normalizar a minuscula
```
**Usar varias estrategias de limpieza de datos**
Nota: Las estrategias se ejecutan en el orden enviado en la lista. Recomiendo analizar primero como desea que funcione: Si primero elimina espacios extras y despues elimina signos de puntuación y luego stop words etc.. esto dependerá de su necesidad concreta; Por lo que se pueden obtener diferentes resultados si cambia el orden de las estrategias.
Enviando varias estrategias para limpiar mis datos:
```python
from spanlp.domain.strategies import Preprocessing, TextToLower, RemoveUserMentions, RemovePunctuation
strategies = [RemoveUserMentions(), TextToLower(), RemovePunctuation()]
tweet = "Hola @jhon, si viste que @freddy va a lanzar una nueva libreria Python para NLP?"
cleaned = Preprocessing().clean(data=tweet, clean_strategies=strategies)
print(cleaned)
#salida: hola si viste que va a lanzar una nueva libreria python para nlp
```
**Limpiemos emoticones, pronombres y pasemos a minuscula**
```python
from spanlp.domain.strategies import Preprocessing, RemoveEmoticons, TextToLower, RemovePronouns
strategies = [RemoveEmoticons(), TextToLower(), RemovePronouns()]
message = "Segun ella Los emoticones <3 :) :D ;) son muy usados y esta rosa tambien @}->--"
cleaned = Preprocessing().clean(data=message, clean_strategies=strategies)
print(cleaned)
# salida: segun los emoticones son muy usados y esta rosa tambien
```
## Testing
¿Eres tester? ¿quieres automatizar pruebas? o ¿simplemente aprender del open source y de las pruebas? Aventurate ya y ayudame a mejorar este proyecto!
A continuación encontrarás algunas pautas para implementar pruebas exitosas que permitarán encontrar posibles errores y mejoras.
### 1. ¿Por donde empezar?
* Instalar python.
* Instalar la librería. Esto lo logras ejecutando en la terminal el comando: `pip install -i https://test.pypi.org/simple/ spanlp`
* Abre un nuevo script de python, importa la librería y empieza a experimentar.
### 2. ¿Qué tipos de pruebas puedo realizar?
Excelente pregunta! no hay limite para la creatividad; Así que haz todas las pruebas que quieras e imagines. Sin embargo, aquí te dejo algunas pautas:
* **Pruebas de caja negra** => Una vez instales la librería y hayas leido la documentación; Trata de usarla sin preocuparte como funciona o como obtuvo el resultado (Las pruebas no se hacen en base al código, sino a la interfaz); Eso si, valida que la salida o el resultado sea el que esperas. Te puedes guiar (pero no mucho) de las pruebas unitarias que están en: `/spanlp/tests/test_palabrota.py`
* **Pruebas de caja blanca** => Si quieres vez como funciona la libreria, estas pruebas son las tuyas. Intenta entender la estructura, los algoritmos y flujos. Una vez los comprendas, "a dar palo", trata de quebar o romper la lógica, observa y encuentra si hay posibles formas de hacer que falle, prueba diferentes parametros, flujos, tipos de datos. Haz que falle, haz que se ponga lento! y me cuentas. ;)
* **Pruebas de carga y stress** => Si lo tuyo son las pruebas de requisitos no funcionales, te invito a que sigas estos pasos para estresar la libreria y medir que tan escalable es frente a procesos de alta carga, mide la velocidad y que tan bien se comparta con miles o millones de hilos usandola al tiempo.
Sigue estos pasos:
1. Crear una API Rest súper simple. Crea el siguiente script y llámalo `server.py`
```console
pip install flask
```
```python
from flask_cors import CORS, cross_origin
from flask import request
from flask import json
from spanlp.palabrota import Palabrota
app = Flask(__name__)
cors = CORS(app)
@app.route('/api/v1/nlp/text/censor', methods = ['POST'])
@cross_origin()
def censor():
try:
body = request.json
in_message = body['message']
if in_message and len(str(in_message).strip()):
palabrota = Palabrota()
censored = palabrota.censor(in_message)
response = {
"success": True,
"message": "OK",
"error_code": 0,
"data": {
'message': in_message,
'censored': censored
}
}
return response
else:
return {
"success": False,
"message": "Bad Request - message is required",
"error_code": 400,
"data": {}
}
except Exception as e:
return {
"success": False,
"message": "Internal Server Error - "+str(e),
"error_code": 500,
"data": {}
}
if __name__ == '__main__':
app.run(host='127.0.0.1', port=8080, debug=True)
```
```
python server.py
```
y listo!!
2. Consume la API desde algún cliente http como [Postman](https://www.postman.com/), [Insomnia](https://insomnia.rest/download/) y/o [SOAP UI](https://www.soapui.org/). Tambien puedes usar alguna herramienta como [Apache JMeter](https://jmeter.apache.org/); o tambien puedes crear un script python con muchos hilos que consuma la API.
### 3. ¿quieres escibrir unit tests y ejecutarlo de forma automática?
Clona el repositorio en tu máquina, abre el proyecto en tu editor favorito, ve al archivo `/spanlp/tests/test_palabrota.py` y empieza a escribir tus propios tests unitarios. Escribe cuantos quieras.
Una vez tengas los tests listos, ejecuta el siguiente comando en la raiz del proyecto (`/spanlp/`):
```console
pytest -ra
```
o tambien el comando:
```
python -m pytest
```
Esto ejecutará de forma automática todas las pruebas programadas e indicará si algún test falló.
## Reportar un bug
Si encuentras algún problema (por muy mínimo que sea) reportalo [aquí](https://github.com/jfreddypuentes/spanlp/issues/new). Solo necesitarás poner título y describir la falla y aportará un montón a que este proyecto mejore su calidad.
## Contacto
¿Alguna duda? escribeme por email: [jfredypuentes@gmail.com](mailto:jfredypuentes@gmail.com)
Cuentame en ([@jfreddypuentes](https://twitter.com/jfreddypuentes)) ¿qué te parece está librería? ¿cómo la estás usando? ¿Qué le mejorarías?
## Contribuidorxs
* [@vivianamarquez](https://github.com/vivianamarquez) en Github, [@vivmarquez](https://twitter.com/vivmarquez) en Twitter (Data Scientist) - Contribución al diseño y funcionalidades de la librería.
* [@normacalamartinez](https://github.com/normacalamartinez) en Github (BI Developer) - Contribución al dataset de vulgaridades por país de habla hispana.
**Hecho con ❤️️ de Colombia para el mundo.**