jcalbornoz commited on
Commit
799910e
·
verified ·
1 Parent(s): 5f46016

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +153 -144
app.py CHANGED
@@ -2,15 +2,24 @@ import os
2
  import subprocess
3
  import pandas as pd
4
  import datetime
 
5
  from fpdf import FPDF
6
  import gradio as gr
7
  from playwright.sync_api import sync_playwright
 
 
8
 
9
  # --- INSTALACIÓN ---
10
  try:
11
  subprocess.run(["playwright", "install", "chromium"], check=True)
12
  except: pass
13
 
 
 
 
 
 
 
14
  # --- 1. GENERADOR DE URLS ---
15
  def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
16
  mapa_ant = {
@@ -31,209 +40,209 @@ def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
31
 
32
  return url_fr, url_mc
33
 
34
- # --- 2. GENERADOR PDF ---
35
- def generar_pdf_con_estimacion(zona, area, df, url_fr, url_mc):
36
- promedio_m2 = df['Precio_M2'].mean()
37
- precio_sugerido = promedio_m2 * area
38
- precio_min = df['Precio'].min()
39
- precio_max = df['Precio'].max()
40
-
41
- pdf_path = f"Estimacion_{int(datetime.datetime.now().timestamp())}.pdf"
42
- pdf = FPDF()
43
- pdf.add_page()
44
-
45
- pdf.set_font("Arial", 'B', 16)
46
- pdf.cell(0, 10, f"ESTIMACION DE CANON: {zona.upper()}", ln=True, align='C')
47
- pdf.ln(5)
48
-
49
- pdf.set_fill_color(220, 230, 240)
50
- pdf.rect(10, 30, 190, 40, 'F')
51
- pdf.set_y(35)
52
-
53
- pdf.set_font("Arial", 'B', 12)
54
- pdf.cell(0, 8, f"CANON SUGERIDO (Area: {area}m2):", ln=True, align='C')
55
-
56
- pdf.set_font("Arial", 'B', 20)
57
- pdf.set_text_color(0, 100, 0)
58
- pdf.cell(0, 10, f"${precio_sugerido:,.0f}", ln=True, align='C')
59
-
60
- pdf.set_font("Arial", '', 10)
61
- pdf.set_text_color(0, 0, 0)
62
- pdf.cell(0, 8, f"Rango de Mercado: ${precio_min:,.0f} - ${precio_max:,.0f}", ln=True, align='C')
63
- pdf.ln(15)
64
 
65
- pdf.set_font("Arial", 'B', 8)
66
- pdf.set_text_color(100, 100, 100)
67
- pdf.cell(0, 5, "URLS DE BUSQUEDA:", ln=True)
68
- pdf.set_font("Courier", '', 7)
69
- pdf.multi_cell(0, 4, f"FR: {url_fr}\nMC: {url_mc}")
70
- pdf.ln(5)
71
-
72
- pdf.set_text_color(0, 0, 0)
73
- pdf.set_font("Arial", 'B', 12)
74
- pdf.cell(0, 10, "COMPARABLES UTILIZADOS:", ln=True)
75
-
76
- pdf.set_font("Arial", '', 10)
77
- for _, r in df.iterrows():
78
- pdf.set_fill_color(245, 245, 245)
79
- pdf.cell(0, 8, f"{r['Portal']} - ${r['Precio']:,.0f} (M2: ${r['Precio_M2']:,.0f})", ln=True, fill=True)
80
- pdf.set_font("Arial", '', 8)
81
- pdf.multi_cell(0, 4, f"{r['Descripcion']}")
82
- pdf.set_font("Arial", 'U', 8)
83
- pdf.set_text_color(0, 0, 255)
84
- pdf.cell(0, 6, "Clic aqui para abrir publicacion", link=r['URL'], ln=True)
85
- pdf.set_text_color(0, 0, 0)
86
- pdf.ln(3)
87
-
88
- pdf.output(pdf_path)
89
- return pdf_path, precio_sugerido, precio_min, precio_max
90
-
91
- # --- 3. MOTOR DE EXTRACCIÓN BLINDADO ---
92
- def motor_tramitia_estimador(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
93
  resultados = []
94
  url_fr, url_mc = construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad)
95
- log_visible = f"✅ URLs AUDITABLES:\nFR: {url_fr}\nMC: {url_mc}\n\n--- INICIANDO ESCANEO ---\n"
 
 
96
 
97
  with sync_playwright() as p:
98
- browser = p.chromium.launch(headless=True, args=["--disable-blink-features=AutomationControlled"])
99
- context = browser.new_context(user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
 
 
 
 
 
 
 
 
 
 
 
 
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  # --- FINCA RAÍZ ---
102
  try:
103
  page = context.new_page()
 
104
  page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
105
 
106
- # Espera ACTIVA: obligamos al bot a esperar hasta ver un signo de pesos o agotar 10 segundos
107
- try:
108
- page.wait_for_selector("text=$", timeout=10000)
109
- except:
110
- log_visible += "⏳ FR: Demoró en cargar precios.\n"
111
-
112
- # Scroll nativo de Playwright
113
- for _ in range(3):
114
- page.mouse.wheel(0, 1000)
115
- page.wait_for_timeout(1000) # Espera 1 segundo real
116
 
117
- # Selector múltiple de respaldo
118
- cards = page.locator("article, div.lc-dataWrapper, div[class*='Card']").all()
 
 
 
 
 
119
 
120
- for card in cards[:8]:
121
- try:
122
- txt = card.inner_text()
123
- if "$" not in txt: continue
124
-
125
- precios = [int(s) for s in txt.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
126
- if not precios: continue
127
-
128
- # Evaluación directa sobre el elemento (infalible para links)
129
- href = card.evaluate("el => el.querySelector('a') ? el.querySelector('a').href : (el.closest('a') ? el.closest('a').href : '')")
130
-
131
- if href:
132
  resultados.append({
133
  "Portal": "Finca Raiz",
134
- "Precio": max(precios),
135
- "Precio_M2": max(precios) / area,
136
- "Descripcion": txt.replace('\n', ' ')[:90] + "...",
137
- "URL": href
138
  })
139
- except: continue
140
  page.close()
 
141
  except Exception as e: log_visible += f"⚠️ Error FR: {e}\n"
142
 
143
  # --- METROCUADRADO ---
144
  try:
145
  page = context.new_page()
 
146
  page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
147
 
148
- try:
149
- page.wait_for_selector("text=$", timeout=10000)
150
- except:
151
- log_visible += "⏳ MC: Demoró en cargar precios.\n"
152
-
153
- for _ in range(3):
154
  page.mouse.wheel(0, 1000)
155
- page.wait_for_timeout(1000)
156
 
157
- # Selector usando tu clase exacta más respaldos
158
- cards = page.locator("div.property-card__detail, li, div.property-card").all()
159
 
160
- for card in cards[:8]:
161
- try:
162
- txt = card.inner_text()
163
- if "$" not in txt: continue
164
-
165
- precios = [int(s) for s in txt.replace('.', '').replace('$', '').split() if s.isdigit() and len(s) >= 6]
166
- if not precios: continue
167
-
168
- href = card.evaluate("el => el.querySelector('a') ? el.querySelector('a').href : (el.closest('li') && el.closest('li').querySelector('a') ? el.closest('li').querySelector('a').href : '')")
169
-
170
- if href:
171
- resultados.append({
172
- "Portal": "Metrocuadrado",
173
- "Precio": max(precios),
174
- "Precio_M2": max(precios) / area,
175
- "Descripcion": txt.replace('\n', ' ')[:90] + "...",
176
- "URL": href
177
- })
178
- except: continue
 
 
 
179
  page.close()
 
180
  except Exception as e: log_visible += f"⚠️ Error MC: {e}\n"
181
 
182
  browser.close()
183
 
184
  if not resultados:
185
- return f"{log_visible}\n❌ NO SE ENCONTRARON DATOS. Posible bloqueo o selectores no coinciden.", None, None, "---"
186
 
187
  df = pd.DataFrame(resultados).drop_duplicates(subset=['URL'])
188
 
189
- if df.empty:
190
- return f"{log_visible}\n❌ SE ENCONTRARON DATOS PERO ERAN DUPLICADOS O INVÁLIDOS.", None, None, "---"
191
-
192
- pdf_path, sug, min_p, max_p = generar_pdf_con_estimacion(zona, area, df, url_fr, url_mc)
 
 
 
 
 
 
 
 
 
 
 
 
193
 
194
- resumen_financiero = (
 
 
 
 
 
195
  f"💰 **ESTIMACIÓN DE RENTA**\n"
196
- f"🔹 **Canon Sugerido:** ${sug:,.0f}\n"
197
- f"📉 Mínimo Zona: ${min_p:,.0f}\n"
198
- f"📈 Máximo Zona: ${max_p:,.0f}\n"
199
- f"📊 Basado en {len(df)} comparables."
200
  )
201
 
202
- return f"{log_visible}\n✅ Extracción exitosa.", df, pdf_path, resumen_financiero
203
 
204
  # --- INTERFAZ ---
205
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
206
- gr.Markdown("## 🤖 TramitIA Pro: Extracción Blindada")
207
 
208
  with gr.Row():
209
  with gr.Column(scale=1):
210
  c = gr.Textbox(label="Ciudad", value="Bogota")
211
- z = gr.Textbox(label="Zona (Ej: Fontibon)", value="Fontibon")
212
- a = gr.Number(label="Área M2 (Tu Cliente)", value=60)
213
  t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
214
 
215
  with gr.Row():
216
- h = gr.Number(label="Habitaciones", value=3)
217
  b = gr.Number(label="Baños", value=2)
218
- p = gr.Number(label="Parqueaderos", value=1)
219
 
220
  e = gr.Dropdown(
221
  ["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"],
222
  label="Antigüedad", value="1 a 8 años"
223
  )
224
- btn = gr.Button("CALCULAR CANON", variant="primary")
225
 
226
  with gr.Column(scale=2):
227
- res_fin = gr.Markdown("### 💰 El resultado aparecerá aquí...")
228
-
229
  with gr.Tabs():
230
- with gr.TabItem("🔍 Log URLs"):
231
- msg = gr.Textbox(lines=6, label="Auditoría del Proceso")
232
- with gr.TabItem("📋 Tabla de Comparables"):
233
- out_df = gr.Dataframe()
234
- with gr.TabItem("📄 Reporte PDF"):
235
- out_pdf = gr.File()
236
 
237
- btn.click(motor_tramitia_estimador, [z, c, a, t, h, b, p, e], [msg, out_df, out_pdf, res_fin])
238
 
239
  demo.launch()
 
2
  import subprocess
3
  import pandas as pd
4
  import datetime
5
+ import re
6
  from fpdf import FPDF
7
  import gradio as gr
8
  from playwright.sync_api import sync_playwright
9
+ import time
10
+ import random
11
 
12
  # --- INSTALACIÓN ---
13
  try:
14
  subprocess.run(["playwright", "install", "chromium"], check=True)
15
  except: pass
16
 
17
+ try:
18
+ from fake_useragent import UserAgent
19
+ except ImportError:
20
+ subprocess.run(["pip", "install", "fake-useragent"], check=True)
21
+ from fake_useragent import UserAgent
22
+
23
  # --- 1. GENERADOR DE URLS ---
24
  def construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad):
25
  mapa_ant = {
 
40
 
41
  return url_fr, url_mc
42
 
43
+ # --- 2. EXTRACTOR GENÉRICO (Regex) ---
44
+ def extraer_precio_regex(texto):
45
+ # Busca patrones como $ 1.500.000 o $1500000
46
+ patron = r'\$\s?(\d{1,3}(?:[.,]\d{3})*)'
47
+ coincidencias = re.findall(patron, texto)
48
+ if coincidencias:
49
+ # Convertir a entero: quitar puntos y comas
50
+ precios = [int(p.replace('.', '').replace(',', '')) for p in coincidencias]
51
+ return max(precios) # Retorna el precio más alto encontrado en el texto
52
+ return 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
+ # --- 3. MOTOR DE EXTRACCIÓN CAMUFLADO ---
55
+ def motor_tramitia_camuflado(zona, ciudad, area, tipo, hab, ban, park, antiguedad):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  resultados = []
57
  url_fr, url_mc = construir_urls_final(zona, ciudad, tipo, hab, ban, park, antiguedad)
58
+ log_visible = f"✅ URLs INICIADAS:\nFR: {url_fr}\nMC: {url_mc}\n\n"
59
+
60
+ ua = UserAgent()
61
 
62
  with sync_playwright() as p:
63
+ # LANZAMIENTO EN MODO FURTIVO
64
+ browser = p.chromium.launch(
65
+ headless=True,
66
+ args=[
67
+ '--disable-blink-features=AutomationControlled', # Oculta que es un robot
68
+ '--no-sandbox',
69
+ '--disable-setuid-sandbox',
70
+ '--disable-infobars',
71
+ '--window-position=0,0',
72
+ '--ignore-certifcate-errors',
73
+ '--ignore-certifcate-errors-spki-list',
74
+ '--user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
75
+ ]
76
+ )
77
 
78
+ # Contexto con Viewport aleatorio para parecer humano
79
+ context = browser.new_context(
80
+ user_agent=ua.random,
81
+ viewport={'width': 1366, 'height': 768},
82
+ locale='es-CO',
83
+ timezone_id='America/Bogota'
84
+ )
85
+
86
+ # Script para evasión de detección de WebDriver
87
+ init_script = """
88
+ Object.defineProperty(navigator, 'webdriver', {
89
+ get: () => undefined
90
+ });
91
+ """
92
+ context.add_init_script(init_script)
93
+
94
  # --- FINCA RAÍZ ---
95
  try:
96
  page = context.new_page()
97
+ log_visible += "🔄 Conectando a FR...\n"
98
  page.goto(url_fr, wait_until="domcontentloaded", timeout=60000)
99
 
100
+ # Simulamos comportamiento humano antes de buscar
101
+ page.mouse.move(random.randint(100, 500), random.randint(100, 500))
102
+ page.mouse.wheel(0, 500)
103
+ time.sleep(random.uniform(2, 4))
 
 
 
 
 
 
104
 
105
+ # Scroll lento
106
+ for _ in range(3):
107
+ page.mouse.wheel(0, 1500)
108
+ time.sleep(1)
109
+
110
+ # ESTRATEGIA: Buscar TODOS los enlaces que contengan un precio dentro
111
+ elementos = page.query_selector_all("a") # Buscamos cualquier link
112
 
113
+ cont_fr = 0
114
+ for el in elementos:
115
+ if cont_fr >= 6: break
116
+ txt = el.inner_text()
117
+
118
+ # Si tiene signo pesos, es un candidato
119
+ if "$" in txt:
120
+ precio = extraer_precio_regex(txt)
121
+ if precio > 500000: # Filtro de "precio lógico" (evitar precios de garajes solos)
122
+ href = el.get_attribute("href")
123
+ full_url = f"https://www.fincaraiz.com.co{href}" if href.startswith("/") else href
124
+
125
  resultados.append({
126
  "Portal": "Finca Raiz",
127
+ "Precio": precio,
128
+ "Precio_M2": precio / area,
129
+ "Descripcion": txt.replace('\n', ' ')[:80] + "...",
130
+ "URL": full_url
131
  })
132
+ cont_fr += 1
133
  page.close()
134
+ log_visible += f"✅ FR: {cont_fr} datos extraídos.\n"
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 += "🔄 Conectando a MC...\n"
141
  page.goto(url_mc, wait_until="domcontentloaded", timeout=60000)
142
 
143
+ page.mouse.move(random.randint(100, 500), random.randint(100, 500))
144
+ time.sleep(random.uniform(2, 4))
145
+
146
+ for _ in range(4):
 
 
147
  page.mouse.wheel(0, 1000)
148
+ time.sleep(1)
149
 
150
+ # MC suele usar Li o Div cards. Buscamos contenedores genéricos
151
+ cards = page.query_selector_all("li, div[class*='card']")
152
 
153
+ cont_mc = 0
154
+ for card in cards:
155
+ if cont_mc >= 6: break
156
+ txt = card.inner_text()
157
+
158
+ if "$" in txt:
159
+ precio = extraer_precio_regex(txt)
160
+ if precio > 500000:
161
+ # Buscamos el link dentro de esa tarjeta
162
+ enlace = card.query_selector("a")
163
+ if enlace:
164
+ href = enlace.get_attribute("href")
165
+ full_url = f"https://www.metrocuadrado.com{href}" if href.startswith("/") else href
166
+
167
+ resultados.append({
168
+ "Portal": "Metrocuadrado",
169
+ "Precio": precio,
170
+ "Precio_M2": precio / area,
171
+ "Descripcion": txt.replace('\n', ' ')[:80] + "...",
172
+ "URL": full_url
173
+ })
174
+ cont_mc += 1
175
  page.close()
176
+ log_visible += f"✅ MC: {cont_mc} datos extraídos.\n"
177
  except Exception as e: log_visible += f"⚠️ Error MC: {e}\n"
178
 
179
  browser.close()
180
 
181
  if not resultados:
182
+ return f"{log_visible}\n❌ NO SE ENCONTRARON DATOS. Bloqueo persistente.", None, None, "---"
183
 
184
  df = pd.DataFrame(resultados).drop_duplicates(subset=['URL'])
185
 
186
+ # PDF
187
+ pdf_path = f"Reporte_{int(time.time())}.pdf"
188
+ pdf = FPDF()
189
+ pdf.add_page()
190
+ pdf.set_font("Arial", 'B', 14)
191
+ pdf.cell(0, 10, f"ESTUDIO {zona.upper()}", ln=True)
192
+ pdf.ln(5)
193
+ for _, r in df.iterrows():
194
+ pdf.set_font("Arial", 'B', 10)
195
+ pdf.cell(0, 8, f"${r['Precio']:,.0f} - {r['Portal']}", ln=True)
196
+ pdf.set_font("Arial", '', 8)
197
+ pdf.multi_cell(0, 4, f"{r['Descripcion']}")
198
+ pdf.set_font("Arial", 'U', 8); pdf.set_text_color(0,0,255)
199
+ pdf.cell(0, 6, "Ver Publicacion", link=r['URL'], ln=True)
200
+ pdf.set_text_color(0,0,0); pdf.ln(3)
201
+ pdf.output(pdf_path)
202
 
203
+ # Cálculos
204
+ promedio = df['Precio'].mean()
205
+ minimo = df['Precio'].min()
206
+ maximo = df['Precio'].max()
207
+
208
+ resumen = (
209
  f"💰 **ESTIMACIÓN DE RENTA**\n"
210
+ f"🔹 **Promedio Zona:** ${promedio:,.0f}\n"
211
+ f"📉 Mínimo: ${minimo:,.0f}\n"
212
+ f"📈 Máximo: ${maximo:,.0f}"
 
213
  )
214
 
215
+ return f"{log_visible}\n✅ Proceso Terminado.", df, pdf_path, resumen
216
 
217
  # --- INTERFAZ ---
218
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
219
+ gr.Markdown("## 🤖 TramitIA Pro: Modo Camuflaje")
220
 
221
  with gr.Row():
222
  with gr.Column(scale=1):
223
  c = gr.Textbox(label="Ciudad", value="Bogota")
224
+ z = gr.Textbox(label="Zona", value="Fontibon")
225
+ a = gr.Number(label="Área M2", value=60)
226
  t = gr.Dropdown(["Apartamento", "Casa"], label="Tipo", value="Apartamento")
227
 
228
  with gr.Row():
229
+ h = gr.Number(label="Hab", value=3)
230
  b = gr.Number(label="Baños", value=2)
231
+ p = gr.Number(label="Park", value=1)
232
 
233
  e = gr.Dropdown(
234
  ["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"],
235
  label="Antigüedad", value="1 a 8 años"
236
  )
237
+ btn = gr.Button("EJECUTAR ESCANEO", variant="primary")
238
 
239
  with gr.Column(scale=2):
240
+ res_fin = gr.Markdown("### 💰 Resultado...")
 
241
  with gr.Tabs():
242
+ with gr.TabItem("Log"): msg = gr.Textbox()
243
+ with gr.TabItem("Tabla"): out_df = gr.Dataframe()
244
+ with gr.TabItem("PDF"): out_pdf = gr.File()
 
 
 
245
 
246
+ btn.click(motor_tramitia_camuflado, [z, c, a, t, h, b, p, e], [msg, out_df, out_pdf, res_fin])
247
 
248
  demo.launch()