---
title: "O que é ETL? Extração, transformação e carga com Python"
description: "ETL é extrair dados de uma fonte, limpá-los em um formato fixo e carregá-los em um banco de dados. Como funciona, ETL vs. ELT e um job em Python testado."
url: https://proxynet.io/pt-br/blog/what-is-etl
date: 2026-09-28
author: "Acar Diveroli"
category: "Web scraping, Proxy 101"
lang: pt-BR
---

# O que é ETL? Extração, transformação e carga com Python

Um scraper que funcionava bem no mês passado agora enche uma planilha com preços como `£51.77`, o mesmo livro três vezes e uma coluna em que "In stock" aparece ao lado de "In stock (19 available)". Ninguém consegue fazer um gráfico com isso. O scraper fez o trabalho dele; o que falta é tudo o que vem depois: transformar o texto bruto da página em linhas tipadas e sem duplicatas, e guardá-las em um lugar que sobreviva à próxima execução. Essa parte que falta tem nome, e é mais antiga que o web scraping: ETL.

Neste artigo explicamos o que significa ETL, o que cada uma das três etapas faz e como um pipeline ETL é executado do gatilho até a tabela pronta. Comparamos ETL com ELT, vemos as duas propriedades que separam um script de um pipeline (poder repetir a execução com segurança e o monitoramento) e montamos um job completo e testado em Python que extrai dados de livros de um sandbox de scraping, limpa esses dados e os carrega no SQLite. As últimas seções tratam de casos de uso, erros comuns e um guia de decisão curto.

> **Nota: Resposta rápida**
>
> ETL significa **extração, transformação e carga** (extract, transform, load). A extração obtém os dados brutos de uma fonte, como um site, uma API ou um arquivo. A transformação dá a eles um formato fixo: tipos corretos, sem duplicatas e com as linhas que não passam na validação separadas. A carga grava as linhas limpas em um destino como SQLite, PostgreSQL ou um data warehouse. No ELT a ordem muda: os dados brutos são carregados primeiro e transformados dentro do destino. Um bom job ETL pode rodar duas vezes sem criar duplicatas e avisa você quando falha.

## O que é ETL?

