← Volver a proyectos

Proyecto 03 / Caso de estudio

Automatización e IA local · tres repositorios independientes

Navegar, clasificar y cubrir un país entero.

Tres herramientas de Python que se pasan el testigo y no comparten código. Una recorre la web guiada por un modelo que corre en mi equipo y entrega el ratón al humano cuando aparece un CAPTCHA. Otra convierte perfiles públicos en dos sectores distintos con reglas propias de descarte. La tercera cubre España con una retícula construida sobre el nomenclátor oficial, gastando menos de lo que el proveedor deja. Las tres alimentan el CRM del caso anterior.

20.906líneas de Python
191pruebas automáticas
0APIs de IA de pago
3fuentes, un mismo destino
RolDiseño, implementación y mantenimiento
EntregaSQLite limpio, listo para el CRM
TecnologíasPython, Playwright, Ollama, Places API, SQLite
01

El reto

Conseguir clientes nuevos es un trabajo que nadie quiere hacer: buscar negocios, abrir otra pestaña para ver si tienen presencia propia, copiar el correo a una lista, repetir. Al final del día tienes cuarenta filas y la sensación de haber perdido la jornada. Y cuando alguien automatiza esto lo hace con un guion que funciona hasta que la página cambia de diseño.

02

La solución

Separé lo que se rompe de lo que no. Cada herramienta tiene un núcleo con su lógica y una capa de arranque que solo presenta opciones. Cuando una página cambia toca un adaptador; el almacenamiento y la clasificación no se mueven. Y el criterio de qué es un cliente se escribe explícitamente, incluidas las razones por las que algo no lo es.

03

El aprendizaje

Que la pregunta correcta no es «¿uso IA?» sino «¿qué decisión estoy delegando?». En estas tres herramientas el modelo decide dos cosas pequeñas y muy caras de hacer a mano. El resto es código aburrido, y el aburrido es el que sigue funcionando sin supervisión seis meses después.

Arquitectura

Tres programas que no se hablan y un destino común.

Ninguno depende de los otros. Cada uno tiene su lanzador de doble clic, su base de datos y sus exportaciones, y todos desembocan en el CRM. Si uno deja de ser rentable, se apaga solo.

La primera es la grande: trece mil trescientas treinta y tres líneas que abren Google o Bing directamente en Chrome para generar candidatos y luego navegan con Playwright sobre un perfil persistente. El modelo es local: Ollama por su endpoint de chat compatible, con qwen2.5:7b. Cero claves de pago, cero datos que salen del equipo, coste marginal nulo por consulta.

Cuando el modelo se atasca, manda el humano. El programa detecta pantalla de consentimiento, CAPTCHA, inicio de sesión, error 429 o bloqueo directo, y en ese caso entrega la ventana de Chrome al usuario; vuelve a comprobar la página al pulsar ENTER, y acepta una S para saltar esa búsqueda. No intenta resolver por fuerza bruta lo que solo una persona puede resolver. El proxy es único y lo pongo yo: no rota direcciones para eludir bloqueos. Si un sitio dice que no, se para.

Las búsquedas de contactos, la comprobación de sesión y el extractor de perfiles comparten el mismo perfil de Chrome, así que el inicio de sesión se reutiliza. Identidad nativa del navegador instalado, idioma español, zona horaria de Madrid y seis segundos de pausa entre búsquedas. Guardar las cookies evita perder sesiones y reduce peticiones innecesariamente rápidas.

La segunda son cinco mil seiscientas sesenta y siete líneas que recorren perfiles públicos. Lo interesante no es el recorrido: es que está diseñada para que el sector sea un parámetro, no reescribir el programa. Y la tercera, mil novecientas seis líneas sin un solo modelo, es la que mejor ingeniería tiene de las tres.

