Saltar para o conteúdo
Voltar ao blogue

Web scraping com Python em 2026: o manual prático do programador

Raluca PenciucÚltima atualização em 34 min read
Web scraping com Python em 2026: o manual prático do programador
Resumo: Este é um guia prático sobre web scraping em Python que começa com uma árvore de decisão e, em seguida, aborda o Requests em conjunto com o Beautiful Soup, o XPath com o lxml, o Playwright, o Selenium, o Scrapy e uma API de scraping para alvos difíceis. No final, ficará com código funcional, um manual de estratégias anti-bloqueio, padrões de armazenamento compatíveis com LLM e uma lista de verificação para produção.

Se escreveres python web scraping numa barra de pesquisa, obtém centenas de tutoriais que instalam sempre as mesmas três bibliotecas e param antes das partes interessantes. Este guia é diferente. O scraping da Web com Python consiste no processo de escrever um script que recupera uma página Web, analisa o seu HTML e extrai campos estruturados que pode inserir numa folha de cálculo, numa base de dados ou num pipeline de modelos. A mecânica é simples. A razão pela qual as pessoas têm dificuldades é que a ferramenta certa depende do site alvo, e escolher a errada faz-nos perder horas.

Este tutorial tem uma abordagem bem definida. Começa com uma breve árvore de decisão para que não instales o Playwright numa página que o Requests poderia ter analisado em cinquenta linhas. Em seguida, percorre todas as camadas de uma pilha de scraping real: HTML estático, renderização de JavaScript, seletores CSS versus XPath, limpeza de campos, escalabilidade com o Scrapy, rotação de proxies, tratamento de erros HTTP, armazenamento dos resultados em CSV, SQLite ou JSONL para LLMs a jusante e agendamento de execuções recorrentes.

Irá ver código lado a lado para o mesmo alvo em diferentes bibliotecas, para que as vantagens e desvantagens sejam visíveis, em vez de serem ignoradas. Também irá encontrar notas sinceras sobre bloqueios, custos de manutenção e quando uma API hospedada representa um melhor uso do seu tempo do que outra time.sleep(random.uniform(1, 3)) . Se já escreveu um scraper em Python e este falhou em produção, este guia irá mostrar-lhe porquê e como evitar que isso se repita.

Por que razão o web scraping em Python é a escolha padrão em 2026

O web scraping em Python domina o setor por três razões: sintaxe legível, um ecossistema maduro e integração simples com o resto da pilha de dados. Uma dúzia de linhas de Python consegue descarregar uma página, analisá-la e fornecer-lhe uma lista de dicionários prontos para o pandas. Esse caminho curto de «URL» a «dataframe» é a razão pela qual os engenheiros de dados, analistas e equipas de ML continuam a optar por ele em vez do Go ou do Node.

O conjunto de bibliotecas é excepcionalmente abrangente. O Requests e o httpx tratam da camada HTTP. O Beautiful Soup e o lxml analisam HTML com seletores CSS ou XPath. O Playwright e o Selenium controlam navegadores reais para páginas renderizadas em JavaScript. O Scrapy fornece uma estrutura completa de rastreamento com concorrência, novas tentativas, pipelines de itens e formatos de exportação prontos a usar. Cada um deles foi testado em condições reais e é mantido ativamente.

A segunda parte da história é a superfície de integração. Os resultados do scraping fluem diretamente para pandas, DuckDB, SQLite, Parquet ou JSONL para ingestão em LLM. Os notebooks aceleram a iteração. As sugestões de tipos e as classes de dados garantem a integridade dos registos. Também não existe uma lacuna significativa entre o protótipo e a produção, porque o mesmo script que funciona no Jupyter é executado no cron ou num contentor sem necessidade de reescrita. Essa continuidade de ponta a ponta é o que torna o Python a escolha pragmática por defeito para o scraping da Web em 2026, e a razão pela qual o resto deste guia se baseia exclusivamente nele.

Escolha a ferramenta certa: uma árvore de decisão para o seu site alvo

Antes de instalar qualquer coisa, responda a quatro perguntas sobre o alvo. Cada uma delas reduz a escolha de bibliotecas numa ordem de magnitude, e seguir esta árvore de decisão poupará mais tempo do que qualquer truque de otimização.

Passo 1: O site tem uma API oficial? Se sim, utilize-a. Uma API é mais rápida, mais barata e juridicamente mais segura do que o scraping. Reserve o scraping para páginas que sejam públicas, mas não legíveis por máquinas.

Passo 2: Os dados estão presentes no HTML bruto que obtém de curl ou requests.get? Abra a página num navegador, veja o código-fonte (não «inspecionar elemento») e procure um dos valores que pretende. Se estiver lá, tem uma página estática. O Requests juntamente com o Beautiful Soup é suficiente. Não recorra a um navegador sem interface gráfica.

Passo 3: O conteúdo é inserido por JavaScript após o carregamento inicial do HTML? Nesse caso, precisa de um contexto de navegador real. O Playwright é a escolha padrão atual. O Selenium serve se a sua equipa já o utilizar na integração contínua (CI). Ambos podem aguardar seletores, clicar, percorrer a página e extrair dados do DOM renderizado.

Passo 4: Está a rastrear milhares de URLs, a encadear pedidos ou a executar tarefas de longa duração? Opte pelo Scrapy. O seu ciclo de eventos, pipelines de itens, controlos de concorrência e tentativas de repetição integradas superam qualquer ciclo desenvolvido internamente.

Existe um quinto ramo para alvos difíceis. Se o site identificar navegadores de forma agressiva, bloquear IPs de centros de dados ou apresentar CAPTCHAs logo à primeira tentativa, mantém o teu código de análise, mas delega a camada de obtenção de dados a uma API de scraping. Trata-se de uma alternativa, não de uma opção padrão.

Utilize esta referência rápida:

Sinal no site alvo

Melhor ferramenta

API pública disponível

A API

Dados no HTML inicial

Pedidos + Beautiful Soup

Conteúdo renderizado em JavaScript

Playwright (ou Selenium)

Milhares de páginas, tentativas de repetição, pipelines

Scrapy

CAPTCHAs, bloqueios de IP, identificação de TLS

API de scraping

A maioria dos projetos de scraping web em Python deve começar com a ferramenta mais leve que consiga recolher os dados, passando para a próxima camada apenas quando a atual atingir o seu limite. Isto reflete as orientações da comunidade: a melhor configuração é a mais simples que consiga obter os dados de forma fiável.

Configuração do ambiente: Python 3, Virtualenv e bibliotecas necessárias

Utilize o Python 3.10 ou mais recente. Crie um virtualenv isolado para cada projeto, para que as dependências não se espalhem pelo Python do seu sistema:

python3 -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate

pip install --upgrade pip
pip install requests beautifulsoup4 lxml pandas
pip install playwright
pip install scrapy
pip install selenium webdriver-manager

# Playwright ships as a Python package plus browser binaries.
# Install the Chromium binary once:
python -m playwright install chromium