ETL é um processo de integração de dados: os dados saem de uma ou mais fontes, são remodelados por regras que você define e chegam a um único armazenamento onde podem ser consultados. O guia de arquitetura da Microsoft o descreve como um processo que [consolida dados de fontes diversas em um armazenamento de dados unificado](https://learn.microsoft.com/pt-br/azure/architecture/data-guide/relational-data/etl), com a transformação aplicada segundo regras de negócio antes da carga.

A ideia vem do data warehousing e se encaixa igualmente bem no scraping. Um site é uma fonte como qualquer outra, só que mais bagunçada: os dados são formatados para pessoas, divididos em várias páginas e podem mudar sem aviso. Se você já escreveu um scraper e depois um segundo script para corrigir a saída dele, já construiu dois terços de um pipeline ETL.

## O que cada etapa faz?

**A extração** coleta os dados brutos e os altera o mínimo possível. Com dados da web, isso significa enviar requisições, seguir a paginação e tirar do HTML os campos de que você precisa. A saída ainda é texto: `"£51.77"`, `"Three"`, `"\n In stock\n"`. Manter a extração simples tem uma vantagem: quando algo quebra, você consegue saber se a fonte mudou ou se foram suas regras de limpeza.

**A transformação** é onde ficam as regras. As operações típicas são:

- converter tipos (o texto do preço para número, a palavra da avaliação para inteiro, uma string de data para data)
- normalizar o texto (espaços em branco, formas Unicode, maiúsculas e minúsculas)
- remover duplicatas com uma chave estável, como a URL do produto
- validar (um preço deve ser positivo, uma avaliação deve ficar entre 1 e 5) e separar as linhas que falham
- enriquecer ou combinar (adicionar uma moeda, uma categoria, o horário do scraping)

Transformar o HTML da página em campos já é uma etapa de parsing; explicamos os tipos de parser e onde eles falham em [O que é parsing de dados?](/pt-br/blog/what-is-data-parsing). Uma limpeza completa com pandas está em [Como limpar dados de scraping com pandas em Python](/pt-br/blog/clean-scraped-data-with-pandas).

**A carga** grava as linhas limpas no destino e torna o resultado visível de uma só vez. Em um projeto pequeno, isso é um arquivo SQLite; em uma equipe, PostgreSQL ou um warehouse na nuvem. A carga também é onde nasce a maioria dos bugs de linhas duplicadas, e por isso o método importa mais que o destino. As vantagens e desvantagens de CSV, JSON e SQLite estão em [Como salvar dados de scraping em CSV, JSON e SQLite](/pt-br/blog/save-scraped-data-csv-json-sqlite).

## Como um pipeline ETL é executado?

Uma execução de um job ETL de scraping agendado passa por estas etapas:

1. **Um gatilho inicia a execução.** Uma entrada do cron, um orquestrador como o Apache Airflow ou uma pessoa rodando o script.
2. **A extração busca a fonte.** As páginas são solicitadas em um ritmo educado, as requisições que falham são repetidas após uma pausa e os campos brutos são coletados.
3. **A transformação limpa cada registro.** Os tipos são convertidos, as duplicatas descartadas, e os registros que quebram uma regra são contados e registrados no log em vez de passarem em silêncio.
4. **Uma barreira de qualidade decide.** Se a extração não retornou nada ou muitas linhas foram rejeitadas, a execução para antes de tocar no destino. Uma mudança de layout no site não deve sobrescrever os dados bons de ontem.
5. **A carga grava em uma única transação.** As linhas são inseridas ou atualizadas pela chave (upsert); ou o lote inteiro é confirmado, ou nada é.
6. **A execução gera um relatório.** As contagens de linhas extraídas, limpas, rejeitadas e carregadas vão para um log ou um alerta, para que uma falha silenciosa fique visível.

Em jobs grandes as fases podem se sobrepor, com a transformação começando nos dados que já chegaram. Com alguns milhares de linhas, rodá-las em sequência facilita a depuração.

## ETL vs. ELT: qual é a diferença?

ELT (extração, carga, transformação) mantém as mesmas três etapas, mas leva a transformação para o destino. Os dados brutos são carregados como estão, e o SQL ou o mecanismo do warehouse os remodela depois. O guia da Microsoft resume a diferença com clareza: no ELT a transformação ocorre no armazenamento de dados de destino.

| | ETL | ELT |
|---|---|---|
| Ordem | Extração, transformação e depois carga | Extração, carga e depois transformação |
| Onde a limpeza roda | No seu script ou em um mecanismo separado | Dentro do banco de dados ou do warehouse (SQL) |
| O que o destino guarda | Só linhas limpas e validadas | Linhas brutas mais tabelas ou views limpas |
| Os dados brutos ficam guardados? | Só se você os armazenar à parte | Sim, por projeto |
| O que exige do destino | Qualquer armazenamento, até um arquivo SQLite | Um destino capaz de transformar em grande escala |
| Corrigir um bug de limpeza | Extrair de novo ou repetir a partir de arquivos brutos salvos | Reescrever o SQL e reconstruir a partir das tabelas brutas |
| Combina com | Jobs de scraping pequenos e médios, esquemas rígidos | Grandes volumes, análise exploratória, regras que mudam |

No scraping existe um meio-termo útil: salvar em disco o HTML ou o JSON bruto de cada execução (uma área de staging) e transformar a partir desses arquivos. Se uma regra de limpeza se mostrar errada, você reconstrói a tabela sem enviar uma única requisição nova ao site.

## Por que um job ETL precisa poder rodar duas vezes com segurança?

Jobs falham no meio do caminho. Uma conexão cai na página 38, a máquina reinicia, um agendador tenta de novo. A pergunta é como fica o destino depois da segunda tentativa. O guia de boas práticas do Apache Airflow responde diretamente: trate uma tarefa como uma [transação em um banco de dados](https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html), nunca produza resultados parciais e troque um `INSERT` simples por um upsert, porque repetir a execução com `INSERT` pode deixar linhas duplicadas.

Essa propriedade se chama **idempotência**: rodar o job uma ou três vezes sobre a mesma entrada deixa o mesmo resultado. Na prática, ela vem de três hábitos:

- **Uma chave natural.** Cada linha tem um identificador que não muda entre execuções. Para uma página de produto, a URL serve; um ID autoincremental, não.
- **Upsert em vez de insert.** Um upsert insere a linha se a chave não existe e a atualiza se ela já existe. O SQLite oferece `INSERT … ON CONFLICT DO UPDATE` desde a versão 3.24.0, e o [qualificador especial `excluded.`](https://sqlite.org/lang_upsert.html) se refere aos valores que teriam sido inseridos. O PostgreSQL tem a mesma sintaxe.
- **Uma transação por carga.** O lote é confirmado inteiro ou revertido, então uma falha nunca deixa meia tabela.

## Agendar e monitorar um job ETL

Um job que só roda quando você lembra é um script. Para uma máquina e um job, o cron (ou o Agendador de Tarefas no Windows) basta. Quando os jobs dependem uns dos outros (fazer scraping, depois limpar, depois gerar um relatório), um orquestrador passa a valer a pena. No Airflow um pipeline é um DAG, um conjunto de tarefas com dependências, e o [argumento `schedule`](https://airflow.apache.org/docs/apache-airflow/stable/core-concepts/dags.html) aceita predefinições como `@daily`, uma string cron ou um intervalo de tempo. O Airflow também repete tarefas que falharam, mais um motivo para que as tarefas sejam idempotentes.

O monitoramento não precisa de uma plataforma no primeiro dia. Registre quatro números em cada execução (páginas buscadas, linhas extraídas, linhas rejeitadas, linhas carregadas) e crie alertas para duas situações: o job não rodou, ou um desses números se afastou muito do valor habitual. Uma queda de 1.000 linhas para 20 costuma significar que o site mudou o layout, não que a loja esgotou o estoque. Do lado do scraping, [Como monitorar alterações em um site e receber alertas](/pt-br/blog/website-change-monitoring) mostra como detectar quando a própria página muda.

## Onde os proxies entram no ETL?

Só na etapa de extração, e só quando o job precisa deles. Algumas páginas por dia de um site público raramente exigem proxy. Eles importam quando você coleta preços que variam por país, quando um job legítimo com muitas requisições esbarra nos limites por IP que um site aplica, ou quando a fonte mostra conteúdo que depende da localização. Um [Proxies residenciais](https://proxynet.io/pt-br/residential-proxy) permite que a etapa de extração solicite páginas a partir de um país e uma cidade escolhidos; um [Proxies rotativos](https://proxynet.io/pt-br/rotating-proxy) distribui as requisições entre vários IPs em rastreamentos maiores.

Um proxy não muda as regras. O robots.txt do site, os termos de uso e um ritmo de requisições razoável continuam valendo, e uma API oficial ou uma exportação vem primeiro sempre que existir. Tratamos o lado das requisições com mais detalhes em [Como fazer web scraping sem ser bloqueado](/pt-br/blog/web-scraping-without-getting-blocked) e na nossa página de [extração de dados](/pt-br/data-scraping).

## Um job ETL completo em Python

O script abaixo é um pipeline inteiro em um só arquivo. Ele extrai todos os cards de livros de [books.toscrape.com](https://books.toscrape.com/), um sandbox criado para praticar scraping, transforma os campos em linhas tipadas e os carrega no SQLite com um upsert. Ele repete as requisições em respostas `429` e `5xx` com pausas crescentes, espera um segundo entre páginas, para se muitas linhas falharem na validação e envia o tráfego por um proxy só se você definir `PROXY_URL`.

Primeiro instale as duas dependências:

```bash
pip install requests beautifulsoup4
```

Depois salve isto como `etl_books.py`:

```python
"""Um pequeno job ETL: books.toscrape.com -> linhas limpas -> SQLite."""
import argparse
import logging
import os
import sqlite3
import time
import unicodedata
from datetime import datetime, timezone
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

BASE = "https://books.toscrape.com/catalogue/"
RATINGS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
log = logging.getLogger("etl")

def make_session():
    retry = Retry(total=4, backoff_factor=1.5,
                  status_forcelist=(429, 500, 502, 503, 504),
                  respect_retry_after_header=True)
    s = requests.Session()
    s.mount("https://", HTTPAdapter(max_retries=retry))
    s.headers["User-Agent"] = "etl-demo/1.0 (contact: you@example.com)"
    proxy = os.getenv("PROXY_URL")  # ex.: http://user:pass@pr.proxynet.io:8000
    if proxy:
        s.proxies = {"http": proxy, "https": proxy}
    return s

# ---------- EXTRACT: buscar páginas, manter as strings brutas ----------
def extract(session, max_pages, delay=1.0):
    raw, url, page = [], urljoin(BASE, "page-1.html"), 0
    while url and page < max_pages:
        resp = session.get(url, timeout=15)
        resp.raise_for_status()
        soup = BeautifulSoup(resp.content, "html.parser")
        for card in soup.select("article.product_pod"):
            raw.append({
                "title": card.h3.a["title"],
                "href": card.h3.a["href"],
                "price": card.select_one("p.price_color").get_text(),
                "rating": card.select_one("p.star-rating")["class"][-1],
                "stock": card.select_one("p.availability").get_text(),
                "page_url": url,
            })
        page += 1
        nxt = soup.select_one("li.next a")
        url = urljoin(url, nxt["href"]) if nxt else None
        time.sleep(delay)
    log.info("extract: %d pages, %d raw rows", page, len(raw))
    return raw

# ---------- TRANSFORM: tipos, limpeza, duplicatas, validação ----------
def transform(raw):
    clean, rejected, seen = [], 0, set()
    now = datetime.now(timezone.utc).isoformat(timespec="seconds")
    for r in raw:
        try:
            url = urljoin(r["page_url"], r["href"])
            if url in seen:
                continue
            seen.add(url)
            title = " ".join(unicodedata.normalize("NFKC", r["title"]).split())
            price = float(r["price"].strip().lstrip("Â£"))
            rating = RATINGS[r["rating"]]
            in_stock = "in stock" in r["stock"].lower()
            if not title or price <= 0:
                raise ValueError("empty title or bad price")
        except (KeyError, ValueError) as exc:
            rejected += 1
            log.warning("rejected %r: %s", r.get("title"), exc)
            continue
        clean.append((url, title, price, rating, int(in_stock), now))
    log.info("transform: %d clean, %d rejected", len(clean), rejected)
    return clean, rejected

# ---------- LOAD: upsert no SQLite em uma única transação ----------
def load(rows, db_path):
    con = sqlite3.connect(db_path)
    with con:  # confirma se der certo, reverte se houver erro
        con.execute("""CREATE TABLE IF NOT EXISTS books (
            url TEXT PRIMARY KEY, title TEXT NOT NULL, price_gbp REAL NOT NULL,
            rating INTEGER, in_stock INTEGER, updated_at TEXT)""")
        con.executemany("""INSERT INTO books VALUES (?, ?, ?, ?, ?, ?)
            ON CONFLICT(url) DO UPDATE SET title=excluded.title,
              price_gbp=excluded.price_gbp, rating=excluded.rating,
              in_stock=excluded.in_stock, updated_at=excluded.updated_at""", rows)
        total = con.execute("SELECT COUNT(*) FROM books").fetchone()[0]
    con.close()
    log.info("load: %d rows upserted, %d rows in table", len(rows), total)
    return total

def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--pages", type=int, default=3)
    ap.add_argument("--db", default="books.db")
    args = ap.parse_args()
    logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
    raw = extract(make_session(), args.pages)
    rows, rejected = transform(raw)
    if not rows or rejected > len(raw) * 0.1:  # barreira: não carregar um lote quebrado
        raise SystemExit(f"aborting load: {len(rows)} clean, {rejected} rejected")
    load(rows, args.db)

if __name__ == "__main__":
    main()
```

Rode o script para o catálogo inteiro com `python etl_books.py --pages 60`. O site tem 50 páginas, então o loop para sozinho quando o link "next" desaparece. Nossa execução imprimiu:

```text
INFO extract: 50 pages, 1000 raw rows
INFO transform: 1000 clean, 0 rejected
INFO load: 1000 rows upserted, 1000 rows in table
```

Rode uma segunda vez e a última linha continua terminando em `1000 rows in table`. O upsert atualizou as linhas existentes em vez de adicionar cópias: essa é a idempotência descrita acima. Também testamos os caminhos de falha. Quando uma conexão estourou o tempo limite, as novas tentativas rodaram e o script terminou com erro antes da etapa de carga, então a tabela manteve as linhas anteriores. Ao passar para `transform()` um preço `£x` e uma avaliação `Six`, as duas linhas foram rejeitadas com o motivo registrado no log, e uma URL repetida foi descartada como duplicata.

Para enviar a etapa de extração por um proxy, defina a variável antes de rodar; nada mais muda:

```bash
export PROXY_URL="http://user:pass@pr.proxynet.io:8000"
python etl_books.py --pages 60
```

Para rodar todas as noites às 03:00 no Linux, adicione ao crontab uma linha como `0 3 * * * cd /opt/etl && .venv/bin/python etl_books.py --pages 60 >> etl.log 2>&1`. Quando você tiver vários jobs dependentes, leve-os para um orquestrador.

## Onde o ETL é usado com dados da web

- **Monitoramento de preços:** um job noturno extrai os preços da concorrência, normaliza as moedas e carrega uma tabela de histórico; veja nossa página de [monitoramento de preços](/pt-br/price-monitoring) e [Como monitorar preços da concorrência no e-commerce](/pt-br/blog/competitor-price-tracking).
- **Pesquisa de mercado:** quantidade de produtos, avaliações e mudanças de sortimento em muitas lojas, carregadas em um só esquema; veja [pesquisa de mercado](/pt-br/market-research).
- **Rastreamentos grandes:** primeiro se descobrem milhares de URLs e depois se extraem os dados de cada uma; nossa página de [web crawler](/pt-br/web-crawler) cobre a parte da descoberta.
- **Dados alternativos:** analistas combinam dados da web com outras fontes, e as regras de qualidade da etapa de transformação decidem se o resultado é confiável; veja [O que são dados alternativos?](/pt-br/blog/what-is-alternative-data).
- **Análise e mineração:** uma tabela limpa e carregada é a entrada de que a [mineração de dados](/pt-br/blog/what-is-data-mining) precisa.

## Erros comuns em ETL

- **Limpar dentro da etapa de extração.** Quando o parsing e a conversão acontecem no mesmo loop, você não consegue diferenciar uma mudança no site de um bug nas suas regras. Mantenha a extração bruta.
- **`INSERT` simples a cada execução.** A segunda execução duplica a tabela. Use uma chave natural e um upsert.
- **Nenhuma barreira de qualidade.** Uma mudança de layout transforma todos os preços em `None`, e o job carrega tranquilamente 1.000 linhas vazias por cima dos dados bons.
- **Confirmar linha por linha.** Uma falha no meio deixa uma tabela que não é nem o estado antigo nem o novo.
- **Rejeições silenciosas.** Descartar linhas ruins sem contá-las esconde problemas até que alguém perceba que os números parecem magros.
- **Codificação errada.** Ler bytes com o conjunto de caracteres errado transforma `£` em `Â£`; nosso artigo sobre [erros de Unicode no Python](/pt-br/blog/python-unicode-encoding-errors) explica a causa.
- **Ignorar os limites da fonte.** Sem intervalo, sem pausa entre tentativas, sem respeitar `Retry-After`. O site começa a responder com `429`; veja [429 Too Many Requests explicado](/pt-br/blog/http-429-too-many-requests).

## Guia de decisão

| Sua situação | O que usar |
|---|---|
| Uma fonte, alguns milhares de linhas, uma pessoa | Um único script ETL em Python com SQLite, rodando pelo cron |
| As regras mudam com frequência e você quer manter os dados brutos | ELT: carregue linhas ou arquivos brutos e transforme com SQL |
| Vários jobs que dependem uns dos outros | Um orquestrador como o Apache Airflow |
| Grandes volumes e um warehouse na nuvem já em uso | ELT dentro do warehouse |
| Preços ou conteúdo que variam por país | ETL com um proxy residencial na etapa de extração |
| A fonte oferece uma API ou um download | Extraia da API; deixe o scraping de lado |

## Perguntas frequentes

### O que significa ETL?

Extract, transform, load: extração, transformação e carga. Os dados são extraídos de uma fonte, transformados em um formato consistente e validado e carregados em um armazenamento de destino, como um banco de dados ou um data warehouse.

### Web scraping é a mesma coisa que ETL?

Não. O scraping é uma forma de fazer a etapa de extração. O ETL também cobre o que acontece depois: limpar, remover duplicatas, validar e carregar os dados em um lugar onde possam ser consultados e mantidos atualizados.

### O ETL ainda é usado ou foi substituído pelo ELT?

Os dois são usados. O ELT é comum quando um warehouse na nuvem consegue fazer a transformação pesada e os dados brutos devem ser mantidos. O ETL continua sendo a escolha natural quando o destino é pequeno, o esquema é rígido ou os dados precisam ser limpos antes de serem armazenados.

### Dá para montar um pipeline ETL só com Python?

Sim. Para jobs pequenos e médios, `requests`, um parser de HTML e o módulo `sqlite3` da biblioteca padrão bastam, como mostra o script acima. Bibliotecas como o pandas ajudam quando a etapa de transformação fica mais pesada.

### O que é um pipeline ETL?

É o processo ETL preparado para rodar repetidamente: um gatilho, as três etapas, uma barreira de qualidade e um relatório, normalmente em um agendamento. Um script avulso vira pipeline quando consegue rodar sem supervisão e se recuperar de falhas.

### Preciso do Airflow para fazer ETL?

Não para um job em uma única máquina; o cron basta. Um orquestrador ajuda quando várias tarefas dependem umas das outras, precisam de novas tentativas e histórico de execuções, ou são compartilhadas por uma equipe.

## Em resumo

ETL é a parte de um projeto de dados que vem depois da coleta: extrair os dados brutos, transformá-los em linhas tipadas, sem duplicatas e validadas, e carregá-los em uma única transação com um upsert, para que repetir a execução nunca duplique a tabela. O ELT leva a transformação para o destino e combina com grandes warehouses; para a maioria dos jobs de scraping, um pequeno script ETL com SQLite é o ponto de partida certo. Adicione uma barreira de qualidade e quatro números registrados, agende o job e use um proxy só quando a etapa de extração precisar de outra localização ou de mais IPs. Quando for o caso, veja nossos [planos de proxy](/pt-br/proxy).
