Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -57,17 +57,30 @@ class SeleniumLobbyScraper:
|
|
| 57 |
def setup_driver(self):
|
| 58 |
print("Configurando el navegador virtual (Chrome)...")
|
| 59 |
options = webdriver.ChromeOptions()
|
| 60 |
-
|
|
|
|
| 61 |
options.add_argument("--no-sandbox")
|
| 62 |
options.add_argument("--disable-dev-shm-usage")
|
| 63 |
options.add_argument("--disable-gpu")
|
| 64 |
-
options.add_argument("--
|
|
|
|
| 65 |
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")
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
# Instala y configura el driver de Chrome automáticamente
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
def shutdown_driver(self):
|
| 73 |
if self.driver:
|
|
@@ -84,16 +97,16 @@ class SeleniumLobbyScraper:
|
|
| 84 |
await asyncio.sleep(random.uniform(2, 4)) # Pequeña pausa para estabilidad
|
| 85 |
try:
|
| 86 |
# Espera a que la tabla o lista de audiencias sea visible
|
| 87 |
-
wait = WebDriverWait(self.driver, 20)
|
| 88 |
-
#
|
| 89 |
-
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "table.audiencias, table.table, .audiencias-list")))
|
| 90 |
print(f"Contenido dinámico detectado en la página {page_num}.")
|
| 91 |
|
| 92 |
# Extraer todos los enlaces "Ver Detalle" de la página actual
|
| 93 |
# Selector genérico que busca cualquier enlace 'a' que contenga '/audiencias/detalle/'
|
| 94 |
detail_links = self.driver.find_elements(By.CSS_SELECTOR, 'a[href*="/audiencias/detalle/"]')
|
| 95 |
if not detail_links:
|
| 96 |
-
print(f"ADVERTENCIA: No se encontraron enlaces de detalle en la página {page_num}.
|
| 97 |
|
| 98 |
for link in detail_links:
|
| 99 |
href = link.get_attribute('href')
|
|
@@ -101,7 +114,7 @@ class SeleniumLobbyScraper:
|
|
| 101 |
print(f"Recolectados {len(detail_links)} enlaces en la página {page_num}. Total únicos: {len(all_detail_urls)}")
|
| 102 |
|
| 103 |
# Intentar ir a la siguiente página
|
| 104 |
-
# Selector genérico para un botón de paginación "Siguiente".
|
| 105 |
next_button = self.driver.find_element(By.CSS_SELECTOR, "li.pagination-next:not(.disabled) a, a.page-link[aria-label='Next']")
|
| 106 |
print("Botón 'Siguiente' encontrado, haciendo clic...")
|
| 107 |
self.driver.execute_script("arguments[0].click();", next_button) # Click con JS para evitar problemas de "interactability"
|
|
@@ -122,7 +135,7 @@ class SeleniumLobbyScraper:
|
|
| 122 |
wait = WebDriverWait(self.driver, 20)
|
| 123 |
# Esperar a que un elemento clave de la página de detalle sea visible
|
| 124 |
# Selector genérico, si falla, es lo tercero a ajustar.
|
| 125 |
-
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.materia, div.info-audiencia")))
|
| 126 |
|
| 127 |
soup = BeautifulSoup(self.driver.page_source, 'html.parser')
|
| 128 |
|
|
@@ -188,23 +201,28 @@ class SeleniumLobbyScraper:
|
|
| 188 |
|
| 189 |
async def run(self):
|
| 190 |
try:
|
| 191 |
-
yield "Configurando navegador virtual...", "Procesando...", None, None
|
| 192 |
self.setup_driver()
|
| 193 |
|
| 194 |
-
yield "Recolectando URLs de detalle...", "Navegando y esperando JavaScript...", None, None
|
| 195 |
audiencia_detail_urls = await self.get_audience_detail_urls()
|
| 196 |
|
| 197 |
if not audiencia_detail_urls:
|
| 198 |
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."
|
| 199 |
-
yield "Proceso finalizado: No se encontraron URLs.", summary_no_urls, None, None
|
| 200 |
return
|
| 201 |
|
| 202 |
-
yield f"Recolectadas {len(audiencia_detail_urls)} URLs. Extrayendo detalles...", "Procesando...", None, None
|
| 203 |
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
|
| 207 |
-
self.all_audiences_data = [item for sublist in
|
| 208 |
|
| 209 |
print(f"Extracción completa. Total de registros: {len(self.all_audiences_data)}")
|
| 210 |
|
|
@@ -228,18 +246,18 @@ class SeleniumLobbyScraper:
|
|
| 228 |
csv_filename = os.path.join(output_dir, f"leylobby_audiencias_{self.institucion_codigo}_{self.anio}_{timestamp}.csv")
|
| 229 |
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
|
| 230 |
|
| 231 |
-
yield "Proceso finalizado.", summary_analysis, csv_filename, None
|
| 232 |
|
| 233 |
except Exception as e:
|
| 234 |
print(f"Error crítico en el scraper: {e}"); traceback.print_exc()
|
| 235 |
-
yield "Error crítico.", f"Ocurrió un error grave: {e}\n\n{traceback.format_exc()}", None, None
|
| 236 |
finally:
|
| 237 |
self.shutdown_driver()
|
| 238 |
|
| 239 |
|
| 240 |
# --- Interfaz Gradio ---
|
| 241 |
def create_interface():
|
| 242 |
-
with gr.Blocks(title="🤖 Ley Lobby Scraper
|
| 243 |
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;">
|
| 244 |
<h1>🤖 Ley Lobby Scraper Robusto</h1>
|
| 245 |
<p>Extractor inteligente que usa un navegador virtual para sortear defensas comunes y ejecutar JavaScript.</p></div>""")
|
|
@@ -250,27 +268,27 @@ def create_interface():
|
|
| 250 |
|
| 251 |
with gr.Group():
|
| 252 |
status_output = gr.Textbox(label="📊 Estado del Proceso", lines=3, interactive=False, autoscroll=True)
|
| 253 |
-
summary_output = gr.
|
| 254 |
|
| 255 |
with gr.Row():
|
| 256 |
download_file_csv = gr.File(label="📥 Descargar Reporte CSV Completo", interactive=False)
|
| 257 |
-
|
| 258 |
-
|
| 259 |
async def run_task(initial_url):
|
| 260 |
if not initial_url or not (initial_url.startswith('http://') or initial_url.startswith('https://')):
|
| 261 |
-
yield "Error: URL inválida.", "Por favor, introduce una URL válida.", None,
|
| 262 |
return
|
| 263 |
try:
|
| 264 |
scraper = SeleniumLobbyScraper(initial_url)
|
| 265 |
-
async for status, summary, csv_file,
|
| 266 |
-
yield status, summary, csv_file,
|
| 267 |
except Exception as e:
|
| 268 |
-
yield "Error Crítico", f"Error: {e}\n{traceback.format_exc()}", None,
|
| 269 |
|
| 270 |
scrape_btn.click(
|
| 271 |
fn=run_task,
|
| 272 |
inputs=[url_input],
|
| 273 |
-
outputs=[status_output, summary_output, download_file_csv,
|
| 274 |
)
|
| 275 |
|
| 276 |
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.")
|
|
|
|
| 57 |
def setup_driver(self):
|
| 58 |
print("Configurando el navegador virtual (Chrome)...")
|
| 59 |
options = webdriver.ChromeOptions()
|
| 60 |
+
# Argumentos esenciales para entornos como Hugging Face Spaces
|
| 61 |
+
options.add_argument("--headless=new") # Nuevo método para modo headless
|
| 62 |
options.add_argument("--no-sandbox")
|
| 63 |
options.add_argument("--disable-dev-shm-usage")
|
| 64 |
options.add_argument("--disable-gpu")
|
| 65 |
+
options.add_argument("--disable-extensions") # Deshabilitar extensiones
|
| 66 |
+
options.add_argument("--window-size=1920,1080")
|
| 67 |
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")
|
| 68 |
|
| 69 |
+
# En HF Spaces, especificar la ruta del binario de Chrome instalado vía packages.txt
|
| 70 |
+
if os.path.exists("/usr/bin/chromium-browser"):
|
| 71 |
+
print("Usando el binario de Chrome de /usr/bin/chromium-browser")
|
| 72 |
+
options.binary_location = "/usr/bin/chromium-browser"
|
| 73 |
+
|
| 74 |
# Instala y configura el driver de Chrome automáticamente
|
| 75 |
+
try:
|
| 76 |
+
print("Instalando/Cacheando chromedriver con webdriver-manager...")
|
| 77 |
+
service = ChromeService(ChromeDriverManager().install())
|
| 78 |
+
self.driver = webdriver.Chrome(service=service, options=options)
|
| 79 |
+
print("Navegador virtual configurado exitosamente.")
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print("Error FATAL al configurar Selenium. Verifica las dependencias en packages.txt.")
|
| 82 |
+
traceback.print_exc()
|
| 83 |
+
raise e
|
| 84 |
|
| 85 |
def shutdown_driver(self):
|
| 86 |
if self.driver:
|
|
|
|
| 97 |
await asyncio.sleep(random.uniform(2, 4)) # Pequeña pausa para estabilidad
|
| 98 |
try:
|
| 99 |
# Espera a que la tabla o lista de audiencias sea visible
|
| 100 |
+
wait = WebDriverWait(self.driver, 20)
|
| 101 |
+
# Selectores genéricos para una tabla de datos. Si falla, es lo primero a ajustar.
|
| 102 |
+
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "table.audiencias, table.table, .audiencias-list, #audiencias")))
|
| 103 |
print(f"Contenido dinámico detectado en la página {page_num}.")
|
| 104 |
|
| 105 |
# Extraer todos los enlaces "Ver Detalle" de la página actual
|
| 106 |
# Selector genérico que busca cualquier enlace 'a' que contenga '/audiencias/detalle/'
|
| 107 |
detail_links = self.driver.find_elements(By.CSS_SELECTOR, 'a[href*="/audiencias/detalle/"]')
|
| 108 |
if not detail_links:
|
| 109 |
+
print(f"ADVERTENCIA: No se encontraron enlaces de detalle en la página {page_num}.")
|
| 110 |
|
| 111 |
for link in detail_links:
|
| 112 |
href = link.get_attribute('href')
|
|
|
|
| 114 |
print(f"Recolectados {len(detail_links)} enlaces en la página {page_num}. Total únicos: {len(all_detail_urls)}")
|
| 115 |
|
| 116 |
# Intentar ir a la siguiente página
|
| 117 |
+
# Selector genérico para un botón de paginación "Siguiente".
|
| 118 |
next_button = self.driver.find_element(By.CSS_SELECTOR, "li.pagination-next:not(.disabled) a, a.page-link[aria-label='Next']")
|
| 119 |
print("Botón 'Siguiente' encontrado, haciendo clic...")
|
| 120 |
self.driver.execute_script("arguments[0].click();", next_button) # Click con JS para evitar problemas de "interactability"
|
|
|
|
| 135 |
wait = WebDriverWait(self.driver, 20)
|
| 136 |
# Esperar a que un elemento clave de la página de detalle sea visible
|
| 137 |
# Selector genérico, si falla, es lo tercero a ajustar.
|
| 138 |
+
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "div.materia, div.info-audiencia, #detalle_audiencia")))
|
| 139 |
|
| 140 |
soup = BeautifulSoup(self.driver.page_source, 'html.parser')
|
| 141 |
|
|
|
|
| 201 |
|
| 202 |
async def run(self):
|
| 203 |
try:
|
| 204 |
+
yield "Configurando navegador virtual...", "Procesando...", None, None
|
| 205 |
self.setup_driver()
|
| 206 |
|
| 207 |
+
yield "Recolectando URLs de detalle...", "Navegando y esperando JavaScript...", None, None
|
| 208 |
audiencia_detail_urls = await self.get_audience_detail_urls()
|
| 209 |
|
| 210 |
if not audiencia_detail_urls:
|
| 211 |
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."
|
| 212 |
+
yield "Proceso finalizado: No se encontraron URLs.", summary_no_urls, None, None
|
| 213 |
return
|
| 214 |
|
| 215 |
+
yield f"Recolectadas {len(audiencia_detail_urls)} URLs. Extrayendo detalles...", "Procesando...", None, None
|
| 216 |
|
| 217 |
+
# Usamos un bucle for secuencial para la extracción para mayor estabilidad con Selenium
|
| 218 |
+
all_results = []
|
| 219 |
+
for i, url in enumerate(audiencia_detail_urls):
|
| 220 |
+
print(f"Procesando detalle {i+1}/{len(audiencia_detail_urls)}: {url}")
|
| 221 |
+
yield f"Extrayendo detalle {i+1}/{len(audiencia_detail_urls)}...", f"URL: {url}", None, None
|
| 222 |
+
result_list = await self.extract_audience_detail(url)
|
| 223 |
+
all_results.append(result_list)
|
| 224 |
|
| 225 |
+
self.all_audiences_data = [item for sublist in all_results for item in sublist]
|
| 226 |
|
| 227 |
print(f"Extracción completa. Total de registros: {len(self.all_audiences_data)}")
|
| 228 |
|
|
|
|
| 246 |
csv_filename = os.path.join(output_dir, f"leylobby_audiencias_{self.institucion_codigo}_{self.anio}_{timestamp}.csv")
|
| 247 |
df.to_csv(csv_filename, index=False, encoding='utf-8-sig')
|
| 248 |
|
| 249 |
+
yield "Proceso finalizado.", summary_analysis, csv_filename, None
|
| 250 |
|
| 251 |
except Exception as e:
|
| 252 |
print(f"Error crítico en el scraper: {e}"); traceback.print_exc()
|
| 253 |
+
yield "Error crítico.", f"Ocurrió un error grave: {e}\n\n{traceback.format_exc()}", None, None
|
| 254 |
finally:
|
| 255 |
self.shutdown_driver()
|
| 256 |
|
| 257 |
|
| 258 |
# --- Interfaz Gradio ---
|
| 259 |
def create_interface():
|
| 260 |
+
with gr.Blocks(title="🤖 Ley Lobby Scraper Robusto", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray")) as demo:
|
| 261 |
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;">
|
| 262 |
<h1>🤖 Ley Lobby Scraper Robusto</h1>
|
| 263 |
<p>Extractor inteligente que usa un navegador virtual para sortear defensas comunes y ejecutar JavaScript.</p></div>""")
|
|
|
|
| 268 |
|
| 269 |
with gr.Group():
|
| 270 |
status_output = gr.Textbox(label="📊 Estado del Proceso", lines=3, interactive=False, autoscroll=True)
|
| 271 |
+
summary_output = gr.Textbox(label="📋 Resumen Ejecutivo", lines=10, interactive=False, autoscroll=True)
|
| 272 |
|
| 273 |
with gr.Row():
|
| 274 |
download_file_csv = gr.File(label="📥 Descargar Reporte CSV Completo", interactive=False)
|
| 275 |
+
download_file_txt = gr.File(label="📥 Descargar Resumen TXT", interactive=False)
|
| 276 |
+
|
| 277 |
async def run_task(initial_url):
|
| 278 |
if not initial_url or not (initial_url.startswith('http://') or initial_url.startswith('https://')):
|
| 279 |
+
yield "Error: URL inválida.", "Por favor, introduce una URL válida.", None, None
|
| 280 |
return
|
| 281 |
try:
|
| 282 |
scraper = SeleniumLobbyScraper(initial_url)
|
| 283 |
+
async for status, summary, csv_file, txt_file in scraper.run():
|
| 284 |
+
yield status, summary, csv_file, txt_file
|
| 285 |
except Exception as e:
|
| 286 |
+
yield "Error Crítico", f"Error: {e}\n{traceback.format_exc()}", None, None
|
| 287 |
|
| 288 |
scrape_btn.click(
|
| 289 |
fn=run_task,
|
| 290 |
inputs=[url_input],
|
| 291 |
+
outputs=[status_output, summary_output, download_file_csv, download_file_txt]
|
| 292 |
)
|
| 293 |
|
| 294 |
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.")
|