AiTattooScraper búsqueda web + Chrome persistente + Ollama local InstagramTattooFinder perfiles públicos · dos sectores aislados ApiPlacesTattoo Places API (New) · retícula sobre el IGN │ └──▶ SQLite por herramienta ──▶ CSV limpios ──▶ CRM (caso 01)
  • Navegación

      PlaywrightChrome persistentePerfil compartidoCaptura y traza JSONL
  • Modelo

      Ollama localqwen2.5:7bEndpoint compatibleSin claves de pago
  • Datos

      SQLiteExportación por sectorAnti-duplicadoReanudable tras cierre
  • Calidad

      191 pruebasDoble pasadaCopia previa al borradoUso presupuestado
  • Operación

      Lanzador de doble clicEntorno virtual propioClaves sin mostrar

Criterio

Definir por qué algo no es un resultado.

Cualquier buscador devuelve cosas que parecen correctas. El trabajo está en escribir las reglas de descarte, porque son las que evitan que un humano repase cuatro mil fichas a mano.

01

El sector es configuración, no código

La herramienta de perfiles trae dos profiles aislados, Tatuadores y Motoclubs de España, cada uno con su fichero de términos de búsqueda, su base de datos y sus exportaciones propias. Los parámetros recomendados viven en un fichero .bat por perfil. Pasar del mercado del tatuaje a otro mercado fue configurar, no programar. Y en la línea de órdenes, el indicador de perfil va siempre antes del comando para que no haya ambigüedad sobre con qué datos se está trabajando.

02

Dos señales explícitas para contar como positivo

En el perfil motero el modelo exige una señal de club —motoclub, club de motos, peña o asociación motera, MC— y una señal de ubicación en España. Un motorista individual que habla de motos, una tienda, un taller, un concesionario o un evento no cuentan como positivo por mucho que hablen de motos. Es más fácil escribir una lista de palabras que una lista de rechazos; la segunda es la que ahorra trabajo de verdad.

03

Reclasificar en vez de tirar la base

Cuando afino el criterio, no empiezo de cero: la herramienta tiene una opción para volver a pasar por el modelo los datos ya recogidos y limpiarlos, otra para previsualizar qué se va a borrar y hacer una copia antes, y un modo estricto que por defecto preserva el correo que ya existía. Descarté el «borra y vuelve a recolectar»: cada ficha costó tiempo de máquina y una recolección nueva nunca devuelve exactamente lo mismo.

04

Ante la duda, no se borra

Las reglas de limpieza solo eliminan lo que tiene una categoría explícitamente ajena al sector. Si una persona ambigua no permite decidir, se queda. Del mismo modo, el correo personal de un perfil se retira de los contactos guardados y la exportación de campaña rechaza dominios inválidos o buzones que no aceptan comunicación. Un falso positivo cuesta un envío; un falso negativo cuesta una oportunidad. Son costes distintos y hay que elegirlos a propósito.

La pieza fina

Cubrir España sin que la factura te cubra a ti.

Mil novecientas seis líneas, seis pruebas y la mejor idea del catálogo. Aquí no hay modelo: hay geometría y un presupuesto.

La consulta nunca cambia; lo que cambia es el rectángulo. Concatenar la ciudad al texto —«tatuajes Madrid»— introduce el sesgo del buscador y hace que el resultado dependa de una frase. Aquí todas las celdas envían la misma consulta, tatuajes, y cada búsqueda queda restringida con locationRestriction.rectangle al perímetro exacto de su celda. Así la cobertura es completa y comparable: si una celda sale vacía, es que no hay negocios ahí, no que mi frase estaba mal escrita.

Y esto está probado. La prueba de extremo a extremo intercepta la petición, comprueba que el texto enviado es exactamente tatuajes y que el rectángulo corresponde a la celda, y luego hace la aserción que de verdad vale: si le quito el token de paginación, el cuerpo de la segunda página es idéntico al de la primera. No es una convención que espero respetar, es una igualdad que el test rompe si alguien la rompe.

La retícula no es una cuadrícula uniforme. Se construye a partir del Nomenclátor Geográfico de Municipios y Entidades de Población del CNIG: se descarga el ZIP nacional, el programa localiza solo el fichero de entidades —admite también el de municipios o una carpeta, y acepta que le arrastres el ZIP a la ventana— y genera una retícula adaptativa. Conserva núcleos con población mayor que cero, evita contar varias veces la jerarquía municipio-capital-entidad-núcleo, garantiza al menos un punto por municipio habitado, hace celdas pequeñas donde hay concentración urbana y agrupa el resto solo cuando contienen población. No se desperdician peticiones cubriendo monte, mar o polígono vacío.

