Un bot de alerta de precios consulta periódicamente una página o una API, extrae el valor de un producto y envía una notificación cuando se cumple una condición. Es un proyecto práctico para aprender solicitudes HTTP, análisis de HTML, programación de tareas, almacenamiento de estado y manejo de errores. La parte importante no es únicamente encontrar un número: el sistema debe identificar el producto correcto, evitar alertas duplicadas y respetar las reglas del sitio.
Para seguir esta guía conviene conocer Requests en Python, el web scraping con Beautiful Soup, las variables de entorno, el módulo logging y el manejo de excepciones.
Elegir una fuente adecuada
Siempre que sea posible, utiliza una API oficial, un feed de datos o una función de alertas proporcionada por la tienda. El scraping de HTML es más frágil porque la estructura visual puede cambiar. Antes de automatizar consultas, revisa los términos de uso, el archivo robots.txt y los límites de acceso.
La documentación oficial de Requests explica sesiones, timeouts y errores HTTP. La documentación oficial de Beautiful Soup describe selectores y análisis de documentos HTML.
Instalar las dependencias
python -m venv .venv
.venv\Scripts\activate
python -m pip install requests beautifulsoup4
En macOS o Linux utiliza source .venv/bin/activate. Guarda las versiones utilizadas en un archivo de requisitos para reproducir el entorno.
Descargar la página con seguridad
import requests
URL = "https://ejemplo.com/producto"
def descargar_pagina(url: str) -> str:
cabeceras = {
"User-Agent": "Mozilla/5.0 PriceMonitor/1.0"
}
respuesta = requests.get(
url,
headers=cabeceras,
timeout=(5, 15),
)
respuesta.raise_for_status()
return respuesta.text
Un timeout de conexión y lectura evita bloqueos indefinidos. No intentes eludir protecciones, captchas o controles de acceso. Si el sitio bloquea automatizaciones, utiliza una alternativa autorizada.
Encontrar el precio
Inspecciona el HTML y busca un selector estable, preferiblemente un atributo semántico:
from bs4 import BeautifulSoup
def extraer_texto_precio(html: str) -> str:
soup = BeautifulSoup(html, "html.parser")
elemento = soup.select_one('[data-testid="product-price"]')
if elemento is None:
raise ValueError("No se encontró el precio")
return elemento.get_text(" ", strip=True)
No uses un selector basado solo en una cadena de clases generadas automáticamente, porque puede cambiar en cada despliegue.
Convertir el texto a Decimal
from decimal import Decimal, InvalidOperation
import re
def convertir_precio(texto: str) -> Decimal:
limpio = re.sub(r"[^0-9,.-]", "", texto)
if "," in limpio and "." in limpio:
limpio = limpio.replace(".", "").replace(",", ".")
elif "," in limpio:
limpio = limpio.replace(",", ".")
try:
precio = Decimal(limpio)
except InvalidOperation as error:
raise ValueError(f"Precio inválido: {texto}") from error
if precio < 0:
raise ValueError("El precio no puede ser negativo")
return precio
Las reglas de separadores dependen de la región. Si el sitio utiliza un formato diferente, adapta la función y añade pruebas específicas.
Comprobar el producto correcto
Además del precio, verifica el nombre o un identificador:
def extraer_producto(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
titulo = soup.select_one("h1")
precio = soup.select_one('[data-testid="product-price"]')
if not titulo or not precio:
raise ValueError("La estructura de la página cambió")
return {
"titulo": titulo.get_text(" ", strip=True),
"precio": convertir_precio(
precio.get_text(" ", strip=True)
),
}
Esto evita enviar una alerta si la URL fue redirigida a una categoría, página de error o producto alternativo.
Definir el umbral
from decimal import Decimal
PRECIO_OBJETIVO = Decimal("299.90")
def debe_alertar(precio_actual: Decimal) -> bool:
return precio_actual <= PRECIO_OBJETIVO
También puedes detectar una reducción porcentual respecto al último precio:
def reduccion_porcentual(anterior, actual):
if anterior <= 0:
return Decimal("0")
return (anterior - actual) / anterior * 100
Guardar el estado
Para no repetir la misma alerta en cada ejecución, guarda el último precio y la última notificación:
import json
from pathlib import Path
RUTA_ESTADO = Path("estado_precio.json")
def cargar_estado():
if not RUTA_ESTADO.exists():
return {}
return json.loads(RUTA_ESTADO.read_text(encoding="utf-8"))
def guardar_estado(datos):
temporal = RUTA_ESTADO.with_suffix(".tmp")
temporal.write_text(
json.dumps(datos, indent=2, ensure_ascii=False),
encoding="utf-8",
)
temporal.replace(RUTA_ESTADO)
La escritura temporal reduce el riesgo de dejar un JSON incompleto si el proceso se interrumpe.
Enviar una notificación por correo
import os
import smtplib
from email.message import EmailMessage
def enviar_correo(titulo, precio, url):
usuario = os.getenv("SMTP_USER")
clave = os.getenv("SMTP_PASSWORD")
destino = os.getenv("ALERT_EMAIL")
if not all([usuario, clave, destino]):
raise RuntimeError("Faltan variables SMTP")
mensaje = EmailMessage()
mensaje["Subject"] = f"Bajó el precio: {titulo}"
mensaje["From"] = usuario
mensaje["To"] = destino
mensaje.set_content(
f"Precio actual: {precio}\nProducto: {url}"
)
with smtplib.SMTP_SSL("smtp.example.com", 465) as servidor:
servidor.login(usuario, clave)
servidor.send_message(mensaje)
Utiliza una contraseña de aplicación o un proveedor transaccional. Nunca guardes credenciales directamente en el archivo.
Integrar el flujo
from datetime import datetime, timezone
import logging
logging.basicConfig(
filename="monitor_precios.log",
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
encoding="utf-8",
)
def ejecutar():
html = descargar_pagina(URL)
producto = extraer_producto(html)
estado = cargar_estado()
precio = producto["precio"]
ultima_alerta = estado.get("precio_alertado")
if debe_alertar(precio) and str(precio) != ultima_alerta:
enviar_correo(producto["titulo"], precio, URL)
estado["precio_alertado"] = str(precio)
estado["ultimo_precio"] = str(precio)
estado["ultima_revision"] = datetime.now(
timezone.utc
).isoformat()
guardar_estado(estado)
logging.info("Precio comprobado: %s", precio)
Manejar fallos
def main():
try:
ejecutar()
except requests.Timeout:
logging.warning("Timeout consultando el producto")
except requests.HTTPError as error:
logging.error("HTTP %s", error.response.status_code)
except (ValueError, KeyError, json.JSONDecodeError):
logging.exception("No se pudo interpretar la información")
except OSError:
logging.exception("Error leyendo o guardando el estado")
No envíes una alerta de “precio cero” cuando el selector falla. Un dato ausente debe tratarse como error, no como oferta.
Programar la ejecución
En Windows utiliza el Programador de tareas. En Linux o macOS puedes usar cron o un servicio. Una frecuencia razonable suele ser cada varias horas, dependiendo del permiso y de los límites del sitio. No ejecutes solicitudes cada pocos segundos.
Probar el monitor
Guarda ejemplos de HTML en archivos locales y prueba la extracción sin Internet. Incluye casos con precio normal, descuento, producto agotado, selector ausente y separadores distintos. Simula también timeout, respuesta 500 y correo fallido. Los tests no deben consultar una tienda real.
Mejoras útiles
Puedes monitorizar varios productos mediante un archivo JSON o CSV, añadir historial para crear gráficos, enviar avisos por Telegram o Discord, usar SQLite y aplicar una ventana de silencio para no repetir notificaciones. Si el sitio ofrece datos estructurados JSON-LD con información de producto, puede ser una fuente más estable que el texto visual.
Seguridad y responsabilidad
Limita la frecuencia, identifica el cliente de forma honesta, almacena secretos fuera del repositorio y registra únicamente información necesaria. No recolectes datos personales ni intentes superar medidas técnicas. Una automatización responsable debe poder desactivarse rápidamente y respetar las condiciones de la fuente.
Conclusión
Un bot de alerta de precios combina scraping, validación, persistencia y notificaciones. La solución confiable comprueba el producto, convierte el valor con precisión, utiliza timeouts, evita alertas duplicadas y maneja cambios de estructura. Empieza con una sola página y una frecuencia conservadora. Después añade pruebas, almacenamiento e interfaces sin comprometer el respeto por el sitio consultado.






