Lukeetah commited on
Commit
f80191b
·
verified ·
1 Parent(s): 0bb26d1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +104 -286
app.py CHANGED
@@ -1,302 +1,120 @@
1
- # app.py
2
-
3
- import asyncio
4
  from bs4 import BeautifulSoup
5
- from selenium import webdriver
6
- from selenium.webdriver.common.by import By
7
- from selenium.webdriver.chrome.service import Service as ChromeService
8
- from selenium.webdriver.support.ui import WebDriverWait
9
- from selenium.webdriver.support import expected_conditions as EC
10
- from selenium.common.exceptions import NoSuchElementException, TimeoutException
11
- from urllib.parse import urljoin, urlparse
12
  import pandas as pd
13
- import re
14
- import random
15
  import time
16
- from datetime import datetime
17
- import gradio as gr
18
- import os
19
- import traceback
20
 
21
- # --- Funciones Utilitarias ---
22
- def clean_text(text):
23
- if not isinstance(text, str): return ""
24
- text = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', text)
25
- return re.sub(r'\s+', ' ', text).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- def extract_inst_anio_from_url(url):
28
- parsed_url = urlparse(url)
29
- path_parts = [part for part in parsed_url.path.split('/') if part]
30
- inst_codigo, anio = "desconocida", "sin_año"
31
  try:
32
- inst_index = path_parts.index('instituciones') + 1
33
- if inst_index < len(path_parts): inst_codigo = path_parts[inst_index]
34
- audiencias_index = path_parts.index('audiencias') + 1
35
- if audiencias_index < len(path_parts) and path_parts[audiencias_index].isdigit():
36
- potential_anio = path_parts[audiencias_index]
37
- if 2000 <= int(potential_anio) <= datetime.now().year + 5: anio = potential_anio
38
- except (ValueError, IndexError): pass
39
- return inst_codigo, anio
40
-
41
- # --- Clase de Scraper Robusto con Selenium ---
42
- class SeleniumLobbyScraper:
43
- def __init__(self, initial_audiencias_url):
44
- if not initial_audiencias_url or not (initial_audiencias_url.startswith('http://') or initial_audiencias_url.startswith('https://')):
45
- raise ValueError("La URL inicial debe ser una URL HTTP o HTTPS válida.")
46
-
47
- self.initial_audiencias_url = initial_audiencias_url
48
- parsed = urlparse(initial_audiencias_url)
49
- self.base_url = f"{parsed.scheme}://{parsed.netloc}"
50
- self.institucion_codigo, self.anio = extract_inst_anio_from_url(initial_audiencias_url)
51
- self.all_audiences_data = []
52
- self.driver = None
53
-
54
- def setup_driver(self):
55
- print("Configurando el navegador virtual (Chrome)...")
56
- options = webdriver.ChromeOptions()
57
- # Argumentos esenciales para entornos como Hugging Face Spaces
58
- options.add_argument("--headless=new") # Nuevo método para modo headless
59
- options.add_argument("--no-sandbox")
60
- options.add_argument("--disable-dev-shm-usage")
61
- options.add_argument("--disable-gpu")
62
- options.add_argument("--disable-extensions") # Deshabilitar extensiones
63
- options.add_argument("--window-size=1920,1080")
64
- options.add_argument("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")
65
 
66
- # En HF Spaces, especificar la ruta del binario de Chrome instalado vía packages.txt
67
- if os.path.exists("/usr/bin/google-chrome"):
68
- print("Usando el binario de Chrome de /usr/bin/google-chrome")
69
- options.binary_location = "/usr/bin/google-chrome"
70
-
71
- # Crear instancia de Selenium. Selenium Manager se encargará del driver automáticamente.
72
- try:
73
- print("Iniciando Selenium. Selenium Manager se encargará de encontrar/descargar el driver...")
74
- self.driver = webdriver.Chrome(options=options)
75
- print("Navegador virtual configurado exitosamente.")
76
- except Exception as e:
77
- print("Error FATAL al configurar Selenium. Verifica que el Dockerfile haya instalado Chrome correctamente.")
78
- traceback.print_exc()
79
- raise e
80
 