lxml funciona simultaneamente como o analisador Beautiful Soup mais rápido e como o motor XPath que utilizaremos mais tarde. pandas é opcional, mas útil para limpeza e exportação. webdriver-manager trata do download do ChromeDriver do Selenium, para que não tenha de corresponder manualmente as versões do navegador em cada executor de CI.

Algumas notas práticas antes de começar:

  • Se estiver a utilizar Apple Silicon, utilize a compilação do Python para ARM64. O binário do Chromium do Playwright é muito mais rápido em ARM nativo do que no Rosetta.
  • No Windows, ative o venv com .venv\Scripts\activate e dê preferência ao PowerShell em vez do cmd para obter traços de pilha legíveis.
  • Congele as versões assim que o seu scraper estiver a funcionar: pip freeze > requirements.txt. Os scrapers são, por natureza, frágeis, e fixar um conjunto de dependências comprovadamente funcional é a forma mais económica de garantir que a execução do cron de amanhã não falhe devido a uma atualização silenciosa da biblioteca.
  • Se pretender utilizar contentores, insira o playwright install passo no seu Dockerfile para que os binários do navegador fiquem na imagem, em vez de serem descarregados a cada arranque a frio.

Analise a página de destino antes de escrever uma única linha de código

Cada hora passada no DevTools poupa dez horas de depuração. Abre a página que pretendes extrair no Chrome ou no Firefox e, em seguida, analisa os três painéis antes de tocares no teu editor.

O painel «Elementos». Clique com o botão direito do rato num valor que lhe interesse e selecione «Inspecionar». Tome nota da tag, da classe e de qualquer atributo estável, como data-testid ou itemprop. Os IDs e as classes que parecem ter sido gerados automaticamente (css-1x9k2j) são voláteis e irão comprometer os teus seletores na próxima implementação. Dá preferência a atributos semânticos, sempre que existirem.

O separador «Rede». Atualize a página com o separador «Rede» aberto e filtre por XHR ou Fetch. Muitos sites modernos apresentam listas chamando um endpoint JSON interno. Se conseguires identificar um que devolva os campos de que precisas, aceder diretamente a esse endpoint com o Requests é mais rápido, mais fiável e menos propenso a falhas do que analisar o HTML. Esta é a maior vantagem que a maioria dos tutoriais de scraping ignora.

Copiar como cURL. Clique com o botão direito do rato em qualquer pedido na separador «Rede» e selecione «Copiar como cURL». Cole-o no seu terminal para confirmar que funciona e, em seguida, traduza-o para Python. A ferramenta curlconverter.com faz isso automaticamente, mas ler os cabeçalhos manualmente ensina-te o que o servidor realmente espera: um User-Agent, um Referer, um cookie de sessão ou um token CSRF.

Se o HTML inicial contiver os teus dados, estás no caminho estático. Se os teus dados só aparecerem depois de a página carregar e o JavaScript ser executado, estás no caminho do navegador. Se existir uma API JSON oculta, opta sempre por ela. O resto deste guia parte do princípio de que já fizeste essa verificação antes de escreveres código.

Criar um scraper estático com Requests e Beautiful Soup

Para uma página estática, bastam três passos: buscar, analisar e extrair. Vamos utilizar books.toscrape.com, um alvo de demonstração público que existe especificamente para que os tutoriais não sobrecarreguem sites reais.

import requests
from bs4 import BeautifulSoup

URL = "https://books.toscrape.com/catalogue/page-1.html"
HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; MyScraper/1.0)"}

resp = requests.get(URL, headers=HEADERS, timeout=15)
resp.raise_for_status()          # raises on 4xx / 5xx

Um resp.status_code de 200 significa que a recuperação foi bem-sucedida. Qualquer outra coisa é um sinal, não uma nota de rodapé. raise_for_status() transforma-o numa exceção que pode ser capturada. Defina um valor explícito timeout em cada pedido. O valor predefinido é «esperar para sempre», que é exatamente o que não se quer numa tarefa agendada.

Analise o HTML com lxml, que é significativamente mais rápido do que o html.parser:

soup = BeautifulSoup(resp.text, "lxml")

Now select. O Beautiful Soup oferece-lhe três formas de consultar a árvore. As duas que irá utilizar diariamente são find_all para pesquisas baseadas em tags e select para seletores CSS. Prefira select quando o alvo tiver classes estáveis:

books = []
for card in soup.select("article.product_pod"):
    title = card.select_one("h3 a")["title"]
    price = card.select_one("p.price_color").get_text(strip=True)
    stock = card.select_one("p.instock").get_text(strip=True)
    books.append({"title": title, "price": price, "stock": stock})

print(books[:3])

Alguns hábitos vão poupar-lhe trabalho mais tarde:

  • Proteja todos os seletores. select_one retorna None se nada corresponder, pelo que .get_text() haverá uma falha na próxima iteração. Utilize if card.select_one(...) ou o operador «walrus» para ignorar as linhas em falta.
  • Extraia o texto com strip=True. O HTML está repleto de espaços em branco e espaços não separáveis. Limpar na altura da extração é mais económico do que corrigir todos os campos a jusante mais tarde.
  • Armazene valores brutos, não valores «finais». Guarde a cadeia de caracteres do preço como "£51.77" e normalize-a numa etapa separada. Quando a sua extração falhar, vai querer ver o que o servidor realmente devolveu.

Para acompanhar a paginação, procura o link «seguinte» e repete o ciclo até este desaparecer:

def scrape_all_pages(start_url):
    url = start_url
    results = []
    while url:
        r = requests.get(url, headers=HEADERS, timeout=15)
        r.raise_for_status()
        s = BeautifulSoup(r.text, "lxml")
        for card in s.select("article.product_pod"):
            results.append({
                "title": card.select_one("h3 a")["title"],
                "price": card.select_one("p.price_color").get_text(strip=True),
            })
        next_link = s.select_one("li.next a")
        url = requests.compat.urljoin(url, next_link["href"]) if next_link else None
    return results

Este é um scraper web em Python estático, completo e honesto: recupere com um cabeçalho real, analise com lxml, selecione com CSS, verifique se faltam nós, siga o link para a página seguinte e termine de forma limpa quando a paginação chegar ao fim. Para um guia mais aprofundado do Beautiful Soup com tabelas, formulários e elementos aninhados, consulte o nosso tutorial complementar sobre o Beautiful Soup.

Há mais dois hábitos que vale a pena adotar logo no primeiro dia. Primeiro, envolva o pedido num try/except requests.RequestException e registe o URL que falhou. Os erros de rede são inevitáveis em grande escala, e um scraper que falhe logo na primeira ConnectionError nunca vai conseguir concluir um rastreio noturno. Em segundo lugar, guarde os resultados no disco a cada algumas centenas de registos, em vez de manter tudo na memória:

import json

def checkpoint(records, path="checkpoint.jsonl"):
    with open(path, "a", encoding="utf-8") as f:
        for r in records:
            f.write(json.dumps(r, ensure_ascii=False) + "\n")

Agora, se o processo falhar na página 47 de 100, terá as primeiras 46 páginas no disco e poderá retomar sem ter de voltar a recuperar os dados. Esses dois hábitos — tratamento de erros e gravação de pontos de verificação — distinguem os scrapers de demonstração daqueles que pode realmente deixar a funcionar.

