← Ruta Perico
Etapa 06

Limpieza y deduplicación

2ª categoría Publicada ~30 min
06

El scraper dejó 137 mensajes de foro. Dentro de esos mensajes hay frases de Perico, pero mezcladas con la conversación de los usuarios. Toca separarlas.

Es la primera vez que pasamos de texto crudo a frases, y el paso donde más volumen se pierde.

Mira los datos antes de escribir el filtro

La tentación es imaginarse el patrón. Mejor contarlo:

python
import collections, re

counts = collections.Counter()
for message in messages:
    body = message["text"]
    counts['comillas rectas "'] += body.count('"') // 2
    counts["comillas tipográficas"] += len(re.findall(r"[“”]", body)) // 2
    counts["guion de diálogo"] += len(re.findall(r"^\s*[-–—]\s+\S", body, re.M))
    counts["línea suelta corta"] += sum(
        1 for line in body.split("\n") if 3 <= len(line.split()) <= 20
    )
SeñalOcurrenciasFiabilidad
Línea suelta corta358Ruidosa: también hay prosa normal
Comillas rectas "…"169Marcador explícito
Guion de diálogo1Irrelevante
Comillas tipográficas0No se usan aquí

Las comillas ganan: el forero decidió marcar eso como cita. Las líneas sueltas son un accidente de formato.

Contar antes de filtrar

Cinco minutos de recuento evitan escribir un extractor para un patrón que no existe — como las comillas tipográficas, que aquí salen a cero. Y revelan cuál de las señales candidatas es la buena, en vez de elegirla por intuición.

Lo que las reglas mecánicas sí pueden hacer

Tres criterios que no necesitan entender el contenido:

ReglaQué elimina
Mínimo 3 palabras"novias", "Pietro", "traducciones"
Máximo 40 palabrasPárrafos citados enteros
Deduplicar por hashLa misma frase repetida en varios mensajes

Deduplicar es quedarse con una sola copia de cada frase que aparece repetida. El hash es lo que lo hace barato: una función que convierte un texto de cualquier longitud en una cadena corta, siempre la misma para la misma entrada. Comparar frases pasa a ser comparar cadenas cortas, y detectar la repetición número mil cuesta igual que la primera. Es lo contrario del embedding de la etapa 0 — el embedding acerca lo que se parece, el hash solo empareja lo idéntico.

clean_forum.py
# Text between straight double quotes. The [^"] stops a stray closing quote
# from swallowing half the message while it hunts for the next one.
QUOTED = re.compile(r'"([^"]{4,300})"')

MIN_WORDS = 3
MAX_WORDS = 40

Ese [^"] importa. Con .*? un mensaje con comillas desparejadas se traga texto hasta encontrar la siguiente, y acabas con "frases" de doscientas palabras.

Normalizar antes de comparar

Para el ordenador, estas dos cadenas son distintas:

python
"Y yo qué sé, tío"   !=   "y yo que se, tio"

Para nosotros son la misma frase, y para el hash no. Normalizar es reducir todas las maneras de escribir lo mismo a una única forma canónica y comparar sobre ella; es lo que hace que deduplicar sirva de algo:

clean_forum.py
import unicodedata

def normalize(text):
    """Comparison key: lowercase, no accents, no punctuation.

    Used only to spot duplicates. What gets stored is always the original.
    """
    text = text.lower()
    # NFD splits a letter from its accent, then the loose marks are dropped
    text = unicodedata.normalize("NFD", text)
    text = "".join(c for c in text if unicodedata.category(c) != "Mn")
    text = re.sub(r"[^\w\s]", " ", text)
    return re.sub(r"\s+", " ", text).strip()

El truco de los acentos merece explicación. NFD descompone é en dos caracteres: la e y una marca de acento suelta. Esa marca pertenece a la categoría Unicode Mn (mark, nonspacing), así que filtrarla deja la letra base. Funciona para cualquier alfabeto, sin tablas de reemplazo a mano.

La clave normalizada nunca se guarda

Sirve solo para comparar. Lo que se almacena es siempre el texto original, con sus acentos y su puntuación — es lo que el modelo va a imitar más adelante.

Resultado, y una sorpresa

terminal
137 messages read
63 unique candidates  (40 duplicates dropped)

De 169 fragmentos a 63. Las reglas quitaron 40 duplicados y 66 por longitud.

Al ordenar por número de repeticiones esperando ver las frases más famosas arriba, salió lo contrario:

las más repetidas
x9  "Sabeis por que los galgos corren tanto? Porque son perricos delgados"
x7  "amar sin ser amado es como lavarse el culo sin haber cagado"
x7  "los locos de las cumbres"

Un chiste, una firma de usuario y un rango del foro. Las tres más repetidas son las tres peores.

Tiene explicación: las firmas se repiten en cada mensaje de ese usuario, y los chistes se recitan entre foreros. Las frases buenas aparecen una vez, porque alguien las oyó en la tele y las apuntó.

Frecuencia ≠ importancia

Cuando la repetición viene de boilerplate, ordenar por frecuencia te da justo la basura. Aquí, coger el top-10 por repeticiones habría llenado el corpus de firmas de foro.

La cuenta de repeticiones sigue mereciendo la pena guardarla — pero como dato, no como criterio de calidad.

Y aquí se acaban las reglas

Entre las 63 supervivientes quedan joyas:

«para su suerte, no hay viento. Se ve en los rabos de las vacas, que no se mueven»
«Ahí hizo un TOTIESO Laurent Fignon»
«Sí, ha atacado, pero hacia atrás»

Metáforas del campo, palabras inventadas, chascarrillos

Y sigue quedando ruido:

«amar sin ser amado es como lavarse el culo sin haber cagado»
«los locos/as de las cumbres»

Firma de un usuario y un rango del foro

Ninguna regla mecánica distingue esos dos grupos. Las dos cosas son texto español entrecomillado de longitud parecida. Hace falta criterio, y eso es la etapa siguiente.

Lo importante: capturar y limpiar son fases distintas

El scraper guardó los mensajes enteros y crudos. Este paso lee ese fichero y escribe otro; no vuelve a tocar la red.

Eso significa que cambiar de idea sobre qué cuenta como frase — bajar el mínimo a 2 palabras, aceptar guiones de diálogo, ampliar el máximo — cuesta un segundo de ejecución, no otra ronda de scraping. Y en este proyecto cambiamos de idea al menos tres veces.

Regla general

Nunca filtres durante la captura. Lo que descartas al capturar lo pierdes para siempre; lo que descartas al limpiar se recupera reejecutando. La captura es cara e irrepetible (las fuentes se mueren), el procesado es barato e infinito.