81
- def shutdown_driver(self):
82
- if self.driver:
83
- self.driver.quit()
84
- print("Navegador virtual cerrado.")
85
-
86
- async def get_audience_detail_urls(self):
87
- print("Navegando a la página inicial y esperando contenido dinámico...")
88
- self.driver.get(self.initial_audiencias_url)
89
- all_detail_urls = set()
90
- page_num = 1
91
 
92
- while True:
93
- await asyncio.sleep(random.uniform(2, 4)) # Pequeña pausa para estabilidad
94
- try:
95
- # Espera a que la tabla o lista de audiencias sea visible
96
- wait = WebDriverWait(self.driver, 20)
97
- # Selectores genéricos para una tabla de datos. Si falla, es lo primero a ajustar.
98
- wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "table.audiencias, table.table, .audiencias-list, #audiencias")))
99
- print(f"Contenido dinámico detectado en la página {page_num}.")
100
-
101
- # Extraer todos los enlaces "Ver Detalle" de la página actual
102
- # Selector genérico que busca cualquier enlace 'a' que contenga '/audiencias/detalle/'
103
- detail_links = self.driver.find_elements(By.CSS_SELECTOR, 'a[href*="/audiencias/detalle/"]')
104
- if not detail_links:
105
- print(f"ADVERTENCIA: No se encontraron enlaces de detalle en la página {page_num}.")
106
-
107
- for link in detail_links:
108
- href = link.get_attribute('href')
109
- if href: all_detail_urls.add(href)
110
- print(f"Recolectados {len(detail_links)} enlaces en la página {page_num}. Total únicos: {len(all_detail_urls)}")
111
-
112
- # Intentar ir a la siguiente página
113
- # Selector genérico para un botón de paginación "Siguiente".
114
- next_button = self.driver.find_element(By.CSS_SELECTOR, "li.pagination-next:not(.disabled) a, a.page-link[aria-label='Next']")
115
- print("Botón 'Siguiente' encontrado, haciendo clic...")
116
- self.driver.execute_script("arguments[0].click();", next_button) # Click con JS para evitar problemas de "interactability"
117
- page_num += 1
118
-
119
- except TimeoutException:
120
- print("Timeout esperando el contenido de la tabla en la página. Asumiendo que no hay más audiencias.")
121
- break # Sale si el contenido principal nunca aparece
122
- except NoSuchElementException:
123
- print("No se encontró el botón 'Siguiente' o ya está deshabilitado. Finalizando paginación.")
124
- break # Sale del bucle si no hay botón "Siguiente"
125
 
126
- return list(all_detail_urls)
127
-
128
- async def extract_audience_detail(self, detail_url):
129
- try:
130
- self.driver.get(detail_url)
131
- wait = WebDriverWait(self.driver, 20)
132
- # Esperar a que un elemento clave de la página de detalle sea visible
133
- # Selector genérico, si falla, es lo tercero a ajustar.
134
- wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.materia, div.info-audiencia, #detalle_audiencia")))
135
-
136
- soup = BeautifulSoup(self.driver.page_source, 'html.parser')
137
-
138
- data = {"Link Audiencia": detail_url, "Identificador Audiencia": detail_url.split('/')[-1]}
139
-
140
- # --- Extracción de datos con selectores genéricos y manejo de errores ---
141
- # Intenta con varios selectores comunes por cada campo. Si ninguno funciona, deja el campo vacío.
142
-
143
- # Fecha y Hora
144
- fecha_hora_elem = soup.select_one(".fecha-audiencia, .audiencia-fecha, #fecha_audiencia")
145
- fecha_hora_text = clean_text(fecha_hora_elem.get_text()) if fecha_hora_elem else ""
146
- data['Fecha'], data['Hora'] = "", ""
147
- if fecha_hora_text:
148
- try: dt_obj = datetime.strptime(fecha_hora_text.strip(), '%d/%m/%Y %H:%M'); data['Fecha'], data['Hora'] = dt_obj.strftime('%Y-%m-%d'), dt_obj.strftime('%H:%M')
149
- except ValueError: parts = fecha_hora_text.strip().split(maxsplit=1); data['Fecha'], data['Hora'] = parts[0] if parts else fecha_hora_text, parts[1] if len(parts)>1 else ""
150
-
151
- # Funcionario
152
- func_nombre = soup.select_one(".funcionario-nombre, .nombre-funcionario, #funcionario_nombre")
153
- func_cargo = soup.select_one(".funcionario-cargo, .cargo-funcionario, #funcionario_cargo")
154
- data['Funcionario (nombre, cargo, código)'] = f"{clean_text(func_nombre.get_text()) if func_nombre else 'N/A'} ({clean_text(func_cargo.get_text()) if func_cargo else 'N/A'}, N/A)"
155
-
156
- # Materia y Detalle
157
- data['Materia'] = clean_text(soup.select_one(".materia, .audiencia-materia, #materia_audiencia").get_text()) if soup.select_one(".materia, .audiencia-materia, #materia_audiencia") else ""
158
- data['Detalle'] = clean_text(soup.select_one(".detalle, .audiencia-detalle, #detalle_audiencia").get_text()) if soup.select_one(".detalle, .audiencia-detalle, #detalle_audiencia") else ""
159
 