Seletores CSS vs. XPath: Escolha uma linguagem de consulta e mantenha-se fiel a ela

O Beautiful Soup oferece-lhe seletores CSS. lxml oferece ambos, mas o seu principal trunfo é o XPath. Os dois resolvem o mesmo problema com modos de falha diferentes, e escolher um deliberadamente é mais importante do que escolher o «melhor».

Os seletores CSS são mais curtos, mais familiares e parecem-se com código front-end. São excelentes para a seleção baseada em classes: article.product_pod > h3 a. Têm dificuldade com tudo o que exija percorrer o DOM, corresponder conteúdo de texto ou navegar em relação a um elemento irmão.

O XPath é mais expressivo. Lida com a navegação por eixos (ancestor::, following-sibling::), correspondência de texto (//a[text()="Next"]) e seleção posicional ((//tr)[3]). A mesma extração com lxml e o XPath fica assim:

from lxml import html

tree = html.fromstring(resp.text)
titles = tree.xpath("//article[contains(@class,'product_pod')]//h3/a/@title")
prices = tree.xpath("//article[contains(@class,'product_pod')]//p[@class='price_color']/text()")

A questão da manutenção é mais importante do que a estética. Os seletores que dependem de nomes de classes deixam de funcionar no dia em que o designer do site renomeia uma classe. Os seletores que dependem da estrutura (div > div > span:nth-child(2)) deixam de funcionar no momento em que alguém adiciona um elemento envolvente. As expressões XPath construídas com base em contains(@class, ...) ou em atributos estáveis, como [@itemprop="price"] resistem a ambas as situações. Na prática, as equipas que optam pelo XPath tendem a escrever scrapers mais resilientes, à custa de uma curva de aprendizagem ligeiramente mais acentuada.

Escolha uma linguagem de consulta por projeto e mantenha a consistência. Uma base de código que mistura soup.select(...) e tree.xpath(...) é aquela em que cada novo engenheiro tem de aprender ambas. O nosso guia de XPath e a nossa comparação entre XPath e seletores CSS abordam os padrões de navegação por eixos que fazem com que valha a pena mudar para o XPath quando a marcação sofre alterações.

Uma opção padrão pragmática para equipas de web scraping em Python: comece cada novo spider com o Beautiful Soup e seletores CSS, porque funcionam de forma semelhante ao front-end que está a extrair. Recorra ao XPath assim que se deparar com um caso em que precise de subir na árvore, filtrar por conteúdo de texto ou navegar por índice. Isso cobre cerca de 90% das tarefas com a ferramenta mais simples e reserva a ferramenta mais expressiva para os momentos em que ela realmente compensa.

Limpar e normalizar os campos extraídos

As cadeias de caracteres extraídas em bruto quase nunca são utilizáveis. Os preços aparecem com símbolos monetários, as datas em meia dúzia de formatos, espaços em branco por todo o lado e, ocasionalmente, \u00a0 espaço não separável escondido no meio de texto que, de resto, está limpo. Separe a extração da normalização para que ambas as etapas continuem a poder ser depuradas.

import re
from datetime import datetime

def to_float_price(raw: str) -> float | None:
    if not raw:
        return None
    cleaned = re.sub(r"[^\d.,]", "", raw).replace(",", "")
    try:
        return float(cleaned)
    except ValueError:
        return None

def to_iso_date(raw: str, fmt: str) -> str | None:
    try:
        return datetime.strptime(raw.strip(), fmt).date().isoformat()
    except (ValueError, AttributeError):
        return None

def clean_text(raw: str) -> str:
    return " ".join(raw.replace("\xa0", " ").split()) if raw else ""

Aplica estes auxiliares numa única passagem pelos teus registos em bruto:

def normalize(record):
    return {
        "title": clean_text(record["title"]),
        "price": to_float_price(record["price"]),
        "in_stock": "in stock" in clean_text(record["stock"]).lower(),
    }

normalized = [normalize(r) for r in raw_records]

Elimine duplicados antes de guardar. Uma chave primária estável baseada no URL ou no ID do produto evita que a sua tarefa cron diária multiplique silenciosamente as linhas. Se estiver a exportar para o pandas, df.drop_duplicates(subset=["url"]) resolva isso numa única linha.

Vale a pena conhecer duas armadilhas de codificação. Primeiro, requests adivinha a codificação da resposta a partir dos cabeçalhos e pode estar errado; define resp.encoding = "utf-8" explicitamente se vir caracteres ilegíveis. Em segundo lugar, alguns sites devolvem caracteres escapados como entidades HTML, tal como ’ mesmo em navegadores modernos. O Beautiful Soup descodifica esses caracteres durante a análise, mas se obtiver JSON diretamente, utilize html.unescape() antes de armazenar. A normalização consistente é o que distingue um scraper de demonstração de um conjunto de dados que se pode realmente consultar.

Extrair páginas renderizadas em JavaScript com o Playwright

As ferramentas estáticas falham assim que um site renderiza o seu conteúdo no navegador. As aplicações modernas em React, Vue e Svelte costumam devolver um esqueleto HTML quase vazio, para depois preencher o DOM com o JSON obtido. O Beautiful Soup irá ver o esqueleto, não os dados. Precisas de um contexto de navegador real, e o Playwright é a forma mais simples de o obter em Python.

Instale uma vez (isto inclui também o binário do Chromium):

pip install playwright
python -m playwright install chromium

Um scraper completo e executável para uma página renderizada em JavaScript:

from playwright.sync_api import sync_playwright

def scrape_js_page(url: str):
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            user_agent="Mozilla/5.0 (compatible; MyScraper/1.0)",
            viewport={"width": 1366, "height": 900},
        )
        page = context.new_page()
        page.goto(url, wait_until="domcontentloaded", timeout=30_000)

        # Wait for the element that only appears after JS runs.
        page.wait_for_selector("article.product_pod", timeout=15_000)

        cards = page.query_selector_all("article.product_pod")
        results = []
        for card in cards:
            title = card.query_selector("h3 a").get_attribute("title")
            price = card.query_selector("p.price_color").inner_text().strip()
            results.append({"title": title, "price": price})

        browser.close()
        return results

Dois detalhes determinam se este código é fiável ou instável.

Estratégia de espera. wait_until="domcontentloaded" é acionada quando o HTML inicial é analisado. "networkidle" espera até que a atividade de rede pare durante 500 ms, o que é mais rigoroso, mas mais lento. Em aplicações de página única, prefira wait_for_selector num elemento concreto de que realmente precisas. É mais rápido do que networkidle e menos frágil do que um time.sleep.

Tratamento do carregamento dinâmico. Para a rolagem infinita, utilize page.mouse.wheel(0, 2000) num ciclo até que o número de registos deixe de mudar. Para conteúdo acedido através de um clique, page.click("button.load-more") e volte a selecionar. Para páginas que exigem início de sessão, efetue o início de sessão uma vez com context.storage_state(path="auth.json") e reutilize o ficheiro de estado em todas as execuções, para não ter de se reautenticar em cada tarefa.

