Scraping por linguagem 8 min de leitura

Web scraping com Django: coleta de dados dentro de uma aplicação web

Como integrar o web scraping em uma aplicação Django: comandos de management, tarefas do Celery e armazenamento de resultados em modelos e no admin.

EW
Equipe Web-Scraping.biz
Coleta de dados para as demandas do negócio
Publicado: 7 janeiro 2025

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

  1. Por que scrapear com Django
  2. Como obtemos a página
  3. Bibliotecas para parsear o conteúdo
  4. Armazenamento do resultado em modelos
  5. Executar o scraping: comandos de management e Celery
  6. Codificações e caracteres especiais no Django
  7. Multithreading e tarefas em segundo plano
  8. Proxies
  9. Scraping através do TOR
  10. HTTPS/SSL
  11. Trabalho com cookies
  12. Status da resposta e cabeçalhos
  13. Armazenamento de URLs e filas com o ORM
  14. 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.

python
# 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 None

A 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:

python
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 items

A 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.

python
# 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.title

Gravação com proteção contra duplicatas via update_or_create:

python
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

python
# 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

python
# 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:

bash
celery -A myproject worker --concurrency=8 --loglevel=info

Se 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»:

python
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.


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:

python
@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.text

Assim, 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.

python
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 .py comum é 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.