160
- # Gestores y Representados
161
- gestores_elems = soup.select(".ficha-gestor, .gestor-item, .info-gestor")
162
- gestores_representados_list = []
163
- if not gestores_elems: gestores_representados_list.append({'Gestor Nombre': '', 'Gestor Empresa': '', 'Representados': ''})
164
- else:
165
- for gestor_elem in gestores_elems:
166
- nombre = clean_text(gestor_elem.select_one(".nombre-gestor, .gestor-nombre").get_text()) if gestor_elem.select_one(".nombre-gestor, .gestor-nombre") else ""
167
- empresa = clean_text(gestor_elem.select_one(".empresa-gestor, .gestor-empresa").get_text()) if gestor_elem.select_one(".empresa-gestor, .gestor-empresa") else ""
168
- representados_nombres = ", ".join([clean_text(rep.get_text()) for rep in gestor_elem.select(".lista-representados li, .representado-item")])
169
- gestores_representados_list.append({'Gestor Nombre': nombre, 'Gestor Empresa': empresa, 'Representados': representados_nombres})
170
-
171
- # Participantes
172
- participantes_elems = soup.select(".lista-participantes li, .participante-item")
173
- participantes_list = []
174
- for part_elem in participantes_elems:
175
- nombre = clean_text(part_elem.select_one(".nombre-participante, .nombre").get_text()) if part_elem.select_one(".nombre-participante, .nombre") else ""
176
- rol = clean_text(part_elem.select_one(".rol-participante, .rol").get_text()) if part_elem.select_one(".rol-participante, .rol") else ""
177
- if nombre or rol: participantes_list.append(f"{nombre} ({rol})")
178
- data['Participantes (rol)'] = "; ".join(participantes_list)
179
-
180
- # Aplanar datos
181
- flattened_rows = []
182
- for gr in gestores_representados_list:
183
- row = data.copy()
184
- nombre_f, empresa_f = gr.get('Gestor Nombre','').strip(), gr.get('Gestor Empresa','').strip()
185
- if nombre_f and empresa_f: row['Gestor de intereses (nombre, empresa)'] = f"{nombre_f} ({empresa_f})"
186
- elif nombre_f: row['Gestor de intereses (nombre, empresa)'] = nombre_f
187
- elif empresa_f: row['Gestor de intereses (nombre, empresa)'] = empresa_f
188
- else: row['Gestor de intereses (nombre, empresa)'] = ""
189
- row['Representados'] = gr.get('Representados','')
190
- flattened_rows.append(row)
191
-
192
- return flattened_rows
193
-
194
- except Exception as e:
195
- print(f"Error EXCEPCIONAL al procesar {detail_url}: {e}"); traceback.print_exc()
196
- return [{"Link Audiencia": detail_url, "Identificador Audiencia": detail_url.split('/')[-1], "Fecha": "Error Parse", "Hora": "Error Parse", "Funcionario (nombre, cargo, código)": "Error Parse", "Gestor de intereses (nombre, empresa)": "Error Parse", "Representados": "Error Parse", "Materia": "Error Parse", "Detalle": "Error Parse", "Participantes (rol)": "Error Parse", "Temas detectados": "Error Parse"}]
197
-
198
- async def run(self):
199
- try:
200
- yield "Configurando navegador virtual...", "Procesando...", None, None
201
- self.setup_driver()
202
-
203
- yield "Recolectando URLs de detalle...", "Navegando y esperando JavaScript...", None, None
204
- audiencia_detail_urls = await self.get_audience_detail_urls()
205
-
206
- if not audiencia_detail_urls:
207
- summary_no_urls = "No se encontraron URLs de detalle para extraer.\n\n**Posibles causas:**\n1. No hay audiencias publicadas para la URL/fecha.\n2. Los selectores CSS genéricos no coinciden con la estructura del sitio.\n3. El sitio requiere una interacción más compleja que la actual.\n\nEl proceso ha finalizado."
208
- yield "Proceso finalizado: No se encontraron URLs.", summary_no_urls, None, None
209
- return
210
 
