Lukeetah commited on
Commit
f5cf48c
·
verified ·
1 Parent(s): 307437b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +47 -29
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
- options.add_argument("--headless")
 
61
  options.add_argument("--no-sandbox")
62
  options.add_argument("--disable-dev-shm-usage")
63
  options.add_argument("--disable-gpu")
64
- options.add_argument("--window-size=1920x1080")
 
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
- service = ChromeService(ChromeDriverManager().install())
69
- self.driver = webdriver.Chrome(service=service, options=options)
70
- print("Navegador virtual configurado.")
 
 
 
 
 
 
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) # Aumentado a 20s
88
- # Selector genérico para una tabla de datos. Si falla, es lo primero a ajustar.
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}. Puede que el selector 'a[href*=\"/audiencias/detalle/\"]' sea incorrecto o no haya más audiencias.")
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". Si falla, es lo segundo a ajustar.
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, pd.DataFrame()
192
  self.setup_driver()
193
 
194
- yield "Recolectando URLs de detalle...", "Navegando y esperando JavaScript...", None, None, pd.DataFrame()
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, pd.DataFrame()
200
  return
201
 
202
- yield f"Recolectadas {len(audiencia_detail_urls)} URLs. Extrayendo detalles...", "Procesando...", None, None, pd.DataFrame()
203
 
204
- tasks = [self.extract_audience_detail(url) for url in audiencia_detail_urls]
205
- results = await asyncio.gather(*tasks)
 
 
 
 
 
206
 
207
- self.all_audiences_data = [item for sublist in results for item in sublist]
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, df_success.head(10)
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, pd.DataFrame()
236
  finally:
237
  self.shutdown_driver()
238
 
239
 
240
  # --- Interfaz Gradio ---
241
  def create_interface():
242
- with gr.Blocks(title="🤖 Ley Lobby Scraper Adaptativo", theme=gr.themes.Soft(primary_hue="blue", secondary_hue="gray")) as demo:
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.Markdown(label="📋 Resumen Ejecutivo")
254
 
255
  with gr.Row():
256
  download_file_csv = gr.File(label="📥 Descargar Reporte CSV Completo", interactive=False)
257
- preview_table = gr.DataFrame(label="👀 Vista Previa (Datos Exitosos)", interactive=False)
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, pd.DataFrame()
262
  return
263
  try:
264
  scraper = SeleniumLobbyScraper(initial_url)
265
- async for status, summary, csv_file, _, preview_df in scraper.run():
266
- yield status, summary, csv_file, preview_df
267
  except Exception as e:
268
- yield "Error Crítico", f"Error: {e}\n{traceback.format_exc()}", None, pd.DataFrame()
269
 
270
  scrape_btn.click(
271
  fn=run_task,
272
  inputs=[url_input],
273
- outputs=[status_output, summary_output, download_file_csv, preview_table]
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.")