Visor de arriendos
Sistema que consolida a diario la oferta de arriendos de tres portales, la deduplica, la geocodifica y la publica en un visor con filtros, métricas e historial.
El problema
El requerimiento era concreto. Con mi compañero de casa decidimos mudarnos y teníamos definido lo que buscábamos: tres dormitorios (dos para dormir y uno de oficina), cerca de Parque Bustamante, Lastarria, Barrio Italia o Manuel Montt, y un tope de $800.000 al mes con gastos comunes incluidos.
Hacerlo a mano tiene varios problemas:
- Los avisos están repartidos en varios portales y el mismo departamento aparece dos o tres veces.
- Casi nadie publica los gastos comunes, así que un arriendo de $700.000 puede terminar costando $850.000.
- Unos precios vienen en UF y otros en pesos.
- Ningún buscador entiende «cerca del metro» ni «un dos dormitorios grande que sirva de tres».
Después de una semana de búsquedas manuales convertí el requerimiento en un problema de datos y construí el pipeline que lo resuelve.
Lo que encontró esta mañana
Estas cifras se leen del visor real cada vez que se publica este sitio.
Actualizado 2026-10-01 11:34
Un pipeline de punta a punta
El diseño es simple a propósito. Cada pieza hace una sola cosa y se puede correr por separado.
- 01ScrapersUn archivo por portal, todos con la misma salida.
scrapers/*.py - 02ConsolidarUne fuentes, elimina duplicados y calcula el total con gastos comunes.
consolidate.py - 03GeocodificarDirecciones a coordenadas con OpenStreetMap y caché.
geocode.py - 04EnriquecerPlaywright abre las fichas importantes para sacar gastos reales, antigüedad y mascotas.
enrich_pi.py - 05Visor estáticoHTML, CSS y JavaScript con Leaflet. Sin servidor ni base de datos.
viewer/ - 06Todos los díaslaunchd lo corre a las 10:00, guarda la foto del día y publica en GitHub Pages.
La decisión más importante fue definir un esquema común antes de escribir el primer scraper. Cada portal tiene su HTML, pero todos devuelven la misma estructura. Agregar un portal nuevo es escribir un archivo que devuelva una lista de avisos y nada más.
Scraping: cada portal es un mundo
| Portal | Técnica | Resultado |
|---|---|---|
| Portal Inmobiliario | requests + HTML | Fuente principal, por barrio |
| Chilepropiedades | requests + HTML | Rápido y completo |
| Yapo | Scrapling / Camoufox | Bloquea bots, unos 40 s por página |
| Facebook Marketplace | CSV manual | Scrapearlo viola sus términos |
Un clásico chileno: los precios llegan como «$ 750.000», «UF 13,5» o «13,5 UF». La conversión usa el valor de la UF del día desde mindicador.cl, con un respaldo si la API no responde.
def parse_precio(texto: str) -> int:
t = texto.upper().replace("\xa0", " ")
num = re.sub(r"[^\d.,]", "", t) # solo dígitos, comas y puntos
if "UF" in t: # la UF usa coma decimal
return round(float(num.replace(".", "").replace(",", ".")) * uf_hoy())
return int(num.replace(".", ""))
Entre los arriendos se colaban avisos de venta. La regla para detectarlos fue simple: ningún arriendo mensual cuesta 158 millones de pesos.
Duplicados y geocodificación
Con más de mil avisos de varias fuentes, los duplicados son inevitables. La deduplicación va en dos pasos: primero un id estable (el hash de la URL) y después una clave de dirección normalizada más precio, que atrapa el mismo aviso publicado por dos corredoras.
Para el mapa hacían falta coordenadas. Nominatim, de OpenStreetMap, es gratis pero acepta una consulta por segundo. Por eso todo pasa por un caché que solo consulta direcciones nuevas. Si una dirección no se encuentra, cae en cascada al centro del barrio y luego al de la comuna, con un pequeño desplazamiento para que los pins no se apilen. Esos avisos quedan marcados como ubicación aproximada.
Y «cerca del metro» no requirió scraping. Con las coordenadas de cada aviso y de 23 estaciones, la fórmula de haversine da la distancia. Cada tarjeta dice, por ejemplo, «Santa Isabel, 257 m».
Codificar lo que de verdad buscábamos
No bastaba con «tres dormitorios bajo $800.000». Un dos dormitorios de 68 m² o más también servía. Esa búsqueda humana se volvió una regla:
# un 2D amplio sirve como oficina y también califica
sirve = dorm >= 3 or (dorm >= 2 and m2 >= 68)
match_perfecto = sirve and total_con_gastos <= 800_000 and en_barrio_objetivo
El resultado es un embudo. Así se veía el 15 de septiembre de 2026:
Cada aviso recibe además un puntaje de 0 a 100 que pondera presupuesto, dormitorios, barrio y metro. Lo más prometedor siempre queda arriba.
¿Está caro o es una oferta?
Un precio solo es caro comparado con algo parecido. El pipeline compara cada arriendo con la mediana de su grupo, y si el grupo tiene menos de cinco avisos, sube a uno más amplio:
- Barrio+ dormitorios + baños + m²
- Barrio+ dormitorios + m²
- Comuna+ dormitorios + m²
- Comuna+ dormitorios
Un aviso 10% bajo la mediana de su grupo se marca como oferta y tiene su propia pestaña. Ahí el visor dejó de mostrar solo qué hay y empezó a mostrar qué conviene.
Actualización automática e historial
Un LaunchAgent de macOS corre todo cada mañana y hace push a GitHub Pages. Cada corrida guarda una foto completa del mercado, y con 30 días de fotos el visor sabe qué avisos son nuevos, cuáles bajaron de precio y cuáles desaparecieron (probablemente se arrendaron). Hay un selector para ver el mercado de cualquier día del último mes.
De más de mil avisos diarios a un par de docenas de candidatos concretos cada mañana.
Aprendizajes técnicos
- Esquema común primero, scrapers después. Sumar fuentes pasa a ser trivial.
- Caché en todo lo caro. Volver a correr es barato y amable con los servicios externos.
- Respaldos en cascada. El pipeline no se cae por un dato faltante.
- Salida estática. Hosting gratis y cero mantención.
- Automatizar desde el día uno. Los datos se mantienen frescos sin acordarse.
¿Lo quieres para tu búsqueda? Todos los criterios viven en config.py: cambia comunas, presupuesto y barrios, corre python run_all.py y tienes tu propio visor.