Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,302 +1,120 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
import
|
| 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 |
-
# ---
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
#
|
| 42 |
-
|
| 43 |
-
|
| 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 |
-
|
| 67 |
-
|
| 68 |
-
|
| 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 |
-
|
| 82 |
-
|
| 83 |
-
|
| 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 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 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 |
-
|
| 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 |
-
|
| 161 |
-
|
| 162 |
-
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
|
| 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 |
-
|
| 243 |
-
|
| 244 |
-
|
| 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 |
-
#
|
| 257 |
-
|
| 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 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 291 |
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
#
|
| 299 |
-
|
| 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)
|
|
|
|
|
|
|
|
|