jcalbornoz commited on
Commit
b757190
·
verified ·
1 Parent(s): 7faaf0b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +173 -166
app.py CHANGED
@@ -1,3 +1,11 @@
 
 
 
 
 
 
 
 
1
  import os
2
  import subprocess
3
  import pandas as pd
@@ -11,7 +19,7 @@ import random
11
  import requests
12
  from PIL import Image
13
  import io
14
- from concurrent.futures import ThreadPoolExecutor # <-- LA LIBRERÍA SALVAVIDAS
15
 
16
  # --- INSTALACIÓN DE DEPENDENCIAS ---
17
  try:
@@ -24,9 +32,9 @@ except ImportError:
24
  subprocess.run(["pip", "install", "fake-useragent"], check=True)
25
  from fake_useragent import UserAgent
26
 
27
- # --- FUNCIÓN PARA DESCARGAR Y PREPARAR FOTOS ---
28
  def descargar_imagen(url, idx):
29
- if not url or url.startswith("data:"):
30
  return None
31
  try:
32
  r = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"})
@@ -38,7 +46,7 @@ def descargar_imagen(url, idx):
38
  img.save(path, format="JPEG")
39
  return path
40
  except:
41
- pass
42
  return None
43
 
44
  # --- 1. GENERADOR DE URLS ---
@@ -79,188 +87,188 @@ def extraer_ubicacion(texto):
79
  lineas = [l.strip() for l in texto.split('\n') if len(l.strip()) > 5]
80
  if len(lineas) > 1:
81
  for linea in lineas[1:4]:
82
- if "$" not in linea and not linea.isdigit():
83
  return linea[:60]
84
  return "Ubicación en zona solicitada"
85
 
86
- # --- 3. MOTOR DE EXTRACCIÓN SÍNCRONO (NÚCLEO) ---
87
  def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
88
  resultados = []
89
- url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
90
 
91
- log_visible = f"✅ INICIANDO EXTRACCIÓN AISLADA:\nFR: {url_fr}\nMC: {url_mc}\n\n"
92
- ua = UserAgent()
 
 
93
 
94
- with sync_playwright() as p:
95
- browser = p.chromium.launch(
96
- headless=True,
97
- args=['--disable-blink-features=AutomationControlled', '--no-sandbox', '--disable-dev-shm-usage', f'--user-agent={ua.random}']
98
- )
99
- context = browser.new_context(viewport={'width': 1366, 'height': 768})
100
 
101
- # --- FINCA RAÍZ ---
102
- try:
103
- page = context.new_page()
104
- log_visible += "🔄 FR: Cargando imágenes y datos...\n"
105
- page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
106
-
107
- for _ in range(4):
108
- page.mouse.wheel(0, 800)
109
- time.sleep(1.5)
110
 
111
- cards = page.query_selector_all("article, div[class*='card']")
112
- for card in cards[:15]:
113
- txt = card.inner_text()
114
- precio = extraer_precio(txt, operacion)
115
- if precio > 0:
116
- enlace_el = card.query_selector("a")
117
- img_el = card.query_selector("img")
118
-
119
- href = enlace_el.get_attribute("href") if enlace_el else ""
120
- img_url = ""
121
- if img_el:
122
- img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or ""
123
 
124
- if href:
125
- resultados.append({
126
- "Portal": "Finca Raiz",
127
- "Precio": precio,
128
- "Precio_M2": precio / area,
129
- "Ubicacion": extraer_ubicacion(txt),
130
- "Descripcion": txt.replace('\n', ' | ')[:120] + "...",
131
- "URL": f"https://www.fincaraiz.com.co{href}" if href.startswith("/") else href,
132
- "Imagen": img_url
133
- })
134
- page.close()
135
- except Exception as e: log_visible += f"⚠️ Error FR: {e}\n"
 
136
 
