Cuando el scraping no se necesita como script puntual, sino como parte de un servicio web completo — con interfaz, calendario de ejecución, almacenamiento en base de datos y procesamiento en segundo plano —, Django pasa a primer plano. El framework aporta el ORM para el almacenamiento, los comandos de management para el lanzamiento y, en combinación con Celery — tareas en segundo plano y periódicas.
Este artículo es la continuación práctica de la guía panorámica «Web scraping con Python». Las técnicas básicas (requests, BeautifulSoup, codificaciones) están explicadas allí; aquí el acento recae en la integración con Django.
Índice
- Por qué scrapear con Django
- Cómo obtenemos la página
- Bibliotecas para parsear el contenido
- Almacenamiento del resultado en modelos
- Lanzar el scraping: comandos de management y Celery
- Codificaciones y caracteres especiales en Django
- Multihilo y tareas en segundo plano
- Proxies
- Scraping a través de TOR
- HTTPS/SSL
- Trabajo con cookies
- Estado de la respuesta y cabeceras
- Almacenamiento de URL y colas con el ORM
- Ventajas y desventajas
1. Por qué scrapear con Django
Django se justifica cuando el scraping es solo una parte del producto:
- un agregador de productos, ofertas de empleo o noticias con escaparate web;
- monitorización de precios con histórico en base de datos y dashboard;
- actualización periódica del catálogo según un calendario;
- un panel de administración para gestionar las fuentes y revisar los resultados.
Django ofrece «de serie»: el ORM (almacenamiento y deduplicación), el admin (gestión de fuentes), el sistema de migraciones y, con Celery — la ejecución en segundo plano y periódica, para que el scraping pesado no bloquee las peticiones web.
2. Cómo obtenemos la página
El cliente HTTP en Django no se diferencia en nada del Python habitual — tomamos requests o httpx. La lógica de scraping se mantiene fuera de las views: las views deben ser rápidas, y las peticiones de red se llevan a la capa de servicios o a tareas de Celery.
# parser/services.py
import requests
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; MyParserBot/1.0)",
"Accept-Language": "es-ES,es;q=0.9",
}
def fetch_page(url: str) -> str | None:
try:
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
resp.encoding = resp.apparent_encoding
return resp.text
except requests.RequestException as exc:
# registramos el error y no tumbamos la aplicación
import logging
logging.getLogger("parser").warning("fetch failed %s: %s", url, exc)
return NoneLa regla de oro: nunca lance un scraping largo directamente en una view — el usuario quedará esperando y el worker del servidor web se bloqueará. La petición se pone en cola y la respuesta se devuelve de inmediato.
3. Bibliotecas para parsear el contenido
Dentro del servicio se usan las mismas herramientas que en cualquier otro contexto — BeautifulSoup por comodidad, lxml por velocidad:
from bs4 import BeautifulSoup
def parse_products(html: str) -> list[dict]:
soup = BeautifulSoup(html, "lxml")
items = []
for card in soup.select(".product-card"):
items.append({
"title": card.select_one(".title").get_text(strip=True),
"price": card.select_one(".price").get_text(strip=True),
"url": card.select_one("a")["href"],
})
return itemsEl análisis detallado de los parsers está en «Web scraping con Python» y en «lxml». Si la fuente son tablas, vea «Extraer tablas HTML con Python y BeautifulSoup». Si la API devuelve JSON — «Parseo de JSON».
4. Almacenamiento del resultado en modelos
La fuerza de Django es el ORM. Se describe el modelo, y la deduplicación, el filtrado y el histórico de actualizaciones se vuelven triviales.
# parser/models.py
from django.db import models
class Product(models.Model):
source_url = models.URLField(unique=True) # unicidad = protección contra duplicados
title = models.CharField(max_length=500)
price = models.DecimalField(max_digits=10, decimal_places=2, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.titleGuardado con protección contra duplicados mediante update_or_create:
from .models import Product
def save_products(items: list[dict]):
for item in items:
Product.objects.update_or_create(
source_url=item["url"],
defaults={"title": item["title"], "price": item["price"]},
)update_or_create actualiza el registro existente o crea uno nuevo — ideal para un scraping repetido.
5. Lanzar el scraping: comandos de management y Celery
Comando de management — para el lanzamiento manual y por cron
# parser/management/commands/run_parser.py
from django.core.management.base import BaseCommand
from parser.services import fetch_page, parse_products, save_products
class Command(BaseCommand):
help = "Lanza el scraping del catálogo"
def add_arguments(self, parser):
parser.add_argument("--url", required=True)
def handle(self, *args, **options):
html = fetch_page(options["url"])
if html:
items = parse_products(html)
save_products(items)
self.stdout.write(self.style.SUCCESS(f"Guardados {len(items)} productos"))Ejecución: python manage.py run_parser --url https://example.com/catalog. Un comando así resulta cómodo de colgar del cron del sistema.
Celery — para tareas en segundo plano y periódicas
# parser/tasks.py
from celery import shared_task
from .services import fetch_page, parse_products, save_products
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def parse_url_task(self, url):
html = fetch_page(url)
if html is None:
raise self.retry() # reintento en caso de fallo
items = parse_products(html)
save_products(items)
return len(items)El lanzamiento periódico se configura con django-celery-beat directamente en el admin — por ejemplo, actualizar el catálogo cada 6 horas. El retry de Celery da resistencia a los fallos transitorios de red sin gestión manual.
6. Codificaciones y caracteres especiales en Django
El problema es el mismo que en cualquier scraper en Python: una codificación de la respuesta mal detectada, que rompe tildes y eñes. Se cura igual — resp.apparent_encoding o trabajar con resp.content (vea en detalle «Web scraping con Python», la sección sobre codificaciones).
La especificidad de Django: asegúrese de que la base de datos usa UTF-8 (en PostgreSQL — codificación UTF8; en MySQL — utf8mb4); de lo contrario, los caracteres acentuados se romperán ya en la fase de guardado, no en la de scraping. En un proyecto nuevo es el valor por defecto, pero al trabajar con una base de datos antigua conviene comprobarlo.
7. Multihilo y tareas en segundo plano
En Django rara vez se usan hilos «a pelo» — para el paralelismo está Celery con su pool de workers. Al lanzar varios workers, obtiene el procesamiento en paralelo de la cola sin gestionar hilos a mano:
celery -A myproject worker --concurrency=8 --loglevel=infoSi necesita recorrer rápido una lista de URL dentro de una misma tarea, sirve ThreadPoolExecutor (vea la sección sobre multihilo del hub). Para una concurrencia muy alta dentro de la tarea se puede aplicar async — pero en Django eso exige cuidado con el ORM (sync_to_async). El tema está tratado en «Scraping asíncrono en Python».
8. Proxies
Los proxies se conectan en la capa de servicios — Django no interviene aquí. La lista de proxies resulta cómodo guardarla en un modelo y marcar los «sanos»:
class Proxy(models.Model):
address = models.CharField(max_length=200) # http://user:pass@ip:port
is_active = models.BooleanField(default=True)
fail_count = models.IntegerField(default=0)El servicio toma entonces un proxy activo al azar y, ante un error, incrementa fail_count y, superado el umbral, lo desactiva. Las técnicas generales de rotación — en el hub, sección «Proxies».
9. Scraping a través de TOR
TOR se conecta como proxy SOCKS5 (socks5h://127.0.0.1:9050) en el mismo fetch_page. Django no añade ninguna especificidad — vea la sección sobre TOR del hub. En el servidor con Django, el demonio de TOR se ejecuta como proceso aparte (servicio de systemd).
10. HTTPS/SSL
La verificación de certificados funciona a nivel de requests. En producción, mantenga verify=True. Si el servidor tiene certificados raíz obsoletos, actualice certifi en el entorno virtual del proyecto. Los detalles — en el hub.
11. Trabajo con cookies
Para el scraping con autenticación use requests.Session dentro de la tarea. Si necesita reutilizar la sesión entre tareas de Celery, guarde el cookie-jar (por ejemplo, serializado en Redis o en un modelo) y restáurelo antes de la petición. Las técnicas básicas — en el hub, sección «Cookies».
12. Estado de la respuesta y cabeceras
Registre el código de respuesta y decida si conviene repetir la tarea. En Celery esto encaja con elegancia en el mecanismo de retry:
@shared_task(bind=True, max_retries=5)
def fetch_task(self, url):
resp = requests.get(url, timeout=15)
if resp.status_code == 429:
delay = int(resp.headers.get("Retry-After", 60))
raise self.retry(countdown=delay)
if resp.status_code in (403, 503):
raise self.retry(countdown=120) # probable bloqueo, esperamos más
resp.raise_for_status()
return resp.textAsí, los bloqueos anti-bot (429/403/503) se convierten en reintentos controlados con espera, y no en pérdida de datos.
13. Almacenamiento de URL y colas con el ORM
En Django, el frontier se implementa cómodamente como una tabla con estados — sobrevive a los reinicios y se ve en el admin.
class CrawlURL(models.Model):
STATUS = [("new", "new"), ("in_progress", "in_progress"),
("done", "done"), ("failed", "failed")]
url = models.URLField(unique=True) # unique = deduplicación automática
status = models.CharField(max_length=20, choices=STATUS, default="new")
attempts = models.IntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)El worker toma las URL «nuevas», las marca in_progress, las procesa y las pasa a done/failed. El índice único sobre url excluye los duplicados. En los proyectos de alta carga, la cola real conviene mantenerla en Redis/el broker de Celery, y en la base de datos guardar solo el resultado y el histórico. El panorama general de enfoques de colas — en el hub, sección «Colas».
14. Ventajas y desventajas del scraping con Django
Ventajas:
- El ORM libera de la gestión del almacenamiento, la deduplicación y las migraciones.
- Admin de serie — gestión de fuentes y revisión de los datos.
- Celery + beat — tareas en segundo plano y periódicas fiables, con retry.
- Una base de código única: el scraper y el escaparate web en el mismo proyecto.
Desventajas:
- Excesivo para scripts puntuales — un
.pynormal es más sencillo. - Exige infraestructura: broker (Redis/RabbitMQ), workers de Celery.
- El async en Django sigue estando lleno de matices (el ORM es mayoritariamente síncrono).
- Para recorrer millones de páginas, el especializado Scrapy es más eficiente.