Quando o scraping não é necessário como script pontual, mas como parte de um serviço web completo — com interface, agenda de execução, armazenamento em banco de dados e processamento em segundo plano —, o Django passa ao primeiro plano. O framework traz o ORM para o armazenamento, os comandos de management para a execução e, em conjunto com o Celery — tarefas em segundo plano e periódicas.
Este artigo é a continuação prática do guia panorâmico «Web scraping com Python». As técnicas básicas (requests, BeautifulSoup, codificações) estão explicadas lá; aqui o foco recai sobre a integração com o Django.
Sumário
- Por que scrapear com Django
- Como obtemos a página
- Bibliotecas para parsear o conteúdo
- Armazenamento do resultado em modelos
- Executar o scraping: comandos de management e Celery
- Codificações e caracteres especiais no Django
- Multithreading e tarefas em segundo plano
- Proxies
- Scraping através do TOR
- HTTPS/SSL
- Trabalho com cookies
- Status da resposta e cabeçalhos
- Armazenamento de URLs e filas com o ORM
- Vantagens e desvantagens
1. Por que scrapear com Django
O Django se justifica quando o scraping é apenas uma parte do produto:
- um agregador de produtos, vagas de emprego ou notícias com vitrine web;
- monitoramento de preços com histórico em banco de dados e dashboard;
- atualização periódica do catálogo conforme uma agenda;
- um painel administrativo para gerenciar as fontes e revisar os resultados.
O Django oferece «de fábrica»: o ORM (armazenamento e deduplicação), o admin (gestão de fontes), o sistema de migrações e, com o Celery — a execução em segundo plano e periódica, para que o scraping pesado não bloqueie as requisições web.
2. Como obtemos a página
O cliente HTTP no Django não difere em nada do Python de sempre — usamos requests ou httpx. A lógica de scraping fica fora das views: as views devem ser rápidas, e as requisições de rede vão para a camada de serviços ou para tarefas do Celery.
# parser/services.py
import requests
HEADERS = {
"User-Agent": "Mozilla/5.0 (compatible; MyParserBot/1.0)",
"Accept-Language": "pt-BR,pt;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 o erro e não derrubamos a aplicação
import logging
logging.getLogger("parser").warning("fetch failed %s: %s", url, exc)
return NoneA regra de ouro: nunca dispare um scraping longo diretamente em uma view — o usuário ficará esperando e o worker do servidor web ficará travado. A requisição entra na fila, e a resposta é devolvida de imediato.
3. Bibliotecas para parsear o conteúdo
Dentro do serviço, usam-se as mesmas ferramentas de qualquer outro contexto — BeautifulSoup pela comodidade, lxml pela velocidade:
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 itemsA análise detalhada dos parsers está em «Web scraping com Python» e em «lxml». Se a fonte são tabelas, veja «Extrair tabelas HTML com Python e BeautifulSoup». Se a API devolve JSON — «Parsing de JSON».
4. Armazenamento do resultado em modelos
A força do Django é o ORM. Descreve-se o modelo, e a deduplicação, a filtragem e o histórico de atualizações se tornam triviais.
# parser/models.py
from django.db import models
class Product(models.Model):
source_url = models.URLField(unique=True) # unicidade = proteção contra duplicatas
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.titleGravação com proteção contra duplicatas via 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 atualiza o registro existente ou cria um novo — ideal para um scraping repetido.
5. Executar o scraping: comandos de management e Celery
Comando de management — para execução manual e via 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 = "Executa o scraping do 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"{len(items)} produtos salvos"))Execução: python manage.py run_parser --url https://example.com/catalog. Um comando assim é cômodo de pendurar no cron do sistema.
Celery — para tarefas em segundo plano e 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() # nova tentativa em caso de falha
items = parse_products(html)
save_products(items)
return len(items)A execução periódica é configurada com o django-celery-beat diretamente no admin — por exemplo, atualizar o catálogo a cada 6 horas. O retry do Celery dá resiliência diante das falhas transitórias de rede sem gestão manual.
6. Codificações e caracteres especiais no Django
O problema é o mesmo de qualquer scraper em Python: uma codificação da resposta mal detectada, que quebra acentos e cedilhas. Cura-se do mesmo jeito — resp.apparent_encoding ou trabalhar com resp.content (veja em detalhe «Web scraping com Python», a seção sobre codificações).
A especificidade do Django: certifique-se de que o banco de dados usa UTF-8 (no PostgreSQL — codificação UTF8; no MySQL — utf8mb4); caso contrário, os caracteres acentuados quebrarão já na fase de gravação, não na de scraping. Em um projeto novo esse é o padrão, mas ao trabalhar com um banco de dados antigo convém conferir.
7. Multithreading e tarefas em segundo plano
No Django, raramente se usam threads «na mão» — para o paralelismo existe o Celery com seu pool de workers. Ao subir vários workers, obtém-se o processamento em paralelo da fila sem gerenciar threads manualmente:
celery -A myproject worker --concurrency=8 --loglevel=infoSe for preciso percorrer rápido uma lista de URLs dentro de uma mesma tarefa, o ThreadPoolExecutor dá conta (veja a seção sobre multithreading do hub). Para uma concorrência muito alta dentro da tarefa, dá para aplicar async — mas no Django isso exige cuidado com o ORM (sync_to_async). O tema está tratado em «Scraping assíncrono em Python».
8. Proxies
Os proxies são conectados na camada de serviços — o Django não interfere aqui. É cômodo guardar a lista de proxies em um modelo e marcar os «saudáveis»:
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)O serviço então pega um proxy ativo ao acaso e, diante de um erro, incrementa fail_count; superado o limite, o desativa. As técnicas gerais de rotação — no hub, seção «Proxies».
9. Scraping através do TOR
O TOR é conectado como proxy SOCKS5 (socks5h://127.0.0.1:9050) no mesmo fetch_page. O Django não acrescenta nenhuma especificidade — veja a seção sobre TOR do hub. No servidor com Django, o daemon do TOR roda como processo à parte (serviço do systemd).
10. HTTPS/SSL
A verificação de certificados funciona no nível do requests. Em produção, mantenha verify=True. Se o servidor tiver certificados-raiz desatualizados, atualize o certifi no ambiente virtual do projeto. Os detalhes — no hub.
11. Trabalho com cookies
Para o scraping com autenticação use requests.Session dentro da tarefa. Se precisar reutilizar a sessão entre tarefas do Celery, guarde o cookie jar (por exemplo, serializado no Redis ou em um modelo) e restaure-o antes da requisição. As técnicas básicas — no hub, seção «Cookies».
12. Status da resposta e cabeçalhos
Registre o código de resposta e decida se vale repetir a tarefa. No Celery, isso se encaixa com elegância no 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) # provável bloqueio, esperamos mais
resp.raise_for_status()
return resp.textAssim, os bloqueios anti-bot (429/403/503) se transformam em novas tentativas controladas com espera, e não em perda de dados.
13. Armazenamento de URLs e filas com o ORM
No Django, o frontier se implementa comodamente como uma tabela com estados — sobrevive aos reinícios e aparece no admin.
class CrawlURL(models.Model):
STATUS = [("new", "new"), ("in_progress", "in_progress"),
("done", "done"), ("failed", "failed")]
url = models.URLField(unique=True) # unique = deduplicação 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)O worker pega as URLs «novas», marca-as como in_progress, processa-as e as passa para done/failed. O índice único sobre url exclui as duplicatas. Nos projetos de alta carga, a fila de verdade convém manter no Redis/broker do Celery, guardando no banco de dados apenas o resultado e o histórico. O panorama geral das abordagens de fila — no hub, seção «Filas».
14. Vantagens e desvantagens do scraping com Django
Vantagens:
- O ORM livra da gestão do armazenamento, da deduplicação e das migrações.
- Admin de fábrica — gestão de fontes e revisão dos dados.
- Celery + beat — tarefas em segundo plano e periódicas confiáveis, com retry.
- Uma base de código única: o scraper e a vitrine web no mesmo projeto.
Desvantagens:
- Exagerado para scripts pontuais — um
.pycomum é mais simples. - Exige infraestrutura: broker (Redis/RabbitMQ), workers do Celery.
- O async no Django ainda é cheio de nuances (o ORM é majoritariamente síncrono).
- Para percorrer milhões de páginas, o especializado Scrapy é mais eficiente.