137
- # --- METROCUADRADO ---
138
- try:
139
- page = context.new_page()
140
- log_visible += "🔄 MC: Cargando imágenes y datos...\n"
141
- page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
142
-
143
- for _ in range(5):
144
- page.mouse.wheel(0, 800)
145
- time.sleep(1.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
- cards = page.query_selector_all("li, div.property-card")
148
- for card in cards[:15]:
149
- txt = card.inner_text()
150
- precio = extraer_precio(txt, operacion)
151
- if precio > 0:
152
- enlace_el = card.query_selector("a")
153
- img_el = card.query_selector("img")
154
-
155
- href = enlace_el.get_attribute("href") if enlace_el else ""
156
- img_url = ""
157
- if img_el:
158
- img_url = img_el.get_attribute("src") or img_el.get_attribute("data-src") or ""
159
 
160
- if href:
161
- resultados.append({
162
- "Portal": "Metrocuadrado",
163
- "Precio": precio,
164
- "Precio_M2": precio / area,
165
- "Ubicacion": extraer_ubicacion(txt),
166
- "Descripcion": txt.replace('\n', ' | ')[:120] + "...",
167
- "URL": f"https://www.metrocuadrado.com{href}" if href.startswith("/") else href,
168
- "Imagen": img_url
169
- })
170
- page.close()
171
- except Exception as e: log_visible += f"⚠️ Error MC: {e}\n"
172
-
173
- browser.close()
174
 
175
- if not resultados:
176
- return f"{log_visible}\n❌ NO SE ENCONTRARON DATOS.", None, None, "---"
 
 
 
177
 
178
- df_crudo = pd.DataFrame(resultados).drop_duplicates(subset=['URL'])
179
- df_fr = df_crudo[df_crudo['Portal'] == 'Finca Raiz'].head(6)
180
- df_mc = df_crudo[df_crudo['Portal'] == 'Metrocuadrado'].head(6)
181
- df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
182
 
183
- # --- GENERACIÓN DEL PDF VISUAL ---
184
- pdf_path = f"Reporte_Visual_{int(time.time())}.pdf"
185
- pdf = FPDF()
186
- pdf.add_page()
187
-
188
- pdf.set_font("Arial", 'B', 16)
189
- pdf.set_fill_color(40, 53, 147)
190
- pdf.set_text_color(255, 255, 255)
191
- pdf.cell(0, 15, f" REPORTE INMOBILIARIO: {barrio.upper()} ({operacion.upper()})", ln=True, fill=True)
192
- pdf.set_text_color(0, 0, 0)
193
- pdf.ln(5)
194
 
195
- for idx, r in df_final.iterrows():
196
- if pdf.get_y() > 220:
197
- pdf.add_page()
 
 
 
198
 
199
- y_start = pdf.get_y()
200
- img_path = descargar_imagen(r['Imagen'], idx)
201
- text_x = 10
202
-
203
- if img_path and os.path.exists(img_path):
204
- try:
205
- pdf.image(img_path, x=10, y=y_start, w=45, h=30)
206
- text_x = 60
207
- except: pass
208
 
209
- pdf.set_xy(text_x, y_start)
210
- pdf.set_font("Arial", 'B', 12)
211
- pdf.cell(0, 6, f"${r['Precio']:,.0f} COP", ln=True)
212
-
213
- pdf.set_x(text_x)
214
- pdf.set_font("Arial", 'B', 9)
215
- pdf.set_text_color(100, 100, 100)
216
- pdf.cell(0, 5, f"📍 {r['Ubicacion']} | Fuente: {r['Portal']}", ln=True)
217
-
218
- pdf.set_x(text_x)
219
- pdf.set_font("Arial", '', 8)
220
- pdf.set_text_color(0, 0, 0)
221
- pdf.multi_cell(0, 4, f"{r['Descripcion']}")
222
-
223
- pdf.set_x(text_x)
224
- pdf.set_font("Arial", 'U', 8)
225
- pdf.set_text_color(0, 102, 204)
226
- pdf.cell(0, 5, "🔗 Hacer clic aquí para ver publicación original", link=r['URL'], ln=True)
227
- pdf.set_text_color(0, 0, 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
228
 
229
- y_end = pdf.get_y()
230
- pdf.set_y(max(y_start + 35, y_end + 5))
 
 
 
 
 
 
231
 
232
- pdf.set_draw_color(200, 200, 200)
233
- pdf.line(10, pdf.get_y(), 200, pdf.get_y())
234
- pdf.ln(5)
235
 
236
- if img_path and os.path.exists(img_path):
237
- os.remove(img_path)
238
-
239
- pdf.output(pdf_path)
240
-
241
- # --- CÁLCULOS ---
242
- promedio = df_final['Precio_M2'].mean() * area
243
- resumen = (
244
- f"💰 **ESTIMACIÓN DE {operacion.upper()}**\n"
245
- f"🔹 **Valor Sugerido:** ${promedio:,.0f}\n"
246
- f"📉 Mínimo Zona: ${df_final['Precio'].min():,.0f}\n"
247
- f"📈 Máximo Zona: ${df_final['Precio'].max():,.0f}"
248
- )
249
-
250
- df_mostrar = df_final[['Portal', 'Precio', 'Ubicacion', 'Descripcion', 'URL']]
251
-
252
- return f"{log_visible}\n✅ PDF Visual Generado con Éxito.", df_mostrar, pdf_path, resumen
253
 
254
- # --- 4. FUNCIÓN ENVOLTORIO (LA BURBUJA SALVAVIDAS) ---
255
- def ejecutor_aislado(*args):
256
- # Ejecuta el motor en un hilo completamente separado para que no choque con Gradio
257
- with ThreadPoolExecutor(max_workers=1) as executor:
258
- futuro = executor.submit(motor_tramitia_visual, *args)
259
- return futuro.result()
260
 
261
  # --- INTERFAZ GRÁFICA ---
262
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
263
- gr.Markdown("## 📸 TramitIA Pro: Generador Visual Anti-Errores")
264
 
265
  with gr.Row():
266
  with gr.Column(scale=1):
@@ -270,7 +278,7 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
270
 
271
  with gr.Row():
272
  t = gr.Dropdown(["Apartamento", "Casa", "Bodega", "Lote", "Oficina"], label="Tipo", value="Apartamento")
273
- a = gr.Number(label="Área M2 (Tu Cliente)", value=70)
274
 
275
  with gr.Row():
276
  m2_min = gr.Number(label="M2 Mínimo", value=10)
@@ -286,16 +294,15 @@ with gr.Blocks(theme=gr.themes.Soft()) as demo:
286
  p = gr.Number(label="Park", value=1)
287
 
288
  e = gr.Dropdown(["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"], label="Antigüedad", value="1 a 8 años")
289
- btn = gr.Button("GENERAR REPORTE CON FOTOS", variant="primary")
290
 
291
  with gr.Column(scale=2):
292
- res_fin = gr.Markdown("### 💰 Resultado...")
293
  with gr.Tabs():
294
- with gr.TabItem("Descargar Reporte PDF"): out_pdf = gr.File()
295
  with gr.TabItem("Tabla de Datos"): out_df = gr.Dataframe()
296
  with gr.TabItem("Auditoría"): msg = gr.Textbox(lines=10)
297
 
298
- # AQUÍ ESTÁ EL TRUCO: Llamamos al ejecutor_aislado, no directamente al motor
299
- btn.click(ejecutor_aislado, [op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina], [msg, out_df, out_pdf, res_fin])
300
 
301
  demo.launch()
 
1
+ import sys
2
+
3
+ # Silenciador del error fantasma de Linux (No congela, solo oculta el texto rojo)
4
+ def silenciador_errores_basura(unraisable):
5
+ if unraisable.exc_type == ValueError and "Invalid file descriptor: -1" in str(unraisable.exc_value): pass
6
+ else: sys.__unraisablehook__(unraisable)
7
+ sys.unraisablehook = silenciador_errores_basura
8
+
9
  import os
10
  import subprocess
11
  import pandas as pd
 
19
  import requests
20
  from PIL import Image
21
  import io
22
+ import traceback
23
 
24
  # --- INSTALACIÓN DE DEPENDENCIAS ---
25
  try:
 
32
  subprocess.run(["pip", "install", "fake-useragent"], check=True)
33
  from fake_useragent import UserAgent
34
 
35
+ # --- DESCARGA SEGURA DE FOTOS ---
36
  def descargar_imagen(url, idx):
37
+ if not url or len(url) < 10 or url.startswith("data:"):
38
  return None
39
  try:
40
  r = requests.get(url, timeout=5, headers={"User-Agent": "Mozilla/5.0"})
 
46
  img.save(path, format="JPEG")
47
  return path
48
  except:
49
+ return None # Si falla, devuelve None silenciosamente
50
  return None
51
 
52
  # --- 1. GENERADOR DE URLS ---
 
87
  lineas = [l.strip() for l in texto.split('\n') if len(l.strip()) > 5]
88
  if len(lineas) > 1:
89
  for linea in lineas[1:4]:
90
+ if "$" not in linea and not linea.isdigit() and "m²" not in linea:
91
  return linea[:60]
92
  return "Ubicación en zona solicitada"
93
 
94
+ # --- 3. MOTOR DE EXTRACCIÓN (SÍNCRONO Y ESTABLE) ---
95
  def motor_tramitia_visual(operacion, barrio, ciudad, area, m2_min, m2_max, tipo, hab, ban, park, antiguedad, ascensor, piscina):
96
  resultados = []
97
+ log_visible = ""
98
 
99
+ try:
100
+ url_fr, url_mc = construir_urls_final(operacion, barrio, ciudad, tipo, hab, ban, park, antiguedad, m2_min, m2_max, ascensor, piscina)
101
+ log_visible = f"✅ INICIANDO EXTRACCIÓN:\nFR: {url_fr}\nMC: {url_mc}\n\n"
102
+ ua = UserAgent()
103
 
104
+ with sync_playwright() as p:
105
+ browser = p.chromium.launch(headless=True, args=['--disable-blink-features=AutomationControlled', '--no-sandbox'])
106
+ context = browser.new_context(viewport={'width': 1366, 'height': 768}, user_agent=ua.random)
 
 
 
107
 
108
+ # --- FINCA RAÍZ ---
109
+ try:
110
+ page = context.new_page()
111
+ log_visible += "🔄 FR: Buscando inmuebles...\n"
112
+ page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
113
+
114
+ for _ in range(3):
115
+ page.mouse.wheel(0, 1000)
116
+ time.sleep(1.5)
117
 
118
+ cards = page.query_selector_all("article, div[class*='card']")
119
+ for card in cards[:12]:
120
+ try:
121
+ txt = card.inner_text()
122
+ precio = extraer_precio(txt, operacion)
123
+ if precio > 0:
124
+ enlace_el = card.query_selector("a")
125
+ img_el = card.query_selector("img")
126
+
127
+ href = enlace_el.get_attribute("href") if enlace_el else ""
128
+ img_url = img_el.get_attribute("src") if img_el else ""
 
129
 
130
+ if href:
131
+ resultados.append({
132
+ "Portal": "Finca Raiz",
133
+ "Precio": precio,
134
+ "Precio_M2": precio / area,
135
+ "Ubicacion": extraer_ubicacion(txt),
136
+ "Descripcion": txt.replace('\n', ' | ')[:120] + "...",
137
+ "URL": f"https://www.fincaraiz.com.co{href}" if href.startswith("/") else href,
138
+ "Imagen": img_url
139
+ })
140
+ except: continue # Si una tarjeta falla, salta a la siguiente
141
+ page.close()
142
+ except Exception as e: log_visible += f"⚠️ Error buscando en FR.\n"
143
 
144
+ # --- METROCUADRADO ---
145
+ try:
146
+ page = context.new_page()
147
+ log_visible += "🔄 MC: Buscando inmuebles...\n"
148
+ page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
149
+
150
+ for _ in range(4):
151
+ page.mouse.wheel(0, 1000)
152
+ time.sleep(1.5)
153
+
154
+ cards = page.query_selector_all("li, div.property-card")
155
+ for card in cards[:12]:
156
+ try:
157
+ txt = card.inner_text()
158
+ precio = extraer_precio(txt, operacion)
159
+ if precio > 0:
160
+ enlace_el = card.query_selector("a")
161
+ img_el = card.query_selector("img")
162
+
163
+ href = enlace_el.get_attribute("href") if enlace_el else ""
164
+ img_url = img_el.get_attribute("src") if img_el else ""
165
+
166
+ if href:
167
+ resultados.append({
168
+ "Portal": "Metrocuadrado",
169
+ "Precio": precio,
170
+ "Precio_M2": precio / area,
171
+ "Ubicacion": extraer_ubicacion(txt),
172
+ "Descripcion": txt.replace('\n', ' | ')[:120] + "...",
173
+ "URL": f"https://www.metrocuadrado.com{href}" if href.startswith("/") else href,
174
+ "Imagen": img_url
175
+ })
176
+ except: continue
177
+ page.close()
178
+ except Exception as e: log_visible += f"⚠️ Error buscando en MC.\n"
179
 
180
+ browser.close()
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ if not resultados:
183
+ return f"{log_visible}\n❌ NO SE ENCONTRARON DATOS.", pd.DataFrame(), None, "---"
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
+ # --- LIMPIEZA ---
186
+ df_crudo = pd.DataFrame(resultados).drop_duplicates(subset=['URL'])
187
+ df_fr = df_crudo[df_crudo['Portal'] == 'Finca Raiz'].head(6)
188
+ df_mc = df_crudo[df_crudo['Portal'] == 'Metrocuadrado'].head(6)
189
+ df_final = pd.concat([df_fr, df_mc]).reset_index(drop=True)
190
 
191
+ log_visible += f"\n✨ PROCESANDO PDF CON {len(df_final)} INMUEBLES..."
 
 
 
192
 
193
+ # --- PDF ---
194
+ pdf_path = f"Reporte_Visual_{int(time.time())}.pdf"
195
+ pdf = FPDF()
196
+ pdf.add_page()
197
+
198
+ pdf.set_font("Arial", 'B', 16)
199
+ pdf.set_fill_color(40, 53, 147)
200
+ pdf.set_text_color(255, 255, 255)
201
+ pdf.cell(0, 15, f" REPORTE INMOBILIARIO: {barrio.upper()} ({operacion.upper()})", ln=True, fill=True)
202
+ pdf.set_text_color(0, 0, 0)
203
+ pdf.ln(5)
204
 
205
+ for idx, r in df_final.iterrows():
206
+ if pdf.get_y() > 220: pdf.add_page()
207
+
208
+ y_start = pdf.get_y()
209
+ img_path = descargar_imagen(r['Imagen'], idx)
210
+ text_x = 10
211
 
212
+ if img_path and os.path.exists(img_path):
213
+ try:
214
+ pdf.image(img_path, x=10, y=y_start, w=45, h=30)
215
+ text_x = 60
216
+ except: pass
 
 
 
 
217
 
218
+ pdf.set_xy(text_x, y_start)
219
+ pdf.set_font("Arial", 'B', 12)
220
+ pdf.cell(0, 6, f"${r['Precio']:,.0f} COP", ln=True)
221
+
222
+ pdf.set_x(text_x)
223
+ pdf.set_font("Arial", 'B', 9)
224
+ pdf.set_text_color(100, 100, 100)
225
+ pdf.cell(0, 5, f"📍 {r['Ubicacion']} | Fuente: {r['Portal']}", ln=True)
226
+
227
+ pdf.set_x(text_x)
228
+ pdf.set_font("Arial", '', 8)
229
+ pdf.set_text_color(0, 0, 0)
230
+ pdf.multi_cell(0, 4, f"{r['Descripcion']}")
231
+
232
+ pdf.set_x(text_x)
233
+ pdf.set_font("Arial", 'U', 8)
234
+ pdf.set_text_color(0, 102, 204)
235
+ pdf.cell(0, 5, "🔗 Hacer clic aquí para ver publicación", link=r['URL'], ln=True)
236
+ pdf.set_text_color(0, 0, 0)
237
+
238
+ y_end = pdf.get_y()
239
+ pdf.set_y(max(y_start + 35, y_end + 5))
240
+
241
+ pdf.set_draw_color(200, 200, 200)
242
+ pdf.line(10, pdf.get_y(), 200, pdf.get_y())
243
+ pdf.ln(5)
244
+
245
+ if img_path and os.path.exists(img_path):
246
+ try: os.remove(img_path)
247
+ except: pass
248
+
249
+ pdf.output(pdf_path)
250
 
251
+ # --- CÁLCULOS ---
252
+ promedio = df_final['Precio_M2'].mean() * area
253
+ resumen = (
254
+ f"💰 **ESTIMACIÓN DE {operacion.upper()}**\n"
255
+ f"🔹 **Valor Sugerido:** ${promedio:,.0f}\n"
256
+ f"📉 Mínimo Zona: ${df_final['Precio'].min():,.0f}\n"
257
+ f"📈 Máximo Zona: ${df_final['Precio'].max():,.0f}"
258
+ )
259
 
260
+ df_mostrar = df_final[['Portal', 'Precio', 'Ubicacion', 'Descripcion', 'URL']]
 
 
261
 
262
+ return f"{log_visible}\n✅ PDF Generado Exitosamente.", df_mostrar, pdf_path, resumen
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
263
 
264
+ except Exception as error_fatal:
265
+ # PARACAÍDAS: Si todo colapsa, te mostrará exactamente DÓNDE y POR QUÉ en la pantalla
266
+ traza = traceback.format_exc()
267
+ return f"❌ ERROR GRAVE DEL SISTEMA:\n{str(error_fatal)}\n\nDetalles:\n{traza}", pd.DataFrame(), None, "⚠️ Falló la ejecución"
 
 
268
 
269
  # --- INTERFAZ GRÁFICA ---
270
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
271
+ gr.Markdown("## 📸 TramitIA Pro: Generador Visual (Versión Estable)")
272
 
273
  with gr.Row():
274
  with gr.Column(scale=1):
 
278
 
279
  with gr.Row():
280
  t = gr.Dropdown(["Apartamento", "Casa", "Bodega", "Lote", "Oficina"], label="Tipo", value="Apartamento")
281
+ a = gr.Number(label="Área M2", value=70)
282
 
283
  with gr.Row():
284
  m2_min = gr.Number(label="M2 Mínimo", value=10)
 
294
  p = gr.Number(label="Park", value=1)
295
 
296
  e = gr.Dropdown(["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"], label="Antigüedad", value="1 a 8 años")
297
+ btn = gr.Button("GENERAR REPORTE", variant="primary")
298
 
299
  with gr.Column(scale=2):
300
+ res_fin = gr.Markdown("### 💰 El resultado aparecerá aquí...")
301
  with gr.Tabs():
302
+ with gr.TabItem("Descargar PDF"): out_pdf = gr.File()
303
  with gr.TabItem("Tabla de Datos"): out_df = gr.Dataframe()
304
  with gr.TabItem("Auditoría"): msg = gr.Textbox(lines=10)
305
 
306
+ btn.click(motor_tramitia_visual, [op, b, c, a, m2_min, m2_max, t, h, ban, p, e, ascensor, piscina], [msg, out_df, out_pdf, res_fin])
 
307
 
308
  demo.launch()