O Playwright também suporta assíncrono, interceção de pedidos (bloqueio de imagens e tipos de letra para acelerar o processo) e alvos em vários navegadores. Para rastreios de grande volume, é frequentemente combinado com o Scrapy através de scrapy-playwright. O nosso guia do Playwright aborda esses padrões em profundidade.

Para páginas com deslocamento infinito, um padrão comum é deslocar-se até que a contagem de registos deixe de aumentar:

def scroll_until_stable(page, selector, max_rounds=20):
    prev = 0
    for _ in range(max_rounds):
        page.mouse.wheel(0, 4000)
        page.wait_for_timeout(1000)
        count = len(page.query_selector_all(selector))
        if count == prev:
            break
        prev = count
    return prev

Para reduzir ainda mais a sobrecarga do navegador, bloqueie recursos pesados de que não necessita:

context.route("**/*.{png,jpg,jpeg,gif,svg,woff2,mp4}", lambda route: route.abort())

Essa única linha reduz frequentemente o tempo de carregamento da página em 40% ou mais em sites com muitos conteúdos multimédia, uma vez que se saltam downloads que acabariam por ser descartados. Combinado com wait_for_selector, torna o Playwright genuinamente competitivo com os scrapers estáticos em termos de velocidade para as páginas que necessitam de renderização em JavaScript. Ao executar o Playwright em grande escala, inicie um contexto de navegador persistente por trabalhador, em vez de um navegador novo por URL, e reutilize a mesma página em pedidos semelhantes. O arranque a frio do Chromium demora cerca de um segundo por página, e essa sobrecarga domina qualquer rastreio real.

O Selenium como alternativa para páginas em JavaScript

O Selenium antecede o Playwright em cerca de uma década, e muitas equipas ainda o utilizam na integração contínua (CI), porque a sua infraestrutura de testes existente já é compatível com o WebDriver. É uma escolha perfeitamente razoável para o scraping, se for uma dessas equipas.

Um scraper mínimo do Selenium utilizando webdriver-manager para que não tenha de descarregar manualmente o ChromeDriver:

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from webdriver_manager.chrome import ChromeDriverManager

opts = Options()
opts.add_argument("--headless=new")
opts.add_argument("--window-size=1366,900")

driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
driver.get("https://books.toscrape.com/")

WebDriverWait(driver, 15).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, "article.product_pod"))
)

results = []
for card in driver.find_elements(By.CSS_SELECTOR, "article.product_pod"):
    title = card.find_element(By.CSS_SELECTOR, "h3 a").get_attribute("title")
    price = card.find_element(By.CSS_SELECTOR, "p.price_color").text.strip()
    results.append({"title": title, "price": price})

driver.quit()

O Selenium depende de um WebDriver, que é a ponte entre o seu script Python e o processo real do navegador. O Chrome utiliza o ChromeDriver, o Firefox utiliza o GeckoDriver, o Edge utiliza o EdgeDriver, e cada um tem de corresponder à versão principal do navegador.

Em comparação com o Playwright, o Selenium tem um arranque mais lento, uma API ligeiramente mais detalhada e não possui interceção de pedidos integrada. Os seus pontos fortes são a maturidade do ecossistema, o suporte de primeira classe na maioria dos fornecedores de CI e o facto de os seus engenheiros de testes já o conhecerem. Se estiver a começar do zero, o Playwright é mais fácil. Se estiver a ampliar um conjunto de testes já existente, o Selenium serve perfeitamente. O nosso guia prático do Selenium aborda padrões de escalabilidade e técnicas para contornar o Cloudflare quando o fluxo predefinido fica bloqueado.

Um detalhe específico do Selenium que vale a pena ter em conta: opte sempre por WebDriverWait condições explícitas (presence_of_element_located, element_to_be_clickable) em vez de time.sleep. As esperas explícitas terminam no momento em que a condição é satisfeita; as pausas desperdiçam tempo em páginas rápidas e continuam a falhar nas lentas.

Escalar com o Scrapy: Spiders, Pipelines e Exportações

O Scrapy não é mais uma biblioteca de análise sintática. É uma estrutura completa de rastreamento: um motor assíncrono, uma pilha de middleware para cabeçalhos e proxies, pipelines de itens para limpeza e persistência e exportações integradas para JSON, JSONL e CSV. Recorra a ele quando a sua tarefa ultrapassar cerca de mil URLs, necessitar de novas tentativas ou tiver várias etapas de saída.

Criar a estrutura de um projeto:

scrapy startproject bookstore
cd bookstore
scrapy genspider books books.toscrape.com

Editar bookstore/spiders/books.py:

import scrapy

class BooksSpider(scrapy.Spider):
    name = "books"
    start_urls = ["https://books.toscrape.com/catalogue/page-1.html"]
    custom_settings = {
        "DOWNLOAD_DELAY": 0.5,
        "CONCURRENT_REQUESTS": 8,
        "USER_AGENT": "Mozilla/5.0 (compatible; MyScraper/1.0)",
        "RETRY_TIMES": 3,
    }

    def parse(self, response):
        for card in response.css("article.product_pod"):
            yield {
                "title": card.css("h3 a::attr(title)").get(),
                "price": card.css("p.price_color::text").get(),
                "stock": card.css("p.instock::text").re_first(r"\S.*"),
            }
        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, self.parse)

Executar e exportar:

scrapy crawl books -O books.jsonl

Esse único comando rastreia todas as páginas, segue o link para a página seguinte, aplica as suas definições de atraso e repetição e transmite os resultados em JSONL. A -O sobrescreve o ficheiro; -o acrescenta.

Há duas funcionalidades do Scrapy que vale a pena aprender desde cedo:

Os pipelines de itens processam cada item extraído através de uma cadeia de classes Python. Utilize-os para normalização, deduplicação e persistência. Um PriceCleanerPipeline pode remover símbolos monetários; um SQLitePipeline pode inserir dados numa base de dados. Ligue os pipelines em settings.py no ITEM_PIPELINES.

Os middlewares ligam-se a cada pedido ou resposta. É aqui que se enquadram a rotação de proxies, a rotação de cabeçalhos e o tratamento do Cloudflare. Middlewares da comunidade, como scrapy-rotating-proxies e scrapy-user-agents são integrados com apenas duas linhas de configuração.

Quando acederes a páginas com muito JavaScript, não abandones o Scrapy. Adiciona scrapy-playwright e marque pedidos específicos com meta={"playwright": True}. Mantém a concorrência, os pipelines e as exportações do Scrapy, pagando o custo do navegador apenas pelas páginas que o necessitam. O nosso manual do Scrapy e o guia de integração do scrapy-playwright abordam esse padrão híbrido.

Um pipeline mínimo que normaliza preços e elimina duplicados fica assim:

# bookstore/pipelines.py
import re

class BookstorePipeline:
    def __init__(self):
        self.seen = set()

    def process_item(self, item, spider):
        title = item.get("title")
        if title in self.seen:
            raise DropItem(f"duplicate: {title}")
        self.seen.add(title)
        raw_price = item.get("price") or ""
        item["price"] = float(re.sub(r"[^\d.]", "", raw_price) or 0)
        return item

