Spaces:
Paused
Paused
| import gradio as gr | |
| import requests | |
| from requests.adapters import HTTPAdapter | |
| from urllib3.util.retry import Retry | |
| from bs4 import BeautifulSoup | |
| from urllib.parse import urljoin, urlparse | |
| import os | |
| def build_session(): | |
| session = requests.Session() | |
| retries = Retry( | |
| total=2, | |
| backoff_factor=1, | |
| status_forcelist=[429, 500, 502, 503, 504], | |
| ) | |
| adapter = HTTPAdapter(max_retries=retries) | |
| session.mount("https://", adapter) | |
| session.mount("http://", adapter) | |
| return session | |
| def extract_images(url): | |
| session = build_session() | |
| try: | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", | |
| "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", | |
| "Accept-Language": "es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7", | |
| "Accept-Encoding": "gzip, deflate, br", | |
| "Connection": "keep-alive", | |
| "Upgrade-Insecure-Requests": "1", | |
| } | |
| # Timeout separado: (conexión, lectura). Antes era un único valor de 50s, | |
| # lo que dejaba la UI colgada mucho tiempo si el sitio nunca respondía. | |
| response = session.get(url, headers=headers, timeout=(10, 20)) | |
| response.raise_for_status() | |
| soup = BeautifulSoup(response.text, "html.parser") | |
| base_url = f"{urlparse(url).scheme}://{urlparse(url).netloc}" | |
| images = [] | |
| seen = set() | |
| # Buscar imágenes en <img> | |
| for img in soup.find_all("img"): | |
| src = img.get("src") or img.get("data-src") or img.get("data-original") | |
| if not src: | |
| continue | |
| # Convertir URLs relativas a absolutas | |
| img_url = urljoin(base_url, src) | |
| # Filtrar duplicados y URLs inválidas | |
| if img_url in seen or not img_url.startswith(("http://", "https://")): | |
| continue | |
| seen.add(img_url) | |
| # Obtener nombre del archivo | |
| parsed = urlparse(img_url) | |
| filename = os.path.basename(parsed.path) or "imagen.jpg" | |
| if not any(filename.lower().endswith(ext) for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg", ".bmp"]): | |
| filename += ".jpg" | |
| images.append({ | |
| "image": img_url, | |
| "caption": filename | |
| }) | |
| # Buscar imágenes de fondo en <div> o elementos con style | |
| for tag in soup.find_all(style=True): | |
| style = tag["style"] | |
| if "url(" in style: | |
| url_start = style.find("url(") + 4 | |
| url_end = style.find(")", url_start) | |
| bg_url = style[url_start:url_end].strip('"\'') | |
| bg_url = urljoin(base_url, bg_url) | |
| if bg_url not in seen and bg_url.startswith(("http://", "https://")): | |
| seen.add(bg_url) | |
| images.append({"image": bg_url, "caption": "background.jpg"}) | |
| if not images: | |
| return [], "No se encontraron imágenes en esta página." | |
| return [img["image"] for img in images], f"Se encontraron {len(images)} imágenes." | |
| except requests.exceptions.Timeout: | |
| return [], ( | |
| "El sitio no respondió a tiempo tras reintentar. Suele pasar cuando " | |
| "el dominio está filtrado a nivel de red/DNS (algunos hosts de archivos " | |
| "terminan en listas de bloqueo por abuso) o el servidor está caído/lento. " | |
| "Prueba el mismo enlace en el navegador desde esta misma red para confirmar." | |
| ) | |
| except requests.exceptions.ConnectionError as e: | |
| # requests envuelve un ReadTimeout que agotó los reintentos como | |
| # ConnectionError (no como Timeout) — es un detalle de la librería, | |
| # no un tipo de fallo distinto al de arriba. | |
| if "ReadTimeoutError" in str(e) or "Read timed out" in str(e): | |
| return [], ( | |
| "El sitio acepta la conexión pero nunca termina de responder, " | |
| "ni siquiera tras reintentar. Es el patrón de un bloqueo de " | |
| "red/firewall o de un mecanismo anti-bots que corta la respuesta " | |
| "a mitad de camino, no el de un timeout demasiado corto." | |
| ) | |
| return [], f"No se pudo conectar con el sitio: {str(e)}" | |
| except requests.exceptions.RequestException as e: | |
| return [], f"Error al acceder a la URL: {str(e)}" | |
| except Exception as e: | |
| return [], f"Error inesperado: {str(e)}" | |
| finally: | |
| session.close() | |
| # Interfaz de Gradio | |
| with gr.Blocks(title="Extractor de Imágenes Web") as demo: | |
| gr.Markdown(""" | |
| # 🖼️ Extractor de Imágenes | |
| Pega el enlace de cualquier página web y extrae todas las imágenes disponibles. | |
| """) | |
| with gr.Row(): | |
| url_input = gr.Textbox( | |
| label="URL de la página", | |
| placeholder="https://ejemplo.com", | |
| scale=4 | |
| ) | |
| submit_btn = gr.Button("🔍 Extraer imágenes", scale=1, variant="primary") | |
| status_text = gr.Textbox(label="Estado", interactive=False) | |
| gallery = gr.Gallery( | |
| label="Imágenes encontradas", | |
| show_label=True, | |
| columns=4, | |
| rows=4, | |
| height="auto", | |
| object_fit="contain", | |
| allow_preview=True | |
| ) | |
| # Información de descarga | |
| gr.Markdown(""" | |
| ### 💡 Para descargar: | |
| 1. Haz **clic en cualquier imagen** para previsualizarla en tamaño completo. | |
| 2. En la vista previa, haz **clic derecho → Guardar imagen como...**. | |
| 3. También puedes hacer clic derecho directamente sobre cualquier miniatura. | |
| """) | |
| submit_btn.click( | |
| fn=extract_images, | |
| inputs=url_input, | |
| outputs=[gallery, status_text] | |
| ) | |
| # Ejemplo | |
| gr.Examples( | |
| examples=["https://es.wikipedia.org/wiki/Gato"], | |
| inputs=url_input, | |
| label="Ejemplo" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() |