diff --git a/Readme.md b/Readme.md index 7936936..521fb85 100644 --- a/Readme.md +++ b/Readme.md @@ -7,6 +7,12 @@ psutil => pip install psutil python-vlc => pip install python-vlc +bs4 => pip install bs4 + + + +requests => pip install requests + ## Como Ejecutar ## > [!NOTE] > Desde la carpeta anterior @@ -36,7 +42,7 @@ python -m ProyectoGlobal 2. ~~Temperatura local~~ -3. Programar Alarma (aviso visual y sonoro al pasar X minutos) +3. ~~Programar Alarma (aviso visual y sonoro al pasar X minutos)~~ 4. Scraping diff --git a/logica/T2/scraping.py b/logica/T2/scraping.py index e69de29..973bdc2 100644 --- a/logica/T2/scraping.py +++ b/logica/T2/scraping.py @@ -0,0 +1,148 @@ +import requests +from bs4 import BeautifulSoup +import os +from datetime import datetime +from urllib.parse import quote +from tkinter import \ + messagebox # Para usar messagebox en errores críticos si el Tkinter Loop lo permite (Aunque se recomienda devolver el error) + +# --- CONSTANTES DE CONFIGURACIÓN --- +# URL base para realizar búsquedas en Wikipedia en español. +URL_BASE_BUSQUEDA = "https://es.wikipedia.org/w/index.php?search=" +URL_TERMINACION = "&title=Especial:Buscar&go=Ir" +ARCHIVO_SALIDA = "res/datos_extraidos.txt" + + +def hacer_scraping(termino_busqueda: str): + """ + Construye una URL de búsqueda en Wikipedia con el término proporcionado, + extrae el contenido del primer artículo encontrado y guarda el resultado. + + Args: + termino_busqueda (str): El texto a buscar (ej: "Expedición 30"). + + Returns: + tuple: (bool, str, str) - Éxito (bool), mensaje de estado (str), y contenido extraído (str). + """ + termino_busqueda = termino_busqueda.strip() + + if not termino_busqueda: + return False, "❌ El término de búsqueda no puede estar vacío.", "" + + # 1. Construir la URL de búsqueda + url_codificada = quote(termino_busqueda) + url_final = URL_BASE_BUSQUEDA + url_codificada + URL_TERMINACION + + print(f"🔗 Buscando y extrayendo datos de: {url_final}") + + try: + # 2. Realizar la solicitud HTTP + headers = { + 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/91.0.4472.124 Safari/537.36') + } + response = requests.get(url_final, headers=headers, timeout=15) + response.raise_for_status() + + # 3. Parsear el contenido + soup = BeautifulSoup(response.content, 'html.parser') + + # 4. Intentar encontrar el artículo directamente + title_tag = soup.find('h1', id='firstHeading') + title = title_tag.get_text().strip() if title_tag else "Sin Título" + + # Si el resultado es una lista de búsqueda (no fue a un artículo directo) + if title.startswith('Buscar:'): + # Intentamos extraer el enlace del primer resultado de búsqueda + results_list = soup.find('ul', class_='mw-search-results') + if results_list: + first_result = results_list.find('a') + if first_result: + # Construir la URL completa del primer resultado + new_url = "https://es.wikipedia.org" + first_result['href'] + print(f"➡️ Redirigiendo a primer resultado: {new_url}") + # Llamamos a la función auxiliar con la URL del artículo + return hacer_scraping_articulo(new_url, termino_busqueda) + + # Si no hay resultados o no se puede seguir el enlace: + return False, f"❌ Wikipedia no encontró resultados de artículo para '{termino_busqueda}'.", "" + + # Si encontramos un artículo directamente, extraemos el contenido inmediatamente + else: + return hacer_scraping_articulo(url_final, termino_busqueda, title, soup) + + + except requests.exceptions.RequestException as e: + return False, f"❌ Error de red o HTTP al buscar: {e}", "" + except Exception as e: + return False, f"❌ Error desconocido en la búsqueda: {type(e).__name__}: {e}", "" + + +# --- FUNCIÓN AUXILIAR DE EXTRACCIÓN DE ARTÍCULO --- + +def hacer_scraping_articulo(url_articulo, termino_busqueda, title=None, soup=None): + """ + Función auxiliar para extraer el contenido de una URL de artículo específica de Wikipedia. + """ + try: + # Si no se pasó el objeto soup (ej. llamada desde redirección), hacemos la solicitud + if soup is None: + headers = { + 'User-Agent': ('Mozilla/5.0 (Windows NT 10.0; Win64; x64) ' + 'AppleWebKit/537.36 (KHTML, like Gecko) ' + 'Chrome/91.0.4472.124 Safari/537.36') + } + response = requests.get(url_articulo, headers=headers, timeout=15) + response.raise_for_status() + soup = BeautifulSoup(response.content, 'html.parser') + title_tag = soup.find('h1', id='firstHeading') + title = title_tag.get_text().strip() if title_tag else "Sin Título" + + # 4b. Extraer el Cuerpo de Texto del artículo + # Buscamos en el div principal y excluimos elementos de navegación/metadatos. + content_div = soup.find('div', id='mw-content-text') + + if content_div: + # Extraer párrafos, encabezados y listas dentro del contenido principal + text_elements = content_div.find_all(['p', 'h2', 'h3', 'li']) + full_text = "\n".join(element.get_text().strip() for element in text_elements if element.get_text().strip()) + else: + full_text = "No se pudo encontrar el contenido principal del artículo." + + # Limitar la longitud del texto + max_length = 1500 + extracted_text = full_text[:max_length] + if len(full_text) > max_length: + extracted_text += "\n[... Contenido truncado ...]" + + # 5. Formatear + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + output = ( + f"\n\n==========================================================\n" + f"== 🌐 DATOS EXTRAÍDOS (Búsqueda: '{termino_busqueda}') ==\n" + f"==========================================================\n" + f"URL Artículo: {url_articulo}\n" + f"TÍTULO: {title}\n" + f"FECHA/HORA: {timestamp}\n" + f"----------------------------------------------------------\n" + f"CONTENIDO (Primeros {len(extracted_text)} chars):\n" + f"----------------------------------------------------------\n" + f"{extracted_text}\n" + ) + + # 6. Guardar en archivo + os.makedirs('res', exist_ok=True) + with open(ARCHIVO_SALIDA, 'a', encoding='utf-8') as f: + f.write(output) + + print(f"✅ Extracción completada. Datos guardados en {ARCHIVO_SALIDA}") + + # 7. Devolver el resultado para su visualización en la GUI + return True, f"✅ Extracción de '{title}' (Artículo de Wikipedia) completada.", output + + except requests.exceptions.RequestException as e: + return False, f"❌ Error de red o HTTP al acceder al artículo: {e}", "" + except Exception as e: + return False, f"❌ Error desconocido en la extracción del artículo: {type(e).__name__}: {e}", "" \ No newline at end of file diff --git a/vista/panel_central.py b/vista/panel_central.py index 78e589f..eddd073 100644 --- a/vista/panel_central.py +++ b/vista/panel_central.py @@ -497,7 +497,7 @@ class PanelCentral(ttk.Frame): sub_notebook = ttk.Notebook(parent_frame) sub_notebook.grid(row=0, column=0, sticky="nsew") - sub_tabs = ["Recursos", "Resultados", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"] + sub_tabs = ["Recursos", "Carrera", "Radios", "Navegador", "Correos", "Tareas", "Alarmas", "Enlaces"] self.tabs = {} for i, sub_tab_text in enumerate(sub_tabs): @@ -513,7 +513,7 @@ class PanelCentral(ttk.Frame): self.canvas_widget = self.canvas.get_tk_widget() self.canvas_widget.pack(expand=True, fill="both") - elif sub_tab_text == "Resultados": + elif sub_tab_text == "Carrera": self.crear_interfaz_carrera(frame) elif sub_tab_text == "Radios": diff --git a/vista/panel_lateral.py b/vista/panel_lateral.py index e01a97f..dcbcaab 100644 --- a/vista/panel_lateral.py +++ b/vista/panel_lateral.py @@ -6,36 +6,35 @@ from tkinter import messagebox from logica.controlador import accion_placeholder from logica.T1.backup import accion_backup_t1 from logica.T1.runVScode import abrir_vscode -# NO necesitamos importar cargar/guardar notas aquí, ya que la lógica se mueve al Panel Central -# from logica.T1.textEditor import cargar_contenido_res_notes, guardar_contenido_res_notes from logica.T1.openBrowser import navegar_a_url +from logica.T2.scraping import hacer_scraping # <--- NUEVA IMPORTACIÓN DE SCRAPING # --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py --- +# Asumo que este archivo existe y contiene las constantes de color/fuente from vista.config import * - class PanelLateral(ttk.Frame): """Contiene el menú de botones y entradas para las tareas.""" - # Usamos la constante importada ANCHO_CARACTERES_FIJO = ANCHO_CARACTERES_PANEL_LATERAL def __init__(self, parent, central_panel=None, *args, **kwargs): super().__init__(parent, *args, **kwargs) - # La referencia al PanelCentral es esencial para iniciar la carrera y acceder a sus métodos self.central_panel = central_panel self.configurar_estilos_locales(parent) - # 1. Entrada superior (amarilla) + # 1. Entrada superior (barra de entrada) self.entrada_superior = ttk.Entry(self, width=self.ANCHO_CARACTERES_FIJO, style='Yellow.TEntry') self.entrada_superior.pack(fill="x", pady=10, padx=5, ipady=3) self.entrada_superior.bind('', self.manejar_navegacion) # 2. Área de Extracción/Navegación acciones_extraccion = [ - ("Actualizar Recursos", self.manejar_extraccion_datos), - ("Navegar", self.manejar_navegacion), + # CAMBIO: Botón renombrado y vinculado a la nueva lógica de scraping + ("Extraer Datos", self.manejar_extraccion_datos), + ("Ir a la URL usando el navegador", self.manejar_navegacion), + # El botón de Google se mantiene como placeholder, esperando la implementación ("Buscar API Google", lambda: accion_placeholder("Buscar API Google")) ] self.crear_seccion(self, titulo="", acciones=acciones_extraccion) @@ -57,29 +56,51 @@ class PanelLateral(ttk.Frame): self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch) # 5. Espacio expandible - # Ahora este marco se expandirá para ocupar todo el espacio restante. tk.Frame(self, height=1).pack(expand=True, fill="both") - # 6. Panel de Notas - ELIMINADO: Se moverá a la pestaña Tareas del Panel Central. - # self.crear_editor_res_notes() # <--- LÍNEA ELIMINADA - # --- MÉTODOS DE LÓGICA / CONTROL --- + def manejar_extraccion_datos(self): + """ + Obtiene el término de búsqueda de la entrada superior, realiza el scraping + en Wikipedia (URL base fija), muestra el resultado y lo carga en el Panel Central. + """ + # Obtenemos el texto introducido por el usuario (el término de búsqueda) + termino_busqueda = self.entrada_superior.get().strip() + + if not termino_busqueda: + messagebox.showwarning("⚠️ Entrada Vacía", + "Por favor, introduce un término de búsqueda para extraer datos.") + return + + # Llama a la lógica de scraping. Se esperan 3 valores: éxito, mensaje y contenido. + success, message, contenido = hacer_scraping(termino_busqueda) + + if success: + messagebox.showinfo("✅ Extracción Exitosa", message) + + # Llamada al Panel Central para visualizar el resultado del scraping + if self.central_panel: + self.central_panel.cargar_texto_en_tareas(termino_busqueda, contenido) + else: + messagebox.showerror("Error", "No se puede visualizar el resultado: Panel Central no disponible.") + + else: + messagebox.showerror("❌ Error de Extracción", message) + def manejar_inicio_carrera_t2(self): """ Llama al método 'manejar_inicio_carrera' del Panel Central. """ if self.central_panel: print("Botón App2 presionado. Iniciando Carrera de Camellos en Panel Central...") - # Llamada a la función expuesta por PanelCentral self.central_panel.manejar_inicio_carrera() # Opcional: Cambiar automáticamente a la pestaña Resultados - if "Resultados" in self.central_panel.tabs: - notebook = self.central_panel.tabs["Resultados"].winfo_toplevel().winfo_children()[0] + if "Carrera" in self.central_panel.tabs: + notebook = self.central_panel.tabs["Carrera"].winfo_toplevel().winfo_children()[0] if isinstance(notebook, ttk.Notebook): - # Asume que el Notebook es el primer widget hijo del frame principal - notebook.select(self.central_panel.tabs["Resultados"]) + notebook.select(self.central_panel.tabs["Carrera"]) else: messagebox.showerror("Error", "El Panel Central no está inicializado.") @@ -91,21 +112,6 @@ class PanelLateral(ttk.Frame): if navegar_a_url(url): self.entrada_superior.delete(0, tk.END) - # --- MÉTODOS DE NOTAS ELIMINADOS (Se moverán a PanelCentral) --- - # def crear_editor_res_notes(self): ... - # def cargar_res_notes(self, initial_load=False): ... - # def guardar_res_notes(self): ... - - def manejar_extraccion_datos(self): - """ - Llama a la lógica de actualización del gráfico de recursos en el panel central (actualización manual). - """ - if self.central_panel: - print("Activando actualización del gráfico de Recursos (Manual)...") - self.central_panel.actualizar_recursos() - else: - messagebox.showerror("Error", "El Panel Central no está inicializado.") - def manejar_backup(self): """Llama a la lógica de backup de T1 e informa al usuario del resultado.""" print("Iniciando proceso de Copia de Seguridad...") @@ -146,4 +152,5 @@ class PanelLateral(ttk.Frame): frame_botones.pack(fill="x", pady=5, padx=5) for texto_boton, comando in acciones: + # Usamos el estilo 'Green.TButton' para los botones de acción principal ttk.Button(frame_botones, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5) \ No newline at end of file