211
- yield f"Recolectadas {len(audiencia_detail_urls)} URLs. Extrayendo detalles...", "Procesando...", None, None
212
-
213
- # Usamos un bucle for secuencial para la extracción para mayor estabilidad con Selenium
214
- all_results = []
215
- for i, url in enumerate(audiencia_detail_urls):
216
- print(f"Procesando detalle {i+1}/{len(audiencia_detail_urls)}: {url}")
217
- yield f"Extrayendo detalle {i+1}/{len(audiencia_detail_urls)}...", f"URL: {url}", None, None
218
- result_list = await self.extract_audience_detail(url)
219
- all_results.append(result_list)
220
-
221
- self.all_audiences_data = [item for sublist in all_results for item in sublist]
222
-
223
- print(f"Extracción completa. Total de registros: {len(self.all_audiences_data)}")
224
-
225
- # Generate final summary and files
226
- df = pd.DataFrame(self.all_audiences_data)
227
- required_cols_final = ['Fecha', 'Hora', 'Identificador Audiencia', 'Link Audiencia', 'Funcionario (nombre, cargo, código)', 'Gestor de intereses (nombre, empresa)', 'Representados', 'Materia', 'Detalle', 'Participantes (rol)']
228
- # FIX: Corrected syntax for creating columns if not exists
229
- for col in required_cols_final:
230
- if col not in df.columns:
231
- df[col] = None
232
- df = df[required_cols_final]
233
-
234
- summary_analysis = "✅ ¡Extracción completada!\n\n"
235
- df_success = df[~df['Fecha'].astype(str).str.startswith('Error')].copy()
236
- summary_analysis += f"**Total de audiencias únicas procesadas exitosamente:** {df_success['Link Audiencia'].nunique()}\n"
237
- summary_analysis += f"**Total de registros generados (incluyendo duplicados por gestor):** {len(df_success)}\n"
238
-
239
- if len(df) > len(df_success):
240
- summary_analysis += f"**Audiencias con errores de extracción:** {len(df) - len(df_success)}\n"
241
 
242
- # Exportar archivos
243
- timestamp = datetime.now().strftime('%Y%m%d_%H%M%S'); output_dir = "output_data"; os.makedirs(output_dir, exist_ok=True)
244
- csv_filename = os.path.join(output_dir, f"leylobby_audiencias_{self.institucion_codigo}_{self.anio}_{timestamp}.csv")
245
- df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
246
-
247
- yield "Proceso finalizado.", summary_analysis, csv_filename, None
248
-
249
- except Exception as e:
250
- print(f"Error crítico en el scraper: {e}"); traceback.print_exc()
251
- yield "Error crítico.", f"Ocurrió un error grave: {e}\n\n{traceback.format_exc()}", None, None
252
- finally:
253
- self.shutdown_driver()
254
 
