jcalbornoz commited on
Commit
5dc688e
·
verified ·
1 Parent(s): 962d809

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +61 -55
app.py CHANGED
@@ -13,9 +13,11 @@ try:
13
  subprocess.run(["playwright", "install", "chromium"], check=True)
14
  except: pass
15
 
16
- # --- GENERADOR DE URLS (Tu lógica exacta) ---
17
  def construir_urls(zona, ciudad, tipo, hab, ban, park, antiguedad):
18
- # Mapeo Antigüedad
 
 
19
  mapa_ant = {
20
  "Menos de 1 año": "de-0-a-1-anos",
21
  "1 a 8 años": "de-1-a-8-anios",
@@ -25,22 +27,23 @@ def construir_urls(zona, ciudad, tipo, hab, ban, park, antiguedad):
25
  }
26
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anios")
27
 
28
- z_slug = zona.lower().replace(" ", "-")
29
- c_slug = ciudad.lower().replace(" ", "-")
30
 
31
- # 1. FINCA RAÍZ
32
- # https://www.fincaraiz.com.co/arriendo/suba/bogota/3-o-mas-habitaciones/2-o-mas-banos/2-parqueaderos/de-1-a-8-anios
33
- url_fr = f"https://www.fincaraiz.com.co/arriendo/{z_slug}/{c_slug}/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{int(park)}-parqueaderos/{slug_ant}?&searchstring={z_slug}-{c_slug}"
 
 
34
 
35
- # 2. METROCUADRADO
36
- # https://www.metrocuadrado.com/apartamento-casa-oficina/arriendo/bogota/3-banos-3-habitaciones/?search=form
37
- # Nota: MC no suele usar el barrio en la URL de filtros avanzados, filtra post-búsqueda, pero intentaremos inyectarlo si funciona
38
  url_mc = f"https://www.metrocuadrado.com/{tipo.lower()}-casa-oficina/arriendo/{c_slug}/{int(ban)}-banos-{int(hab)}-habitaciones/?search=form"
39
 
40
  return url_fr, url_mc
41
 
