Un scraper de noticias con envío a Telegram combina tres tareas: descargar una página, extraer titulares y publicar un mensaje mediante un bot. Es un proyecto práctico para aprender solicitudes HTTP, análisis de HTML, deduplicación, variables de entorno, registros y automatización. La parte más importante no es únicamente obtener texto, sino hacerlo de forma responsable, resistente a cambios y sin enviar la misma noticia repetidamente.
Antes de comenzar conviene revisar la guía de web scraping con Beautiful Soup y Requests, el tutorial de Requests en Python, la creación de un bot de Telegram, el uso de variables de entorno y la guía de logging.
Las referencias externas principales son la documentación oficial de Requests, la documentación de Beautiful Soup y la documentación oficial de Telegram Bot API.
Revisar permisos antes de extraer datos
No todas las páginas permiten scraping. Consulta los términos del sitio, su archivo robots.txt, las licencias del contenido y cualquier API oficial disponible. No intentes eludir autenticación, bloqueos, captchas o límites. Este tutorial debe aplicarse únicamente a fuentes que permitan el acceso automatizado y a datos públicos.
Crear el entorno
mkdir noticias_telegram
cd noticias_telegram
python -m venv .venv
Activa el entorno e instala las dependencias:
python -m pip install requests beautifulsoup4 python-dotenv
Guarda las versiones en un archivo:
python -m pip freeze > requirements.txt
Crear el bot de Telegram
Utiliza BotFather dentro de Telegram para crear un bot y recibir un token. No publiques ese token ni lo pegues en el código. Para conocer el identificador del chat, envía un mensaje al bot y consulta las actualizaciones durante la configuración, respetando la documentación oficial.
Guardar secretos en variables de entorno
Crea un archivo .env local:
TELEGRAM_BOT_TOKEN=tu_token
TELEGRAM_CHAT_ID=tu_chat_id
Añádelo a .gitignore:
.env
.venv/
__pycache__/
noticias_enviadas.json
Carga la configuración:
import os
from dotenv import load_dotenv
load_dotenv()
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
if not TOKEN or not CHAT_ID:
raise RuntimeError("Faltan las variables de Telegram")
Descargar una página con Requests
import requests
URL_FUENTE = "https://ejemplo.com/noticias"
def descargar_html(url):
encabezados = {
"User-Agent": "NoticiasEducativasBot/1.0 [email protected]"
}
respuesta = requests.get(
url,
headers=encabezados,
timeout=(5, 20),
)
respuesta.raise_for_status()
return respuesta.text
Utiliza un User-Agent identificable cuando la fuente lo permita, añade un medio de contacto y define timeout. raise_for_status() transforma respuestas HTTP de error en excepciones.
Crear una sesión reutilizable
sesion = requests.Session()
sesion.headers.update({
"User-Agent": "NoticiasEducativasBot/1.0 [email protected]"
})
def descargar_html(url):
respuesta = sesion.get(url, timeout=(5, 20))
respuesta.raise_for_status()
return respuesta.text
Una sesión reutiliza conexiones y permite configurar encabezados comunes. No aumenta automáticamente el ritmo permitido; conserva pausas razonables y respeta límites.
Inspeccionar el HTML
Abre las herramientas del navegador y localiza el elemento que contiene cada noticia. Una estructura hipotética podría ser:
<article class="noticia">
<h2><a href="/noticias/python-actualizado">Python recibe una actualización</a></h2>
</article>
Los selectores dependen del sitio real. No copies un selector sin revisarlo y evita depender de clases generadas que cambian en cada despliegue.
Extraer titulares y enlaces
from bs4 import BeautifulSoup
from urllib.parse import urljoin
def extraer_noticias(html, url_base):
soup = BeautifulSoup(html, "html.parser")
noticias = []
for enlace in soup.select("article.noticia h2 a"):
titulo = enlace.get_text(" ", strip=True)
href = enlace.get("href")
if not titulo or not href:
continue
noticias.append({
"titulo": titulo,
"url": urljoin(url_base, href),
})
return noticias
urljoin convierte rutas relativas en URLs completas. Limpiar el texto con get_text evita espacios y saltos innecesarios.
Validar los resultados
def validar_noticias(noticias):
validas = []
vistas = set()
for noticia in noticias:
titulo = noticia["titulo"][:300]
url = noticia["url"]
clave = (titulo.casefold(), url)
if clave in vistas:
continue
if not url.startswith(("http://", "https://")):
continue
vistas.add(clave)
validas.append({"titulo": titulo, "url": url})
return validas
Limitar longitudes protege la salida y evita mensajes enormes. Una validación más estricta puede comprobar que los enlaces pertenezcan al dominio esperado.
Enviar un mensaje a Telegram
def enviar_telegram(texto):
url = f"https://api.telegram.org/bot{TOKEN}/sendMessage"
datos = {
"chat_id": CHAT_ID,
"text": texto,
"disable_web_page_preview": True,
}
respuesta = requests.post(
url,
json=datos,
timeout=(5, 20),
)
respuesta.raise_for_status()
resultado = respuesta.json()
if not resultado.get("ok"):
raise RuntimeError("Telegram rechazó el mensaje")
return resultado
Enviar JSON evita construir manualmente la query. No registres el URL completo porque contiene el token.
Formatear las noticias
def formatear_noticia(noticia):
return f"{noticia['titulo']}\n{noticia['url']}"
Telegram admite formatos adicionales, pero el texto plano reduce errores de escape. Si utilizas HTML o Markdown, escapa correctamente el contenido extraído.
Evitar envíos duplicados
Guarda identificadores de noticias ya publicadas:
import hashlib
import json
from pathlib import Path
RUTA_ESTADO = Path("noticias_enviadas.json")
def crear_id(noticia):
contenido = noticia["url"].encode("utf-8")
return hashlib.sha256(contenido).hexdigest()
def cargar_enviadas():
if not RUTA_ESTADO.exists():
return set()
try:
datos = json.loads(RUTA_ESTADO.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return set()
return set(datos)
def guardar_enviadas(ids):
temporal = RUTA_ESTADO.with_suffix(".tmp")
temporal.write_text(
json.dumps(sorted(ids), indent=2),
encoding="utf-8",
)
temporal.replace(RUTA_ESTADO)
La escritura temporal disminuye el riesgo de dejar un archivo incompleto. Para varios procesos simultáneos, utiliza una base de datos o un mecanismo de bloqueo.
Unir el flujo completo
def ejecutar():
enviadas = cargar_enviadas()
html = descargar_html(URL_FUENTE)
noticias = validar_noticias(
extraer_noticias(html, URL_FUENTE)
)
nuevas = [
noticia for noticia in noticias
if crear_id(noticia) not in enviadas
]
for noticia in nuevas[:5]:
enviar_telegram(formatear_noticia(noticia))
enviadas.add(crear_id(noticia))
guardar_enviadas(enviadas)
if __name__ == "__main__":
ejecutar()
El límite de cinco evita inundar el chat si el estado se pierde o el selector cambia. Ajusta la cantidad a una frecuencia razonable.
Añadir logging
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(message)s",
)
logging.info("Iniciando consulta de noticias")
Registra cantidad de noticias encontradas, nuevas y enviadas, pero nunca tokens ni contenido privado.
Manejar errores de red
def main():
try:
ejecutar()
except requests.Timeout:
logging.error("La solicitud superó el tiempo límite")
except requests.HTTPError as error:
logging.error("Error HTTP: %s", error.response.status_code)
except requests.RequestException as error:
logging.error("Error de red: %s", error)
except Exception:
logging.exception("Fallo inesperado")
Separar timeouts, errores HTTP y fallos inesperados facilita diagnosticar el problema. No reintentes indefinidamente.
Implementar reintentos limitados
import time
def ejecutar_con_reintentos(max_intentos=3):
for intento in range(1, max_intentos + 1):
try:
ejecutar()
return
except requests.RequestException:
if intento == max_intentos:
raise
espera = 2 ** intento
time.sleep(espera)
El backoff exponencial reduce presión durante una falla. Si el servidor devuelve Retry-After, respeta esa indicación.
Programar la ejecución
En Windows puedes usar el Programador de tareas; en Linux, cron o un servicio. Ejecuta el script con el Python del entorno virtual y configura el directorio de trabajo. Una frecuencia de quince minutos o una hora suele ser más razonable que consultar cada pocos segundos, pero depende del permiso y de la fuente.
Detectar cambios en el HTML
Un selector roto puede devolver cero resultados sin generar excepción. Añade una comprobación:
if not noticias:
raise RuntimeError("No se encontraron noticias; revisa el selector")
Guarda una pequeña muestra HTML para pruebas, sin redistribuir contenido protegido. Las pruebas deben confirmar que el parser extrae títulos, resuelve enlaces y elimina duplicados.
Preferir RSS o API cuando existan
Si la fuente ofrece RSS o una API, normalmente son opciones más estables y explícitas que analizar HTML. RSS conserva títulos, enlaces y fechas en una estructura pensada para sindicación. El scraper debería ser la alternativa cuando el acceso esté permitido y no exista un canal oficial apropiado.
Seguridad de URLs y contenido
No sigas enlaces extraídos para descargar archivos automáticamente sin validar dominio, esquema, tamaño y tipo. Si el scraper recibe una URL configurable, protege el sistema contra solicitudes a redes internas. Mantén el bot en un chat controlado y limita comandos y permisos.
Errores frecuentes
Los fallos habituales son guardar el token en el código, consultar demasiado rápido, ignorar términos del sitio, usar selectores frágiles, enviar noticias duplicadas y no definir timeout. También es peligroso registrar el endpoint completo de Telegram, porque contiene la credencial del bot.
Conclusión
Un scraper de noticias confiable descarga páginas con límites, analiza HTML mediante selectores revisados, valida enlaces, conserva un estado de deduplicación y envía mensajes sin exponer el token. Respeta las reglas de la fuente y prefiere APIs o RSS cuando estén disponibles. Con logging, pruebas y reintentos limitados, el proyecto puede ejecutarse de forma automatizada sin convertirse en una carga para el sitio ni para el chat.