Integre-o settings.py com ITEM_PIPELINES = {"bookstore.pipelines.BookstorePipeline": 300}. O número inteiro representa a prioridade; os valores mais baixos são executados primeiro. Adicione mais classes para validação, armazenamento e alertas no Slack, e toda a cadeia de pós-processamento torna-se declarativa.

Quando dispensar o Scrapy. Para uma recolha pontual de uma única página, a estrutura de projeto do Scrapy é exagerada. Para qualquer tarefa recorrente, ou qualquer coisa que envolva mais do que algumas centenas de URLs, as predefinições de concorrência e repetição compensam imediatamente o código padrão. Uma regra prática útil: se te vires a escrever o teu próprio ciclo assíncrono, conjunto de threads ou decorador de novas tentativas em torno do Requests, já reinventaste o Scrapy o suficiente para que devas simplesmente usá-lo.

Utilize uma API de extração da Web para alvos anti-bot

Alguns sites irão resistir a tudo o que leu até agora. Eles identificam o seu handshake TLS, bloqueiam IPs de centros de dados, apresentam CAPTCHAs logo no primeiro contacto ou devolvem um 200 OK com um corpo que diz «por favor, ative o JavaScript». O Playwright consegue contornar alguns destes; os proxies residenciais conseguem contornar mais. Quando a combinação continuar a falhar, uma API de scraping alojada é a solução mais pragmática.

Uma API de scraping recebe um URL e devolve HTML. Ela trata da camada de pedidos: renderização do navegador, rotação de proxies, novas tentativas e evasão de medidas anti-bot. Mantém o teu código de análise existente em Beautiful Soup ou lxml e basta substituir a chamada de recuperação. Isso significa que não precisas de gerir conjuntos de proxies, manter as impressões digitais do navegador atualizadas nem depurar por que razão o solucionador de CAPTCHA de uma determinada região está mais lento hoje.

Uma chamada típica tem este aspeto:

import requests

API_ENDPOINT = "https://api.webscrapingapi.com/v2"
params = {
    "api_key": "YOUR_KEY",
    "url": "https://example.com/hard-target",
    "render_js": "true",
    "proxy_type": "residential",
    "country": "us",
}
resp = requests.get(API_ENDPOINT, params=params, timeout=60)
resp.raise_for_status()
html = resp.text

# Parse with the same code you would use on a static page.
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "lxml")

Os parâmetros são importantes. render_js=true executa o alvo através de um navegador sem interface gráfica do lado do servidor. proxy_type=residential encaminha o tráfego através de IPs que se assemelham a tráfego doméstico. country=us geolocaliza o pedido quando o alvo apresenta conteúdos diferentes consoante a região.

Vale a pena destacar dois limites importantes. Em primeiro lugar, paga-se por pedido, pelo que os custos da API de scraping aumentam linearmente com o volume; se a sua página for estática e não bloquear nada, o Requests mais o Beautiful Soup é mais barato em ordens de magnitude. Em segundo lugar, alguns fornecedores publicam números muito elevados de conjuntos de proxies como dados de marketing; considere esses números específicos como orientativos até os confirmar na documentação atualizada do fornecedor. A decisão certa não é «API por predefinição». É «API quando a camada de obtenção de dados é que falha».

Uma regra prática útil: se passares mais de um dia por semana a depurar bloqueios em vez de erros de análise, a camada de recuperação deixou de ser um problema teu e passou a ser de outra pessoa. Transferi-la para uma API de scraping e recupera as horas de engenharia para o trabalho de extração e modelação que só a tua equipa pode fazer.

Lidar com erros HTTP, novas tentativas e tempos de espera

Um scraper sem uma política de novas tentativas é um scraper que irá falhar na sua primeira noite em produção. As respostas HTTP dizem-lhe exatamente o que fazer, se prestar atenção.

Status

Significado

Reação correta

200

Sucesso

Analisar e prosseguir

301 / 302

Redirecionar

Seguir (o Requests faz isto por predefinição)

403

Proibido

Alternar IP ou user-agent, verificar se há proteção anti-bot

404

Não encontrado

Ignorar e registar; o recurso já não existe ou o URL está incorreto

429

Demasiadas solicitações

Respeite Retry-After, afasta-te, abranda

5xx

Erro do servidor

Tentar novamente com recuo exponencial e, em seguida, desistir

O Requests plus urllib3 proporciona-lhe uma política de novas tentativas adequada em poucas linhas:

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

session = requests.Session()
retry = Retry(
    total=5,
    backoff_factor=1.5,           # 1.5s, 3s, 6s, 12s, 24s
    status_forcelist=[429, 500, 502, 503, 504],
    allowed_methods=["GET", "HEAD"],
    respect_retry_after_header=True,
)
session.mount("https://", HTTPAdapter(max_retries=retry))
session.mount("http://", HTTPAdapter(max_retries=retry))

resp = session.get(url, timeout=(5, 30))    # (connect, read)

Defina valores explícitos timeout em cada pedido. Uma tupla de (connect, read) segundos é mais seguro do que um único número e evita a falha clássica em que «um servidor de origem lento bloqueia todo o rastreio».

No Playwright, os tempos de espera são definidos por ação: page.goto(url, timeout=30_000) e page.wait_for_selector(sel, timeout=15_000). As tentativas de repetição têm de ser explícitas, porque o Playwright não repete automaticamente. Envolva as navegações no seu próprio ciclo ou utilize tenacity para um decorador declarativo:

from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=1.5, min=2, max=30))
def goto_with_retry(page, url):
    page.goto(url, wait_until="domcontentloaded", timeout=30_000)

Registe o código de estado, o URL e o número da tentativa em cada nova tentativa. As novas tentativas silenciosas são a forma como os scrapers «funcionam» durante semanas, ao mesmo tempo que devolvem dados desatualizados. Mais um hábito: limite o orçamento total de tentativas por URL, não apenas o número de tentativas. Um pedido que demora 90 segundos ao longo de quatro tentativas é pior do que um que falha rapidamente aos 15 segundos e segue em frente, porque a falha lenta atrasa todas as outras URLs que partilham esse worker.

Evite ser bloqueado: proxies, cabeçalhos e limites de taxa

A maioria dos bloqueios resume-se a três sinais: os seus cabeçalhos não se assemelham aos de um navegador, as suas solicitações são demasiado rápidas e o seu IP está numa lista de bloqueio do centro de dados. Corrija os três.

Alterne entre um conjunto de user-agents reais e envie os cabeçalhos que um navegador real enviaria. Os sites identificam-se com base Accept, Accept-Language, e Accept-Encoding também, não apenas User-Agent.

import random, time

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ...",
    # ...more real UAs
]

def browser_headers():
    return {
        "User-Agent": random.choice(USER_AGENTS),
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.9",
        "Accept-Encoding": "gzip, deflate, br",
    }

Adicione variação à cadência das suas solicitações. Uma cadência fixa time.sleep(1) é mais fácil de detetar do que um intervalo aleatório, como o de um utilizador humano.

time.sleep(random.uniform(1.2, 3.5))