42
- # --- MOTOR CON SELECTORES ESPECÍFICOS ---
43
- def motor_tramitia_clases(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
44
  resultados = []
45
  log = []
46
 
@@ -54,46 +57,44 @@ def motor_tramitia_clases(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
54
  user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
55
  )
56
 
57
- # --- 1. FINCA RAÍZ (Selector: lc-dataWrapper) ---
58
  try:
59
  page = context.new_page()
60
  page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
61
 
62
- # Scroll progresivo para cargar elementos lazy
63
  for _ in range(3):
64
  page.mouse.wheel(0, 1000)
65
- time.sleep(1)
66
 
67
- # Buscamos EXACTAMENTE el contenedor que nos diste
68
- wrappers = page.query_selector_all("div.lc-dataWrapper")
 
69
 
70
  for wrapper in wrappers[:6]:
71
  try:
72
  texto = wrapper.inner_text()
73
-
74
- # Filtro de seguridad: ignorar si no tiene precio
75
  if "$" not in texto: continue
76
 
77
- # Extracción de Precio
78
  precios = [int(s) for s in texto.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
79
  if not precios: continue
80
 
81
- # JS Injection: Usamos 'closest' para hallar el link padre <a>
82
  href = wrapper.evaluate("el => el.closest('a') ? el.closest('a').href : ''")
83
 
84
  resultados.append({
85
  "Portal": "Finca Raiz",
86
  "Precio": max(precios),
87
  "Precio_M2": max(precios) / area,
88
- "Descripcion": texto.split('\n')[0][:80],
89
  "URL": href if href else url_fr
90
  })
91
  except: continue
92
  page.close()
93
  except Exception as e:
94
- log.append(f" Error FR: {str(e)}")
95
 
96
- # --- 2. METROCUADRADO (Selector: property-card__content) ---
97
  try:
98
  page = context.new_page()
99
  page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
@@ -101,45 +102,50 @@ def motor_tramitia_clases(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
101
  for _ in range(4):
102
  page.mouse.wheel(0, 800)
103
  time.sleep(1)
104
-
105
- # Buscamos EXACTAMENTE el contenedor de MC
106
- cards_mc = page.query_selector_all("div.property-card__content")
107
 
108
- for card in cards_mc[:6]:
 
 
 
109
  try:
110
- texto = card.inner_text()
111
 
112
- # Validar si corresponde a la zona (MC mezcla zonas a veces)
113
- if zona.lower() not in texto.lower() and ciudad.lower() not in texto.lower():
114
- continue
115
-
116
  precios = [int(s) for s in texto.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
117
  if not precios: continue
118
 
119
- # JS Injection: Buscar link en ancestros o hijos
120
- # En MC a veces el link envuelve todo o está en un h2
121
- href = card.evaluate("""el => {
122
- let link = el.closest('a');
123
- if (!link) link = el.querySelector('a');
124
- return link ? link.href : '';
 
 
 
 
125
  }""")
 
 
 
 
126
 
127
  resultados.append({
128
  "Portal": "Metrocuadrado",
129
  "Precio": max(precios),
130
  "Precio_M2": max(precios) / area,
131
- "Descripcion": "Inmueble en " + zona,
132
  "URL": href if href else url_mc
133
  })
134
  except: continue
135
  page.close()
136
  except Exception as e:
137
- log.append(f" Error MC: {str(e)}")
138
 
139
  browser.close()
140
 
141
  if not resultados:
142
- return f"⚠️ No se encontraron datos.\nLOG TÉCNICO:\n" + "\n".join(log), None, None
143
 
144
  df = pd.DataFrame(resultados)
145
 
@@ -147,28 +153,28 @@ def motor_tramitia_clases(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
147
  pdf_path = f"Reporte_{int(time.time())}.pdf"
148
  pdf = FPDF()
149
  pdf.add_page()
150
- pdf.set_font("Arial", 'B', 16)
151
- pdf.cell(0, 10, f"ESTUDIO DE MERCADO: {zona.upper()}", ln=True)
152
- pdf.set_font("Arial", '', 10)
153
  pdf.ln(5)
154
 
155
  for _, r in df.iterrows():
156
- pdf.set_fill_color(245, 245, 245)
157
- pdf.multi_cell(0, 10, f"{r['Portal']} | ${r['Precio']:,.0f}\nLink: {r['URL']}", border=1, fill=True)
158
- pdf.ln(2)
 
159
 
160
  pdf.output(pdf_path)
161
 
162
- return f"✅ Éxito: {len(df)} inmuebles procesados.", df, pdf_path
163
 
164
  # --- INTERFAZ ---
165
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
166
- gr.Markdown("## 🤖 TramitIA Pro: Selectores Nativos (2026)")
167
 
168
  with gr.Row():
169
  with gr.Column():
170
  c = gr.Textbox(label="Ciudad", value="Bogota")
171
- z = gr.Textbox(label="Barrio (Ej: Suba)", value="Suba")
172
  a = gr.Number(label="Área M2", value=60)
173
  t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
174
 
@@ -181,13 +187,13 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
181
  ["Menos de 1 año", "1 a 8 años", "9 a 15 años", "16 a 30 años", "Más de 30 años"],
182
  label="Antigüedad", value="1 a 8 años"
183
  )
184
- btn = gr.Button("BUSCAR (SELECTORES PRECISOS)", variant="primary")
185
 
186
  with gr.Column():
187
- msg = gr.Textbox(label="Estado del Sistema", lines=4)
188
  out_df = gr.Dataframe(label="Resultados")
189
- out_pdf = gr.File(label="Descargar PDF")
190
 
191
- btn.click(motor_tramitia_clases, [z, c, a, t, h, b, p, e], [msg, out_df, out_pdf])
192
 
193
  demo.launch()
 
13
  subprocess.run(["playwright", "install", "chromium"], check=True)
14
  except: pass
15
 
16
+ # --- GENERADOR DE URLS (CORREGIDO SEGÚN TUS EJEMPLOS) ---
17
  def construir_urls(zona, ciudad, tipo, hab, ban, park, antiguedad):
18
+ # 1. FINCA RAÍZ
19
+ # Estructura usuario: https://www.fincaraiz.com.co/arriendo/3-o-mas-habitaciones/2-o-mas-banos/1-parqueadero/de-1-a-8-anios?&searchstring=fontibon-bogota
20
+
21
  mapa_ant = {
22
  "Menos de 1 año": "de-0-a-1-anos",
23
  "1 a 8 años": "de-1-a-8-anios",
 
27
  }
28
  slug_ant = mapa_ant.get(antiguedad, "de-1-a-8-anios")
29
 
30
+ # Lógica singular/plural para parqueaderos
31
+ slug_park = f"{int(park)}-parqueadero" if int(park) == 1 else f"{int(park)}-parqueaderos"
32
 
33
+ # Searchstring limpio
34
+ query_geo = f"{zona.lower().replace(' ', '-')}-{ciudad.lower().replace(' ', '-')}"
35
+
36
+ # URL SIN LA ZONA EN EL PATH, SOLO EN SEARCHSTRING
37
+ url_fr = f"https://www.fincaraiz.com.co/arriendo/{int(hab)}-o-mas-habitaciones/{int(ban)}-o-mas-banos/{slug_park}/{slug_ant}?&searchstring={query_geo}"
38
 
39
+ # 2. METROCUADRADO (Esta funcionaba bien, solo ajustamos el scraper)
40
+ c_slug = ciudad.lower().replace(" ", "-")
 
41
  url_mc = f"https://www.metrocuadrado.com/{tipo.lower()}-casa-oficina/arriendo/{c_slug}/{int(ban)}-banos-{int(hab)}-habitaciones/?search=form"
42
 
43
  return url_fr, url_mc
44
 
45
+ # --- MOTOR DE EXTRACCIÓN ---
46
+ def motor_tramitia_final(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
47
  resultados = []
48
  log = []
49
 
 
57
  user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
58
  )
59
 
60
+ # --- 1. FINCA RAÍZ ---
61
  try:
62
  page = context.new_page()
63
  page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
64
 
65
+ # Scroll para cargar
66
  for _ in range(3):
67
  page.mouse.wheel(0, 1000)
68
+ time.sleep(1.5)
69
 
70
+ # Buscamos en el wrapper de datos que nos diste antes
71
+ # Si ese falla, tenemos un backup genérico
72
+ wrappers = page.query_selector_all("div.lc-dataWrapper, article")
73
 
74
  for wrapper in wrappers[:6]:
75
  try:
76
  texto = wrapper.inner_text()
 
 
77
  if "$" not in texto: continue
78
 
 
79
  precios = [int(s) for s in texto.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
80
  if not precios: continue
81
 
82
+ # JS para encontrar el link padre
83
  href = wrapper.evaluate("el => el.closest('a') ? el.closest('a').href : ''")
84
 
85
  resultados.append({
86
  "Portal": "Finca Raiz",
87
  "Precio": max(precios),
88
  "Precio_M2": max(precios) / area,
89
+ "Descripcion": texto.split('\n')[0][:50] + "...",
90
  "URL": href if href else url_fr
91
  })
92
  except: continue
93
  page.close()
94
  except Exception as e:
95
+ log.append(f"⚠️ Error FR: {str(e)}")
96
 
97
+ # --- 2. METROCUADRADO (Selector corregido: property-card__detail) ---
98
  try:
99
  page = context.new_page()
100
  page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
 
102
  for _ in range(4):
103
  page.mouse.wheel(0, 800)
104
  time.sleep(1)
 
 
 
105
 
106
+ # AQUI ESTA EL CAMBIO: Usamos tu clase exacta
107
+ detalles = page.query_selector_all("div.property-card__detail")
108
+
109
+ for det in detalles[:6]:
110
  try:
111
+ texto = det.inner_text()
112
 
113
+ # Extraer precio del texto del detalle
 
 
 
114
  precios = [int(s) for s in texto.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
115
  if not precios: continue
116
 
117
+ # IMPORTANTE: El link NO suele estar dentro de 'property-card__detail',
118
+ # suele estar en el contenedor padre o abuelo. Usamos JS para subir de nivel.
119
+ href = det.evaluate("""el => {
120
+ // Intentamos subir hasta encontrar la tarjeta completa y su link
121
+ let card = el.closest('li') || el.closest('div.card') || el.closest('div[class*="card"]');
122
+ if (card) {
123
+ let link = card.querySelector('a');
124
+ return link ? link.href : '';
125
+ }
126
+ return '';
127
  }""")
128
+
129
+ # Si no encuentra link por JS, intentamos buscar el <a> más cercano hacia arriba
130
+ if not href:
131
+ href = det.evaluate("el => el.closest('a') ? el.closest('a').href : ''")
132
 
133
  resultados.append({
134
  "Portal": "Metrocuadrado",
135
  "Precio": max(precios),
136
  "Precio_M2": max(precios) / area,
137
+ "Descripcion": texto.split('\n')[0][:50] + "...", # El detalle suele tener la info clave
138
  "URL": href if href else url_mc
139
  })
140
  except: continue
141
  page.close()
142
  except Exception as e:
143
+ log.append(f"⚠️ Error MC: {str(e)}")
144
 
145
  browser.close()
146
 
147
  if not resultados:
148
+ return f"⚠️ No se encontraron datos.\nLOG:\n" + "\n".join(log), None, None
149
 
150
  df = pd.DataFrame(resultados)
151
 
 
153
  pdf_path = f"Reporte_{int(time.time())}.pdf"
154
  pdf = FPDF()
155
  pdf.add_page()
156
+ pdf.set_font("Arial", 'B', 14)
157
+ pdf.cell(0, 10, f"REPORTE: {zona.upper()}", ln=True)
 
158
  pdf.ln(5)
159
 
160
  for _, r in df.iterrows():
161
+ pdf.set_font("Arial", 'B', 10)
162
+ pdf.cell(0, 8, f"{r['Portal']} - ${r['Precio']:,.0f}", ln=True)
163
+ pdf.set_font("Arial", '', 8)
164
+ pdf.multi_cell(0, 5, f"Link: {r['URL']}\nDesc: {r['Descripcion']}\n---")
165
 
166
  pdf.output(pdf_path)
167
 
168
+ return f"✅ {len(df)} inmuebles encontrados.", df, pdf_path
169
 
170
  # --- INTERFAZ ---
171
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
172
+ gr.Markdown("## 🤖 TramitIA Pro: Configuración Exacta")
173
 
174
  with gr.Row():
175
  with gr.Column():
176
  c = gr.Textbox(label="Ciudad", value="Bogota")
177
+ z = gr.Textbox(label="Zona (Ej: Fontibon)", value="Fontibon")
178
  a = gr.Number(label="Área M2", value=60)
179
  t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
180
 
 
187
  ["Menos de 1 año", "1 a 8 años", "9 a 15 años", "16 a 30 años", "Más de 30 años"],
188
  label="Antigüedad", value="1 a 8 años"
189
  )
190
+ btn = gr.Button("BUSCAR", variant="primary")
191
 
192
  with gr.Column():
193
+ msg = gr.Textbox(label="Log URL", lines=3)
194
  out_df = gr.Dataframe(label="Resultados")
195
+ out_pdf = gr.File(label="Reporte")
196
 
197
+ btn.click(motor_tramitia_final, [z, c, a, t, h, b, p, e], [msg, out_df, out_pdf])
198
 
199
  demo.launch()