```
feat(vista): Integra scraping y refactoriza (main) Integra scraping de Wikipedia y refactoriza la interfaz. * Implementa scraping de Wikipedia en panel lateral. * Mueve editor de notas a la pestaña "Tareas" del panel central. * Reemplaza "Resultados" por "Carrera" en las pestañas. * Elimina editor de notas del panel lateral. * Agrega dependencias bs4 y requests en Readme.md. ```
This commit is contained in:
parent
8911576bb7
commit
8904425f63
|
|
@ -7,6 +7,12 @@ psutil => pip install psutil
|
||||||
|
|
||||||
python-vlc => pip install python-vlc
|
python-vlc => pip install python-vlc
|
||||||
|
|
||||||
|
bs4 => pip install bs4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
requests => pip install requests
|
||||||
|
|
||||||
## Como Ejecutar ##
|
## Como Ejecutar ##
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Desde la carpeta anterior
|
> Desde la carpeta anterior
|
||||||
|
|
@ -36,7 +42,7 @@ python -m ProyectoGlobal
|
||||||
|
|
||||||
2. ~~Temperatura local~~
|
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
|
4. Scraping
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}", ""
|
||||||
|
|
@ -497,7 +497,7 @@ class PanelCentral(ttk.Frame):
|
||||||
sub_notebook = ttk.Notebook(parent_frame)
|
sub_notebook = ttk.Notebook(parent_frame)
|
||||||
sub_notebook.grid(row=0, column=0, sticky="nsew")
|
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 = {}
|
self.tabs = {}
|
||||||
|
|
||||||
for i, sub_tab_text in enumerate(sub_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 = self.canvas.get_tk_widget()
|
||||||
self.canvas_widget.pack(expand=True, fill="both")
|
self.canvas_widget.pack(expand=True, fill="both")
|
||||||
|
|
||||||
elif sub_tab_text == "Resultados":
|
elif sub_tab_text == "Carrera":
|
||||||
self.crear_interfaz_carrera(frame)
|
self.crear_interfaz_carrera(frame)
|
||||||
|
|
||||||
elif sub_tab_text == "Radios":
|
elif sub_tab_text == "Radios":
|
||||||
|
|
|
||||||
|
|
@ -6,36 +6,35 @@ from tkinter import messagebox
|
||||||
from logica.controlador import accion_placeholder
|
from logica.controlador import accion_placeholder
|
||||||
from logica.T1.backup import accion_backup_t1
|
from logica.T1.backup import accion_backup_t1
|
||||||
from logica.T1.runVScode import abrir_vscode
|
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.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 ---
|
# --- IMPORTACIÓN DE CONSTANTES DESDE vista/config.py ---
|
||||||
|
# Asumo que este archivo existe y contiene las constantes de color/fuente
|
||||||
from vista.config import *
|
from vista.config import *
|
||||||
|
|
||||||
|
|
||||||
class PanelLateral(ttk.Frame):
|
class PanelLateral(ttk.Frame):
|
||||||
"""Contiene el menú de botones y entradas para las tareas."""
|
"""Contiene el menú de botones y entradas para las tareas."""
|
||||||
|
|
||||||
# Usamos la constante importada
|
|
||||||
ANCHO_CARACTERES_FIJO = ANCHO_CARACTERES_PANEL_LATERAL
|
ANCHO_CARACTERES_FIJO = ANCHO_CARACTERES_PANEL_LATERAL
|
||||||
|
|
||||||
def __init__(self, parent, central_panel=None, *args, **kwargs):
|
def __init__(self, parent, central_panel=None, *args, **kwargs):
|
||||||
super().__init__(parent, *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.central_panel = central_panel
|
||||||
|
|
||||||
self.configurar_estilos_locales(parent)
|
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 = 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.pack(fill="x", pady=10, padx=5, ipady=3)
|
||||||
self.entrada_superior.bind('<Return>', self.manejar_navegacion)
|
self.entrada_superior.bind('<Return>', self.manejar_navegacion)
|
||||||
|
|
||||||
# 2. Área de Extracción/Navegación
|
# 2. Área de Extracción/Navegación
|
||||||
acciones_extraccion = [
|
acciones_extraccion = [
|
||||||
("Actualizar Recursos", self.manejar_extraccion_datos),
|
# CAMBIO: Botón renombrado y vinculado a la nueva lógica de scraping
|
||||||
("Navegar", self.manejar_navegacion),
|
("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"))
|
("Buscar API Google", lambda: accion_placeholder("Buscar API Google"))
|
||||||
]
|
]
|
||||||
self.crear_seccion(self, titulo="", acciones=acciones_extraccion)
|
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)
|
self.crear_seccion(self, titulo="Procesos batch", acciones=acciones_batch)
|
||||||
|
|
||||||
# 5. Espacio expandible
|
# 5. Espacio expandible
|
||||||
# Ahora este marco se expandirá para ocupar todo el espacio restante.
|
|
||||||
tk.Frame(self, height=1).pack(expand=True, fill="both")
|
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 ---
|
# --- 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):
|
def manejar_inicio_carrera_t2(self):
|
||||||
"""
|
"""
|
||||||
Llama al método 'manejar_inicio_carrera' del Panel Central.
|
Llama al método 'manejar_inicio_carrera' del Panel Central.
|
||||||
"""
|
"""
|
||||||
if self.central_panel:
|
if self.central_panel:
|
||||||
print("Botón App2 presionado. Iniciando Carrera de Camellos en Panel Central...")
|
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()
|
self.central_panel.manejar_inicio_carrera()
|
||||||
|
|
||||||
# Opcional: Cambiar automáticamente a la pestaña Resultados
|
# Opcional: Cambiar automáticamente a la pestaña Resultados
|
||||||
if "Resultados" in self.central_panel.tabs:
|
if "Carrera" in self.central_panel.tabs:
|
||||||
notebook = self.central_panel.tabs["Resultados"].winfo_toplevel().winfo_children()[0]
|
notebook = self.central_panel.tabs["Carrera"].winfo_toplevel().winfo_children()[0]
|
||||||
if isinstance(notebook, ttk.Notebook):
|
if isinstance(notebook, ttk.Notebook):
|
||||||
# Asume que el Notebook es el primer widget hijo del frame principal
|
notebook.select(self.central_panel.tabs["Carrera"])
|
||||||
notebook.select(self.central_panel.tabs["Resultados"])
|
|
||||||
else:
|
else:
|
||||||
messagebox.showerror("Error", "El Panel Central no está inicializado.")
|
messagebox.showerror("Error", "El Panel Central no está inicializado.")
|
||||||
|
|
||||||
|
|
@ -91,21 +112,6 @@ class PanelLateral(ttk.Frame):
|
||||||
if navegar_a_url(url):
|
if navegar_a_url(url):
|
||||||
self.entrada_superior.delete(0, tk.END)
|
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):
|
def manejar_backup(self):
|
||||||
"""Llama a la lógica de backup de T1 e informa al usuario del resultado."""
|
"""Llama a la lógica de backup de T1 e informa al usuario del resultado."""
|
||||||
print("Iniciando proceso de Copia de Seguridad...")
|
print("Iniciando proceso de Copia de Seguridad...")
|
||||||
|
|
@ -146,4 +152,5 @@ class PanelLateral(ttk.Frame):
|
||||||
frame_botones.pack(fill="x", pady=5, padx=5)
|
frame_botones.pack(fill="x", pady=5, padx=5)
|
||||||
|
|
||||||
for texto_boton, comando in acciones:
|
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)
|
ttk.Button(frame_botones, text=texto_boton, command=comando, style='Green.TButton').pack(fill="x", pady=5)
|
||||||
Loading…
Reference in New Issue