255
-
256
- # --- Interfaz Gradio ---
257
- def create_interface():
258
- with gr.Blocks(title="🤖 Ley Lobby Scraper Robusto", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray")) as demo:
259
- gr.HTML("""<div style="text-align: center; background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%); color: white; padding: 25px; border-radius: 15px; margin-bottom: 25px;">
260
- <h1>🤖 Ley Lobby Scraper Robusto</h1>
261
- <p>Extractor inteligente que usa un navegador virtual para sortear defensas comunes y ejecutar JavaScript.</p></div>""")
262
-
263
- with gr.Row():
264
- url_input = gr.Textbox(label="🌐 URL de Audiencias", placeholder="https://www.leylobby.gob.cl/instituciones/AO001/audiencias/2025", info="Introduce la URL principal de audiencias.")
265
- scrape_btn = gr.Button("🚀 Iniciar Extracción Inteligente", variant="primary", size="lg")
266
 
267
- with gr.Group():
268
- status_output = gr.Textbox(label="📊 Estado del Proceso", lines=3, interactive=False, autoscroll=True)
269
- summary_output = gr.Textbox(label="📋 Resumen Ejecutivo", lines=10, interactive=False, autoscroll=True)
270
-
271
- with gr.Row():
272
- download_file_csv = gr.File(label="📥 Descargar Reporte CSV Completo", interactive=False)
273
- download_file_txt = gr.File(label="📥 Descargar Resumen TXT", interactive=False)
274
-
275
- async def run_task(initial_url):
276
- if not initial_url or not (initial_url.startswith('http://') or initial_url.startswith('https://')):
277
- yield "Error: URL inválida.", "Por favor, introduce una URL válida.", None, None
278
- return
279
- try:
280
- scraper = SeleniumLobbyScraper(initial_url)
281
- async for status, summary, csv_file, txt_file in scraper.run():
282
- yield status, summary, csv_file, txt_file
283
- except Exception as e:
284
- yield "Error Crítico", f"Error: {e}\n{traceback.format_exc()}", None, None
285
-
286
- scrape_btn.click(
287
- fn=run_task,
288
- inputs=[url_input],
289
- outputs=[status_output, summary_output, download_file_csv, download_file_txt]
290
- )
 
 
 
 
 
 
 
 
 
 
 
 
291
 
292
- gr.Markdown("### ¿Cómo funciona?\nEste sistema utiliza un navegador web virtual (Selenium con Chrome) para cargar completamente las páginas, incluyendo contenido dinámico de JavaScript. Navega automáticamente a través de la paginación para encontrar todas las audiencias y luego extrae los detalles de cada una. Esto lo hace mucho más resistente a los sitios web modernos que los scrapers tradicionales.")
293
-
294
- return demo
295
-
296
- # --- Bloque principal para ejecutar la aplicación Gradio ---
297
- if __name__ == "__main__":
298
- # Necesitarás instalar las dependencias: pip install selenium webdriver-manager
299
- print("Iniciando aplicación Gradio con scraper basado en Selenium...")
300
- demo = create_interface()
301
- demo.launch(server_name="0.0.0.0", server_port=7860)
302
- print("Aplicación Gradio lanzada.")
 
1
+ import os
2
+ import requests
3
+ from flask import Flask, render_template, request, flash, redirect, url_for
4
  from bs4 import BeautifulSoup
 
 
 
 
 
 
 
5
  import pandas as pd
 
 
6
  import time
 
 
 
 
7
 
8
+ # --- Inicialización de la Aplicación Flask ---
9
+ app = Flask(__name__)
10
+ # Se necesita una clave secreta para mostrar mensajes (flashing)
11
+ app.secret_key = 'supersecretkey'
12
+
13
+ # --- Lógica de Web Scraping ---
14
+ def scrape_lobby_data():
15
+ """
16
+ Función que realiza el web scraping en el sitio de Ley de Lobby,
17
+ extrae los datos de audiencias y los guarda en archivos TXT y CSV.
18
+ """
19
+ # URL del sitio a scrapear para el año 2025
20
+ url = "https://www.leylobby.gob.cl/instituciones/AO001/audiencias/2025"
21
+
22
+ # --- Consideración Ética ---
23
+ # Es una buena práctica identificarse. Algunos sitios web pueden bloquear
24
+ # solicitudes sin un User-Agent reconocible.
25
+ headers = {
26
+ 'User-Agent': 'EthicalScraper/1.0 (contacto@ejemplo.com) - Script para proyecto educativo'
27
+ }
28
 
 
 
 
 
29
  try:
30
+ # Realizar la solicitud GET para obtener el contenido de la página
31
+ print(f"Obteniendo datos desde: {url}")
32
+ response = requests.get(url, headers=headers, timeout=15)
33
+ # Genera un error si la solicitud no fue exitosa (ej. error 404, 500)
34
+ response.raise_for_status()
35
+
36
+ # --- Parseo del HTML con BeautifulSoup ---
37
+ soup = BeautifulSoup(response.content, 'html.parser')
38
+
39
+ # Encontrar la tabla que contiene los datos de las audiencias
40
+ # Se debe inspeccionar el HTML del sitio para encontrar los selectores correctos.
41
+ table = soup.find('table', class_='table-striped')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ if not table:
44
+ print("No se encontró la tabla de datos. El sitio puede haber cambiado su estructura.")
45
+ return False, "No se pudo encontrar la tabla de datos en la página."
 
 
 
 
 
 
 
 
 
 
 
46
 
47
+ # --- Extracción de Datos ---
48
+ # Extraer los encabezados de la tabla
49
+ headers_list = [th.get_text(strip=True) for th in table.find('thead').find_all('th')]
 
 
 
 
 
 
 
50
 
51
+ # Extraer las filas de datos de la tabla
52
+ data_rows = []
53
+ for row in table.find('tbody').find_all('tr'):
54
+ columns = [td.get_text(strip=True) for td in row.find_all('td')]
55
+ data_rows.append(columns)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
+ if not data_rows:
58
+ return False, "La tabla fue encontrada, pero no contenía datos de audiencias."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
+ # --- Guardado de Archivos ---
61
+ # Crear un DataFrame de pandas con los datos extraídos
62
+ df = pd.DataFrame(data_rows, columns=headers_list)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
 
64
+ # 1. Guardar en formato CSV
65
+ df.to_csv('audiencias_lobby.csv', index=False, encoding='utf-8-sig')
66
+ print("Datos guardados exitosamente en 'audiencias_lobby.csv'")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
 
68
+ # 2. Guardar en formato TXT (formato de texto plano separado por comas)
69
+ df.to_csv('audiencias_lobby.txt', index=False, sep='\t')
70
+ print("Datos guardados exitosamente en 'audiencias_lobby.txt'")
 
 
 
 
 
 
 
 
 
71
 
72
+ # --- Pausa Ética ---
73
+ # Si fueras a hacer más solicitudes, es bueno esperar un momento.
74
+ time.sleep(1)
 
 
 
 
 
 
 
 
75
 
76
+ return True, f"¡Éxito! Se extrajeron {len(data_rows)} registros y se guardaron en 'audiencias_lobby.csv' y 'audiencias_lobby.txt'."
77
+
78
+ except requests.exceptions.RequestException as e:
79
+ # Capturar errores de red (ej. sin conexión, DNS no encontrado)
80
+ error_message = f"Error de red al intentar acceder a la URL: {e}"
81
+ print(error_message)
82
+ return False, error_message
83
+ except Exception as e:
84
+ # Capturar cualquier otro error inesperado
85
+ error_message = f"Ocurrió un error inesperado: {e}"
86
+ print(error_message)
87
+ return False, error_message
88
+
89
+ # --- Rutas de la Aplicación Web ---
90
+
91
+ @app.route('/')
92
+ def index():
93
+ """
94
+ Renderiza la página principal (index.html).
95
+ """
96
+ return render_template('index.html')
97
+
98
+ @app.route('/scrape', methods=['POST'])
99
+ def run_scraper():
100
+ """
101
+ Esta ruta se activa cuando se hace clic en el botón del formulario.
102
+ Llama a la función de scraping y muestra un mensaje al usuario.
103
+ """
104
+ print("Iniciando proceso de scraping...")
105
+ success, message = scrape_lobby_data()
106
+
107
+ # Muestra un mensaje de éxito o error en la página
108
+ if success:
109
+ flash(message, 'success')
110
+ else:
111
+ flash(message, 'error')
112
 
113
+ # Redirige al usuario de vuelta a la página principal
114
+ return redirect(url_for('index'))
115
+
116
+ # --- Punto de Entrada Principal ---
117
+ if __name__ == '__main__':
118
+ # Inicia el servidor de desarrollo de Flask
119
+ # El debug=True permite ver los errores en el navegador y recarga el servidor automáticamente
120
+ app.run(debug=True)