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:
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ñal | Ocurrencias | Fiabilidad |
|---|---|---|
| Línea suelta corta | 358 | Ruidosa: también hay prosa normal |
Comillas rectas "…" | 169 | Marcador explícito |
| Guion de diálogo | 1 | Irrelevante |
| Comillas tipográficas | 0 | No se usan aquí |
Las comillas ganan: el forero decidió marcar eso como cita. Las líneas sueltas son un accidente de formato.
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:
| Regla | Qué elimina |
|---|---|
| Mínimo 3 palabras | "novias", "Pietro", "traducciones" |
| Máximo 40 palabras | Párrafos citados enteros |
| Deduplicar por hash | La 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.
# 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 = 40Ese [^"] 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:
"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:
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.
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
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:
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ó.
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»
Y sigue quedando ruido:
«amar sin ser amado es como lavarse el culo sin haber cagado»
«los locos/as de las cumbres»
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.
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.