Spaces:
Running
Running
Create recolector.py
Browse files- recolector.py +135 -0
recolector.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sqlite3
|
| 2 |
+
import time
|
| 3 |
+
import random
|
| 4 |
+
import re
|
| 5 |
+
from duckduckgo_search import DDGS
|
| 6 |
+
import pandas as pd
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
|
| 9 |
+
# --- CONFIGURACIÓN DEL ROBOT ---
|
| 10 |
+
DB_NAME = "base_datos_inmobiliaria.db"
|
| 11 |
+
CIUDADES_ZONAS = {
|
| 12 |
+
"Bogota": ["Salitre", "Chico", "Chapinero", "Cedritos", "Colina Campestre", "Suba", "Kennedy", "Modelia"],
|
| 13 |
+
"Medellin": ["Poblado", "Laureles", "Envigado", "Belen", "Sabaneta"],
|
| 14 |
+
"Cali": ["Ciudad Jardin", "El Peñon", "Valle del Lili", "Granada"],
|
| 15 |
+
"Cartagena": ["Bocagrande", "Castillogrande", "Manga", "Crespo"]
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
# --- 1. GESTIÓN DE BASE DE DATOS ---
|
| 19 |
+
def iniciar_db():
|
| 20 |
+
conn = sqlite3.connect(DB_NAME)
|
| 21 |
+
c = conn.cursor()
|
| 22 |
+
# Creamos una tabla robusta para guardar histórico
|
| 23 |
+
c.execute('''CREATE TABLE IF NOT EXISTS mercado (
|
| 24 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 25 |
+
ciudad TEXT,
|
| 26 |
+
zona TEXT,
|
| 27 |
+
titulo TEXT,
|
| 28 |
+
precio REAL,
|
| 29 |
+
area REAL,
|
| 30 |
+
precio_m2 REAL,
|
| 31 |
+
url TEXT UNIQUE,
|
| 32 |
+
fuente TEXT,
|
| 33 |
+
fecha_captura DATE
|
| 34 |
+
)''')
|
| 35 |
+
conn.commit()
|
| 36 |
+
conn.close()
|
| 37 |
+
|
| 38 |
+
def guardar_dato(dato):
|
| 39 |
+
conn = sqlite3.connect(DB_NAME)
|
| 40 |
+
c = conn.cursor()
|
| 41 |
+
try:
|
| 42 |
+
# INSERT OR IGNORE evita duplicados si ya escaneamos esa URL
|
| 43 |
+
c.execute('''INSERT OR IGNORE INTO mercado
|
| 44 |
+
(ciudad, zona, titulo, precio, area, precio_m2, url, fuente, fecha_captura)
|
| 45 |
+
VALUES (?,?,?,?,?,?,?,?,?)''',
|
| 46 |
+
(dato['ciudad'], dato['zona'], dato['titulo'], dato['precio'],
|
| 47 |
+
dato['area'], dato['precio_m2'], dato['url'], dato['fuente'], datetime.now().date()))
|
| 48 |
+
conn.commit()
|
| 49 |
+
return c.rowcount # Retorna 1 si guardó, 0 si ya existía
|
| 50 |
+
except Exception as e:
|
| 51 |
+
print(f"Error guardando: {e}")
|
| 52 |
+
return 0
|
| 53 |
+
finally:
|
| 54 |
+
conn.close()
|
| 55 |
+
|
| 56 |
+
# --- 2. MOTOR DE EXTRACCIÓN (PARSEERS) ---
|
| 57 |
+
def limpiar_precio(texto):
|
| 58 |
+
match = re.search(r'\$\s?([\d.,]+)', texto)
|
| 59 |
+
if match:
|
| 60 |
+
clean = match.group(1).replace('.', '').replace(',', '')
|
| 61 |
+
try: return float(clean)
|
| 62 |
+
except: return 0
|
| 63 |
+
return 0
|
| 64 |
+
|
| 65 |
+
def limpiar_area(texto):
|
| 66 |
+
match = re.search(r'(\d+)\s?(m2|mt)', texto.lower())
|
| 67 |
+
if match:
|
| 68 |
+
try: return float(match.group(1))
|
| 69 |
+
except: return 0
|
| 70 |
+
return 0
|
| 71 |
+
|
| 72 |
+
# --- 3. EL ROBOT (CRAWLER) ---
|
| 73 |
+
def robot_inmobiliario():
|
| 74 |
+
print("🤖 INICIANDO ROBOT RECOLECTOR DE DATA INMOBILIARIA...")
|
| 75 |
+
iniciar_db()
|
| 76 |
+
|
| 77 |
+
total_nuevos = 0
|
| 78 |
+
|
| 79 |
+
# Recorremos cada ciudad y zona configurada
|
| 80 |
+
for ciudad, zonas in CIUDADES_ZONAS.items():
|
| 81 |
+
for zona in zonas:
|
| 82 |
+
print(f"\n📡 Escaneando: {ciudad} - {zona}...")
|
| 83 |
+
|
| 84 |
+
# Usamos DuckDuckGo para buscar fichas específicas sin entrar al portal (Anti-Bloqueo)
|
| 85 |
+
# Buscamos en los 3 grandes portales
|
| 86 |
+
query = f'site:fincaraiz.com.co/inmueble OR site:metrocuadrado.com/inmueble OR site:casas.mitula.com.co venta apartamento "{ciudad}" "{zona}"'
|
| 87 |
+
|
| 88 |
+
try:
|
| 89 |
+
# Usamos el backend 'api' o 'html'
|
| 90 |
+
results = DDGS().text(query, max_results=20)
|
| 91 |
+
|
| 92 |
+
if not results:
|
| 93 |
+
print(" ⚠️ No se encontraron resultados nuevos.")
|
| 94 |
+
|
| 95 |
+
count_zona = 0
|
| 96 |
+
for r in results:
|
| 97 |
+
texto = f"{r['title']} {r['body']}"
|
| 98 |
+
precio = limpiar_precio(texto)
|
| 99 |
+
area = limpiar_area(texto)
|
| 100 |
+
|
| 101 |
+
# Filtros de Calidad de Dato
|
| 102 |
+
if precio > 50000000 and area > 10:
|
| 103 |
+
dato = {
|
| 104 |
+
'ciudad': ciudad,
|
| 105 |
+
'zona': zona,
|
| 106 |
+
'titulo': r['title'],
|
| 107 |
+
'precio': precio,
|
| 108 |
+
'area': area,
|
| 109 |
+
'precio_m2': round(precio/area, 2),
|
| 110 |
+
'url': r['href'],
|
| 111 |
+
'fuente': 'Web Scraping'
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
guardado = guardar_dato(dato)
|
| 115 |
+
if guardado:
|
| 116 |
+
count_zona += 1
|
| 117 |
+
total_nuevos += 1
|
| 118 |
+
print(f" ✅ Guardado: {titulo[:30]}... (${precio:,.0f})")
|
| 119 |
+
|
| 120 |
+
print(f" 📊 Resumen {zona}: {count_zona} inmuebles nuevos agregados.")
|
| 121 |
+
|
| 122 |
+
# PAUSA ESTRATÉGICA (Dormir para parecer humano)
|
| 123 |
+
tiempo_espera = random.uniform(2, 5)
|
| 124 |
+
print(f" 💤 Durmiendo {tiempo_espera:.1f} segundos...")
|
| 125 |
+
time.sleep(tiempo_espera)
|
| 126 |
+
|
| 127 |
+
except Exception as e:
|
| 128 |
+
print(f" ❌ Error en zona {zona}: {e}")
|
| 129 |
+
time.sleep(5) # Espera de error
|
| 130 |
+
|
| 131 |
+
print(f"\n🏁 CICLO TERMINADO. Total inmuebles nuevos en base de datos: {total_nuevos}")
|
| 132 |
+
|
| 133 |
+
if __name__ == "__main__":
|
| 134 |
+
# Ejecutar el robot
|
| 135 |
+
robot_inmobiliario()
|