El control de coste es la parte que más uso en mi trabajo diario. Las celdas crecen automáticamente hasta que el peor caso de tres páginas por celda no supera 4.500 llamadas, dejando margen frente al bloqueo de 4.800 del plan contratado. Hay un presupuesto con límite duro que lanza excepción al pasarse —y ojo, incluso con presupuesto ilimitado se sigue registrando el uso—, y la cabecera de campos devueltos se reduce al mínimo necesario para que cada llamada cueste lo menos posible.

ZIP nacional del Nomenclátor → ENTIDADES.csv → núcleos con población > 0 · sin contar dos veces la jerarquía → celda pequeña en ciudad · celda territorial agrupada en el resto → agrandar hasta que 3 páginas/celda ≤ 4.500 llamadas → misma consulta «tatuajes» + locationRestriction.rectangle por celda → presupuesto con límite duro · el uso se apunta siempre
  • 1consulta única para todo el territorio
  • 4.500llamadas de tope automático frente a 4.800
  • 3formatos de entrada aceptados para el nomenclátor
  • 2tablas separando el sitio único de cada avistamiento
  • 0celdas repetidas al relanzar la misma retícula
  • 2estrategias de celda: urbana y ocupada

Reanudar, no repetir

La misma retícula vuelta a lanzar no vuelve a buscar nada: informa de cero celdas buscadas y todas omitidas. Y el modelo de datos separa el sitio único de cada vez que se ha visto, así que una peluquería que cae en el borde de dos celdas es una ficha con dos observaciones, no dos fichas. Es la diferencia entre una base que se puede volver a lanzar cada mes y una que hay que limpiar cada mes. La prueba lo comprueba: dos pasadas dejan una semilla y dos avistamientos.

Resultados

Lo que se puede contar con el código delante.

Cifras medidas sobre los repositorios. Las pruebas están contadas una a una, no estimadas.

  • 13.333líneas en la herramienta de navegación, con 139 pruebas
  • 5.667líneas en la de perfiles, con 46 pruebas
  • 1.906líneas en la de la API oficial, con 6 pruebas
  • 6segundos de pausa deliberada entre búsquedas
  • 2sectores aislados, mismo motor
  • 0dependencias entre las tres herramientas

Lo que haría distinto

Dos de las tres fuentes dependen de que una página ajena no cambie de diseño, y esa es la parte cara de mantener. Si volviera a empezar, priorizaría APIs con licencia y dejaría la navegación asistida solo para lo que no existe en ningún sitio estructurado: la herramienta de la API oficial tiene una quinta parte del código, seis pruebas, presupuesto controlado y no se le rompe nada. La otra cosa que falta es un contrato de datos compartido entre las tres. Cada una define su esquema y hay transformaciones de costura; un esquema común con validación en la frontera habría eliminado duplicados y algún fallo silencioso. En el CRM ya lo corregí con la lista negra por entidad; en el pipeline todavía se nota. Y el límite de pruebas de la herramienta grande es otro asunto: ciento treinta y nueve pruebas para trece mil líneas suena bien hasta que ves que son un único fichero.

Límites

Cómo se usa esto, por escrito.

No es una coletilla legal. Son decisiones de diseño que se pueden comprobar en el código.

Solo datos públicos, a ritmo deliberadamente lento, con sesión propia y sin interacciones automáticas: nada de dar like, comentar o enviar mensajes por ti. Un único proxy, sin rotación, y respeto explícito del bloqueo: si la plataforma dice que no, el programa se para o se lo pasa a una persona. Las capturas y trazas existen para reproducir un error, no para archivar a nadie.

Los correos obtenidos son datos personales y su tratamiento está sujeto al RGPD. La base de datos no se publica ni se vende: forma parte de cartera confidencial, el código es privado y se puede revisar en conversación con quien lo pida.