Alterne entre proxies, de preferência residenciais. Os IPs de centros de dados são bloqueados em massa porque são fáceis de detetar. Os proxies residenciais encaminham o tráfego através de dispositivos reais dos utilizadores e passam despercebidos.

PROXIES = [
    "http://user:pass@proxy1.example.com:8000",
    "http://user:pass@proxy2.example.com:8000",
    # ...
]

def get(url):
    proxy = random.choice(PROXIES)
    return requests.get(
        url,
        headers=browser_headers(),
        proxies={"http": proxy, "https": proxy},
        timeout=(5, 30),
    )

Deteta rapidamente se a tentativa falhar. Uma resposta com o estado 200 e um corpo que contenha «Just a moment...», «Attention Required» ou «cf-chl-bypass» é um desafio da Cloudflare, não conteúdo real.

def looks_blocked(resp):
    body = resp.text[:5000].lower()
    return any(k in body for k in [
        "just a moment", "attention required", "cf-chl-bypass",
        "captcha", "access denied",
    ])

Quando looks_blocked ocorrer um bloqueio, alterne o proxy, reduza drasticamente a frequência das solicitações e considere passar a utilizar uma API de scraping. Os nossos guias sobre rotação de proxies e estratégias anti-bloqueio abordam em profundidade os padrões de recuperação após bloqueios de IP.

Recupere-se de bloqueios de IP sem reiniciar o processo. Mantenha os proxies ativos numa collections.deque. Quando um receber um 403 ou um CAPTCHA, alterne-o para o fim da fila e aplique um período de espera antes de o colocar novamente à frente. Se todo o conjunto estiver inativo, aguarde o bloqueio em vez de encerrar a operação:

from collections import deque
pool = deque(PROXIES)

def next_proxy():
    proxy = pool.popleft()
    pool.append(proxy)
    return proxy

Isso não é suficiente para os alvos mais difíceis, mas manterá um scraper bem comportado a funcionar na maioria dos sites durante meses sem intervenção manual. Quando deixar de ser suficiente, esse é o sinal para trocar a camada de recuperação por uma API alojada, em vez de continuar a aumentar o seu conjunto de proxies.

Armazenar dados extraídos: CSV, JSON, SQLite e formatos compatíveis com LLM

A sua escolha de armazenamento depende da forma como os dados serão utilizados. Escolha com cuidado; converter mais tarde é complicado.

CSV para folhas de cálculo e transferências rápidas:

import csv
with open("books.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.DictWriter(f, fieldnames=["title", "price", "in_stock"])
    writer.writeheader()
    writer.writerows(records)

JSON para registos aninhados e resultados no formato de API:

import json
with open("books.json", "w", encoding="utf-8") as f:
    json.dump(records, f, ensure_ascii=False, indent=2)

SQLite para execuções repetidas com deduplicação e consultas. É um único ficheiro, sem servidores, e vem integrado com o Python:

import sqlite3
conn = sqlite3.connect("books.db")
conn.execute(
    "CREATE TABLE IF NOT EXISTS books ("
    " url TEXT PRIMARY KEY,"
    " title TEXT, price REAL, scraped_at TEXT)"
)
conn.executemany(
    "INSERT OR REPLACE INTO books VALUES (?, ?, ?, ?)",
    [(r["url"], r["title"], r["price"], r["scraped_at"]) for r in records],
)
conn.commit()

Parquet para cargas de trabalho analíticas que serão transferidas para o DuckDB, Spark ou um armazém de dados: pandas.DataFrame(records).to_parquet("books.parquet"). Em formato colunar e tipado, pelo que as consultas a jusante são rápidas.

Saída JSONL pronta para LLM

Se os seus dados extraídos forem alimentar um LLM ou um pipeline de embedding, envie-os como JSONL: um objeto JSON por linha, um registo por documento. Cada registo deve conter um ID estável, o URL de origem, um carimbo de data/hora e um text campo que o modelo possa segmentar.

def to_llm_record(item):
    return {
        "id": item["url"],
        "source_url": item["url"],
        "scraped_at": item["scraped_at"],
        "title": item["title"],
        "text": f"{item['title']}\n\nPrice: {item['price']}\n\n{item.get('description', '')}",
        "metadata": {"category": item.get("category"), "in_stock": item["in_stock"]},
    }

with open("books.jsonl", "w", encoding="utf-8") as f:
    for r in records:
        f.write(json.dumps(to_llm_record(r), ensure_ascii=False) + "\n")

Mantenha os fragmentos com menos de cerca de 2000 tokens, preserve os URLs de origem para citação e nunca junte registos de fontes diferentes. São esses metadados por registo que permitem que um pipeline RAG a jusante se mantenha fiável.

Inserir resultados no pandas ou num armazém de dados. Para fluxos de trabalho de análise, carregue o seu JSONL ou SQLite diretamente num dataframe e continue na memória:

import pandas as pd
df = pd.read_json("books.jsonl", lines=True)
df["price"] = df["price"].astype(float)
df.to_parquet("books.parquet")

Para ficheiros maiores, duckdb.read_json("books.jsonl") permite-lhe utilizar SQL no mesmo ficheiro sem o carregar na memória. Ambas as abordagens mantêm a sua fase de extração completamente separada da fase de análise, que é o que pretende quando o scraper muda e a análise permanece a mesma.

Agendar e monitorizar scrapers recorrentes

Um scraper que executa manualmente é um passatempo. Um scraper que executa de forma programada é infraestrutura de dados e requer a mesma disciplina que qualquer outra tarefa.

O Cron é a forma mais rápida de agendar no Linux. Edite com crontab -e:

undefined0 6 * * * /path/to/.venv/bin/python /path/to/scraper.py >> /var/log/scraper.log 2>&1

Isso é executado diariamente às 06:00 e captura o stdout e o stderr. Se a sua tarefa demorar mais do que alguns minutos ou tiver dependências, opte por um temporizador do systemd, que lhe oferece semântica adequada de início/paragem, políticas de reinício e integração com journalctl. Para agendamento durante o processo de desenvolvimento, a schedule biblioteca é leve e de fácil leitura:

import schedule, time
schedule.every().day.at("06:00").do(run_job)
while True:
    schedule.run_pending()
    time.sleep(30)

Registe de forma estruturada. Os registos em texto simples não são pesquisáveis; os registos em JSON são encaminhados para o Loki, o Datadog ou uma tabela SQLite sem necessidade de pós-processamento:

import logging, json
logging.basicConfig(level=logging.INFO, format="%(message)s")

def log_event(event, **fields):
    logging.info(json.dumps({"event": event, **fields}))

log_event("page_scraped", url=url, status=resp.status_code, items=len(items))

Alerte sobre as métricas que importam: erros 403 repetidos, uma queda repentina na contagem de itens (um sinal comum de que o site alterou a marcação) e o tempo de execução total a exceder um limiar. Um scraper silencioso que devolve zero linhas é pior do que um que falha de forma evidente, porque pode não dar por isso durante semanas. Defina uma verificação de «número mínimo esperado de linhas» e envie um aviso a si próprio se o número ficar abaixo desse valor:

MIN_EXPECTED = 400
if len(items) < MIN_EXPECTED:
    log_event("row_count_alert", got=len(items), expected=MIN_EXPECTED)
    raise SystemExit(2)     # non-zero exit code trips your cron alerter

Para tarefas de web scraping em Python de longa duração, emita também um sinal de vida a cada N páginas, para que um monitor externo (Healthchecks.io, Uptime Kuma ou um simples sentinela cron) possa detetar um processo bloqueado em vez de um que esteja apenas lento. O silêncio é o modo de falha contra o qual tem de projetar.

Depurar scrapers avariados: uma lista de verificação repetível

Os scrapers avariam-se por um número reduzido de razões. Siga esta lista de verificação por ordem; a solução está quase sempre nos três primeiros passos.

  1. Verifique o comprimento do HTML bruto. print(len(resp.text)). Um corpo com menos de alguns kilobytes significa normalmente uma página de desafio, um limite de taxa ou um shell vazio antes da execução do JavaScript. Se o comprimento diminuiu em comparação com uma linha de base funcional, o problema está na camada de recuperação, não no analisador.
  2. Guarde a página renderizada no disco e abra-a num navegador. Path("debug.html").write_bytes(resp.content). Em nove de cada dez casos, isto revela instantaneamente se obteve conteúdo real, uma página de login ou a mensagem «por favor, ative o JavaScript».
  3. Compare os seletores com um instantâneo que se sabe estar correto. Mantenha um pequeno tests/fixtures/ com amostras reais de páginas da altura em que o scraper funcionava. Quando algo falhar, diff compare o HTML atual com o modelo de referência e procure por classes renomeadas, novos wrappers ou elementos removidos.
  4. Verifique se há carregamento diferido. Se o número de itens diminuiu, mas a página parece normal no navegador, o site provavelmente mudou para listas virtualizadas ou deslocamento infinito. Reabra o separador «Rede» e procure um ponto de extremidade JSON que o front-end agora chama após a montagem.
  5. Procure uma API JSON oculta. Com o tempo, os front-ends são refatorados para utilizar APIs. O que antes exigia o Playwright torna-se frequentemente uma única fetch() chamada para /api/products?page=2. Esse ponto de extremidade é estável, rápido e, quase sempre, a resposta certa, uma vez que exista.
  6. Verifique a semântica HTTP. Está a receber 200 mas com um JSON vazio? Uma 403 de forma intermitente? Cookies errados? Adicione registos relativos aos códigos de estado, cabeçalhos de resposta e cookies antes de assumir que a culpa é do analisador.

Limites legais e éticos para o scraping em Python

O scraping na Web é legal em muitas jurisdições quando aplicado a dados públicos, mas «legal» não é o mesmo que «responsável». Certifique-se de que cumpre ambos.

Respeite robots.txt. O Protocolo de Exclusão de Robôs é agora uma norma formal da Internet publicada como IETF RFC 9309 e, embora não seja, por si só, uma lei, muitos documentos de termos de serviço incorporam-no por referência. Analise-o com a função incorporada do Python urllib.robotparser e ignore os caminhos não permitidos:

from urllib.robotparser import RobotFileParser
rp = RobotFileParser()
rp.set_url("https://example.com/robots.txt")
rp.read()
if not rp.can_fetch("MyScraper/1.0", target_url):
    return

Leia os termos de serviço do alvo. Alguns sites proíbem explicitamente a recolha automatizada; ignorar essa disposição pode dar origem a uma queixa por quebra de contrato, mesmo que nenhuma lei tenha sido violada. Preste especial atenção aos sites que exigem início de sessão, uma vez que ultrapassar uma barreira de autenticação altera substancialmente o panorama jurídico.

Não recolha dados pessoais de forma descuidada. Se os dados incluírem qualquer informação que identifique uma pessoa (nome, e-mail, morada, IP, cookies associados a um utilizador), é provável que se encontre no âmbito de aplicação de leis como o RGPD, a Lei de Proteção de Dados do Reino Unido ou a CCPA. Consulte um advogado antes de criar um pipeline que armazene esses dados.

Dê preferência às APIs oficiais quando estas existirem, não contorne barreiras de acesso pago nem a autenticação, e imponha limites à sua própria taxa de acesso, mesmo quando o alvo não o obrigar a fazê-lo. Um scraper lento e «educado», que se expande de forma sustentável, vale mais do que um rápido que faz com que toda a sua empresa seja banida por IP. O nosso quadro de conformidade legal aborda em pormenor o panorama país a país.

Por fim, defina um User-Agent que identifique o seu projeto e forneça um endereço de contacto (MyProject/1.0 (+https://example.com/contact)). É uma pequena cortesia que dá aos proprietários dos sites alguém a quem enviar um e-mail antes de recorrerem ao seu WAF e, na prática, reduz significativamente as probabilidades de um bloqueio de IP.

Três projetos iniciais para praticar

A melhor forma de consolidar uma pilha de web scraping em Python é construir o mesmo scraper principal para três tipos diferentes de alvo. Cada um destes é suficientemente pequeno para ser concluído numa tarde e suficientemente realista para lhe ensinar os padrões que irá utilizar em grande escala.

Rastreador de preços. Escolha uma página de comércio eletrónico de demonstração (books.toscrape.com é segura e estável). Extraia o nome do produto, o preço e o stock para o SQLite uma vez por dia, guarde todos os instantâneos e trace uma série de preços por produto com o pandas. Isto ensina execuções idempotentes, deduplicação por URL e armazenamento de séries temporais. No momento em que o site de teste adicionar um campo «desconto», poderá também praticar a adaptação ao esquema de dados. O nosso guia de dados de produtos apresenta padrões relacionados para alvos reais de comércio eletrónico.

Agregador de ofertas de emprego. Recolha fichas de emprego de um portal público de emprego (ou de uma demonstração como realpython.github.io/fake-jobs/). Normalize títulos, localizações e datas de publicação e, em seguida, elimine duplicados entre as páginas. Este projeto valoriza uma normalização rigorosa e ensina-lhe por que razão a extração e a limpeza têm de ser etapas separadas. O nosso guia de dados de empregos aborda padrões de escalabilidade quando se passa de um portal para cinco.

Painel de manchetes de notícias. Recolha manchetes e carimbos de data/hora de publicação de um feed RSS público ou de um agregador de notícias. Armazene em formato JSONL com title, url, published_at, source. Este projeto prepara-o para trabalhos posteriores com LLM: o formato JSONL integra-se diretamente num pipeline de embedding, e os metadados por registo (fonte, data e hora) suportam RAG com muitas citações. O nosso guia de dados de notícias aborda a monitorização de alterações com base no mesmo scraper.

O mesmo pipeline de três etapas (recuperação, análise, extração), três formatos de dados diferentes. É assim que se interioriza o padrão.

Lista de verificação de produção antes de colocar um scraper em produção

Antes de promover o seu scraper de um script local para uma tarefa de produção, verifique esta lista. Itens em falta são a razão pela qual «ontem funcionou» se transforma numa chamada às 3 da manhã.

Pontos-chave

  • Comece com a ferramenta mais leve. O Requests, juntamente com o Beautiful Soup, lida com a maioria das páginas estáticas; recorra ao Playwright apenas quando o JavaScript renderizar os dados e ao Scrapy apenas quando precisar de escalabilidade, pipelines e tentativas de recuperação.
  • Inspecione o alvo no DevTools antes de escrever código. Os endpoints JSON ocultos são quase sempre mais fiáveis do que o scraping de HTML.
  • Separe a extração da normalização. Armazene valores brutos, limpe-os numa segunda passagem e elimine duplicados com base numa chave estável.
  • O bloqueio é um problema da camada de recuperação. Alterne os user agents, adicione jitter, utilize proxies residenciais e detete páginas de desafio explicitamente.
  • Lance os scrapers como se fossem tarefas reais: registos estruturados, novas tentativas, tempos de espera, alertas de desvio de esquema e uma lista de verificação de produção. A falha silenciosa é o modo de falha contra o qual tem de projetar.

Perguntas frequentes

Em que difere o web scraping da utilização de uma API oficial?

Uma API oficial é um contrato suportado e versionado, com limites de taxa, autenticação e nomes de campos estáveis. O scraping lê uma página concebida para humanos e infere a estrutura a partir de HTML que pode mudar sem aviso prévio. As APIs são mais rápidas, mais baratas e juridicamente mais seguras quando existem. Recorra ao scraping apenas quando nenhuma API cobrir os seus dados ou quando os termos da API excluírem o seu caso de utilização.

Quantas solicitações por segundo são seguras ao fazer scraping num site público?

Não existe um número universal, mas um ponto de partida defensável é 1 pedido a cada 2 a 5 segundos por domínio, com intervalos variáveis. Se o site publicar um limite de taxa na robots.txt ou na documentação, respeite-o. Esteja atento a respostas HTTP 429 e reduza a frequência exponencialmente. Um rastreamento mais rápido só é possível com a autorização explícita do site ou através de um serviço alojado que já negocie a carga com o destino.

Como posso manter o meu scraper em Python a funcionar quando o site de destino altera o seu HTML?

Teste os seletores contra um modelo HTML fixo na CI, monitore as taxas de preenchimento por campo em cada execução e emita um alerta quando estas diminuírem. Dê preferência a seletores baseados em atributos semânticos (data-testid, itemprop) em vez de nomes de classes gerados automaticamente. Mantenha a extração e a normalização separadas, para que um seletor avariado falhe num único campo, e não em todo o trabalho. Controle as versões da sua lógica de análise para que a reversão seja feita com um único commit.

Quando devo mudar de um scraper Python auto-hospedado para uma API de scraping gerida?

Mude quando for a camada de recuperação, e não o analisador, a falhar. Os sinais incluem erros 403 repetidos apesar da rotação de proxies, CAPTCHAs no primeiro contacto, bloqueios por impressão digital TLS e tempo de manutenção na infraestrutura a exceder o tempo dedicado à lógica de dados. Mantenha o seu código de análise existente; apenas a camada de pedidos precisa de ser substituída. Se o alvo for estático e desprotegido, a auto-hospedagem continua a ser significativamente mais económica.

Como devo armazenar os dados extraídos se pretender introduzi-los num LLM mais tarde?

Utilize JSONL com um registo por linha. Cada registo necessita de um id, um source_url, um scraped_at carimbo de data/hora, um text para incorporação e um metadata objeto para filtros. Mantenha os text com cerca de 2000 tokens, para que os fragmentadores a jusante não tenham de os dividir arbitrariamente. Nunca junte registos de fontes diferentes e preserve os URLs para que um pipeline RAG os possa citar.

Conclusão

O web scraping em Python em 2026 tem menos a ver com aprender mais uma biblioteca e mais com escolher a mais adequada para cada alvo, para depois a integrar na infraestrutura que a mantém em funcionamento. Uma primeira abordagem com o Requests e o Beautiful Soup cobre a maioria das páginas estáticas. O Playwright lida com JavaScript. O Scrapy permite-lhe escalar para milhares de URLs. XPath, rotação de proxies, registo estruturado e alertas de alterações no esquema transformam um script numa tarefa que pode realmente deixar a funcionar sozinha. E quando a camada de recuperação de um alvo é genuinamente hostil, uma API de scraping é a válvula de escape pragmática, em vez de ser a opção por defeito.

O hábito que mais importa é começar por pequenas coisas. Não inicie um navegador sem interface gráfica para extrair uma página que poderia analisar com lxml. Não crie um conjunto de proxies antes de ter escrito um scraper funcional. Lance primeiro a versão simples, adicione camadas apenas quando a atual ficar sem espaço e implemente tudo de forma a perceber quando o site mudar antes que o seu painel de controlo o faça.

Quando se deparar com um alvo que identifica navegadores, bloqueia IPs de centros de dados ou apresenta CAPTCHAs logo no primeiro contacto, a nossa equipa na WebScrapingAPI gere a camada de pedidos (rotação de proxies, renderização de JS, novas tentativas) para que possa manter o seu código de análise em Python e evitar a corrida ao armamento anti-bot. Experimente-o na sua URL mais complexa e veja quanto do pipeline continua a ser seu.

Sobre o autor

Raluca Penciuc, Desenvolvedor Full-Stack @ WebScrapingAPI

Raluca Penciuc

Desenvolvedor Full-Stack

Raluca Penciuc é programadora Full Stack na WebScrapingAPI, onde desenvolve scrapers, aperfeiçoa estratégias de evasão e procura formas fiáveis de reduzir a deteção nos sites-alvo.

Web Scraping com AWS Lambda: Guia para Python e Java 2026
Guias

Web Scraping com AWS Lambda: Guia para Python e Java 2026

Resumo: A extração de dados da Web com o AWS Lambda funciona melhor quando cada invocação é curta, delimitada e pode ser repetida de forma independente. Comece com HTTP direto, AWS SAM e S3 e, só depois, adicione SQS, contentores, renderização no navegador, proxies ou uma camada de recuperação gerida, apenas quando a carga de trabalho demonstrar que precisa deles.

Suciu Dan32 min read
Ler artigo
Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados
Guias

Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados

Resumo: O GoSpider é um rastreador de linha de comandos destinado a descobrir URLs, não um extrator completo de dados estruturados. Este guia sobre como utilizar o GoSpider mostra como realizar um rastreio limitado, gerir resultados de forma organizada, fazer a transferência de dados do Colly para CSV e seguir um percurso de diagnóstico para respostas 403 ou páginas que exijam renderização em JavaScript.

Suciu Dan23 min read
Ler artigo
Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade
Guias

Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade

TL;DR: A Redfin expõe pontos de extremidade de API ocultos que retornam JSON estruturado para listagens de propriedades, tornando possível ignorar totalmente a análise HTML frágil. Este guia orienta-o na construção de um scraper Python que extrai dados de aluguer e venda, pesquisa por localização, monitoriza novas listagens através de sitemaps XML e exporta resultados limpos para CSV ou JSON.

Suciu Dan14 min read
Ler artigo

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.