Scraping por linguagem 12 min de leitura

Web scraping com Python: guia completo

Percorremos todo o processo de web scraping com Python: requests e BeautifulSoup, conteúdo dinâmico, como evitar bloqueios e a estrutura de um scraper completo.

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

O web scraping é a coleta automática de dados de páginas web e sua conversão para um formato estruturado: tabelas, JSON ou um banco de dados. Python se tornou o padrão de fato para essa tarefa: tem sintaxe simples, um ecossistema enorme de bibliotecas e uma comunidade muito ativa.

Este material é um «hub» panorâmico. Aqui vamos percorrer todo o processo, da requisição da página ao armazenamento das filas de URLs, entregar um mapa de todas as bibliotecas e soluções-chave e, para os temas mais específicos, apontar artigos detalhados à parte.

Sumário

  1. O que é web scraping e do que ele se compõe
  2. Panorama de bibliotecas e soluções
  3. Como obtemos a página
  4. Bibliotecas para parsear o conteúdo
  5. Resolução de problemas com codificações e caracteres especiais
  6. Uso de multithreading
  7. Uso de proxies
  8. Scraping através do TOR
  9. Trabalho com HTTPS/SSL
  10. Trabalho com cookies
  11. Status da resposta e cabeçalhos
  12. Armazenamento de URLs e filas
  13. Vantagens e desvantagens da implementação
  14. Aspectos legais e éticos

1. O que é web scraping e do que ele se compõe

Qualquer scraper, seja qual for a escala, consiste em quatro etapas:

  1. Obtenção da página — requisição HTTP ao servidor e recebimento da resposta (HTML, XML, JSON).
  2. Parsing do conteúdo — extração dos dados necessários da estrutura por meio de seletores (CSS, XPath) ou expressões regulares.
  3. Normalização e armazenamento — conversão dos dados para um formato uniforme e gravação em um arquivo ou banco de dados.
  4. Gerenciamento do rastreamento — fila de URLs, deduplicação, limite de velocidade, novas tentativas.

Um script simples resolve tudo isso em 10 linhas. Um crawler industrial separa cada etapa em uma camada própria, com filas, proxies e workers distribuídos.


2. Panorama de bibliotecas e soluções

Para não se perder, vamos dividir as ferramentas de acordo com a função.

Download de páginas (clientes HTTP)

Biblioteca Tipo Quando usar
requests síncrono o padrão para a maioria das tarefas, API conveniente
urllib síncrono incluída na biblioteca padrão, sem dependências
httpx sínc./assínc. substituto moderno do requests, com suporte a async e HTTP/2
aiohttp assíncrono alta concorrência, milhares de requisições
pycurl síncrono controle fino da requisição, velocidade máxima

Parsing de HTML/XML

Biblioteca Motor Particularidades
BeautifulSoup (bs4) html.parser / lxml a API mais amigável, tolera HTML «sujo»
lxml libxml2 (C) velocidade máxima, XPath completo
parsel lxml CSS + XPath, a base do Scrapy
selectolax Modest/Lexbor (C) parser CSS muito rápido para grandes volumes
pyquery lxml sintaxe no estilo jQuery

Sites dinâmicos (JavaScript)

Ferramenta Propósito
Selenium controle de um navegador real, o clássico
Playwright alternativa moderna, mais rápida e estável
Pyppeteer port do Puppeteer para Python

Frameworks e plataformas

Solução Propósito
Scrapy framework completo para crawlers: filas, pipelines, middleware
Scrapy + Splash/Playwright Scrapy com renderização de JS
Django + Celery scraping como parte de uma aplicação web, com tarefas em segundo plano

Como escolher

  • Página simples sem JS, tarefa pontual → requests + BeautifulSoup.
  • Precisa de velocidade com grandes volumes → httpx/aiohttp + lxml/selectolax.
  • Centenas de milhares de páginas, rastreamento do site inteiro → Scrapy. Mais detalhes no artigo «Web scraping em Python com Scrapy».
  • O conteúdo é renderizado com JavaScript → Playwright/Selenium.
  • O scraping faz parte de um serviço web → Django, veja «Web scraping com Django».
  • Precisa de concorrência máxima → abordagem assíncrona.

3. Como obtemos a página

Requisição básica com requests:

python
import requests

url = "https://example.com"
headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                  "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36",
    "Accept-Language": "pt-BR,pt;q=0.9",
}

response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()   # lança uma exceção em caso de 4xx/5xx
html = response.text

Pontos-chave:

  • User-Agent: defina sempre — muitos sites bloqueiam as requisições que chegam com o python-requests padrão.
  • timeout: informe sempre; caso contrário, o script pode ficar travado para sempre.
  • raise_for_status() poupa a verificação manual do código de resposta.

Se a página é renderizada com JavaScript, o requests devolverá um esqueleto vazio. Nesse caso, é preciso um motor de navegador:

python
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=True)
    page = browser.new_page()
    page.goto("https://example.com")
    page.wait_for_selector(".content")   # esperamos os dados aparecerem
    html = page.content()
    browser.close()

Se o que se espera na saída não é HTML, e sim dados estruturados de uma API, consulte o material à parte «Parsing de JSON».


4. Bibliotecas para parsear o conteúdo

BeautifulSoup — a porta de entrada para iniciantes

python
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "lxml")   # o parser lxml é mais rápido que o html.parser

title = soup.find("h1").get_text(strip=True)
links = [a["href"] for a in soup.select("a.product-link")]
price = soup.select_one(".price").text

O BeautifulSoup tolera estrutura quebrada e se lê quase como linguagem natural. Para a extração de tabelas há uma análise à parte: «Extrair tabelas HTML com Python e BeautifulSoup».

lxml — velocidade e XPath

python
from lxml import html as lxml_html

tree = lxml_html.fromstring(html)
titles = tree.xpath('//h2[@class="title"]/text()')
prices = tree.xpath('//span[@class="price"]/text()')

O lxml é escrito em C e trabalha várias vezes mais rápido com grandes volumes. Seu XPath completo traz uma flexibilidade que os seletores CSS não alcançam. A análise em profundidade está no artigo «Web scraping em Python com lxml».

parsel e selectolax

O parsel (o núcleo do Scrapy) combina CSS e XPath:

python
from parsel import Selector

sel = Selector(text=html)
sel.css("h1::text").get()
sel.xpath("//a/@href").getall()

O selectolax é a opção certa quando é preciso parsear centenas de milhares de documentos: é visivelmente mais rápido até que o lxml nas seleções CSS.

Expressões regulares

O re só é apropriado para padrões simples e planos (telefone, e-mail, código de produto). Não convém parsear HTML aninhado com regex: a estrutura quebra com facilidade demais.


5. Resolução de problemas com codificações e caracteres especiais

O incômodo mais frequente são os caracteres corrompidos (mojibake) no lugar dos acentos e cedilhas: informação em vez de informação, São Paulo em vez de São Paulo. A causa é uma codificação da resposta detectada de forma errada.

Por que acontece

O requests tenta adivinhar a codificação a partir do cabeçalho Content-Type. Se o servidor não o envia, ou envia errado, o texto é decodificado com a codificação equivocada (muitas vezes assume-se ISO-8859-1 no lugar de windows-1252 ou utf-8).

Solução 1: fixar a codificação manualmente

python
response = requests.get(url)
response.encoding = "utf-8"      # ou "windows-1252" em sites antigos
html = response.text

Solução 2: detecção automática

python
response = requests.get(url)
response.encoding = response.apparent_encoding   # detectada a partir do conteúdo
html = response.text

O apparent_encoding usa a biblioteca charset-normalizer (ou chardet), que analisa os bytes e deduz a codificação.

Solução 3: trabalhar diretamente com os bytes

O caminho mais confiável é entregar os bytes ao parser e deixar que ele mesmo leia o <meta charset>:

python
from bs4 import BeautifulSoup

response = requests.get(url)
soup = BeautifulSoup(response.content, "lxml")   # .content, não .text

response.content são os bytes «crus»; lxml e BeautifulSoup encontram sozinhos a declaração de codificação dentro do HTML.

Solução 4: decodificação manual

python
html = response.content.decode("windows-1252", errors="replace")

O parâmetro errors="replace" substitui os caracteres não conversíveis por sem derrubar o script. As nuances do XML com caracteres especiais estão descritas em «Parsing de XML em Python».


6. Uso de multithreading

O scraping consiste sobretudo em esperar a rede (I/O-bound); por isso, as threads trazem uma aceleração notável apesar do GIL: enquanto uma thread espera a resposta, outra trabalha.

ThreadPoolExecutor — o caminho mais simples

python
from concurrent.futures import ThreadPoolExecutor
import requests

urls = [f"https://example.com/page/{i}" for i in range(1, 101)]

def fetch(url):
    r = requests.get(url, timeout=10)
    return url, r.status_code

with ThreadPoolExecutor(max_workers=10) as executor:
    for url, status in executor.map(fetch, urls):
        print(url, status)

Quando recorrer ao multiprocessing

Se o gargalo não é a rede, e sim o parsing pesado do HTML e seu processamento (CPU-bound), as threads vão esbarrar no GIL. Aí ajuda o multiprocessing: vários processos, cada um com seu próprio interpretador.

A melhor alternativa: async

Para milhares de requisições simultâneas, as threads consomem memória demais. A assincronia (asyncio + aiohttp) mantém dezenas de milhares de conexões em uma única thread. É um grande tema à parte: veja «Scraping assíncrono em Python».

Importante: concorrência alta não dá o direito de «derrubar» o servidor alheio. Limite a velocidade e respeite o robots.txt.


7. Uso de proxies

Com um scraping intensivo, o site bane seu IP pelo número de requisições. A solução: um pool de proxies com rotação.

Conexão simples de um proxy

python
proxies = {
    "http":  "http://user:pass@123.45.67.89:8080",
    "https": "http://user:pass@123.45.67.89:8080",
}
response = requests.get(url, proxies=proxies, timeout=15)

Rotação de proxies

python
import random
import requests

PROXIES = [
    "http://user:pass@ip1:port",
    "http://user:pass@ip2:port",
    "http://user:pass@ip3:port",
]

def fetch_with_rotation(url):
    proxy = random.choice(PROXIES)
    return requests.get(url, proxies={"http": proxy, "https": proxy}, timeout=15)

Tipos de proxies

  • Datacenter — baratos e rápidos, mas fáceis de detectar e banir.
  • Residenciais — IPs de provedores reais; mais caros, porém menos visíveis.
  • Móveis — IPs de operadoras de celular, os mais «confiáveis» e os mais caros.

Em produção, convém manter uma lista de proxies «saudáveis»: descartar os que devolvem timeout ou 403 e verificá-los periodicamente.


8. Scraping através do TOR

O TOR é uma forma gratuita de trocar o IP de saída. É mais lento que os proxies pagos e serve para volumes modestos, mas não exige investimento.

Conexão

Depois de instalar o TOR (o daemon ou o Tor Browser), ele levanta um proxy SOCKS5 em 127.0.0.1:9050:

python
import requests

proxies = {
    "http":  "socks5h://127.0.0.1:9050",
    "https": "socks5h://127.0.0.1:9050",
}
# é necessário o pacote: pip install requests[socks]
r = requests.get("https://httpbin.org/ip", proxies=proxies)
print(r.json())   # você verá o IP do nó de saída do TOR, não o seu

O esquema socks5h (com a letra h) é importante: a resolução DNS passa pelo TOR e não é feita localmente; caso contrário, sua requisição DNS real ficaria exposta.

Troca de identidade (novo IP)

Para obter um novo nó de saída, envie o sinal NEWNYM pela porta de controle (9051) com a biblioteca stem:

python
from stem import Signal
from stem.control import Controller

def renew_tor_ip():
    with Controller.from_port(port=9051) as controller:
        controller.authenticate(password="sua_senha")
        controller.signal(Signal.NEWNYM)

A porta de controle deve ser habilitada no torrc, definindo também o hash da senha (tor --hash-password).

Leve em conta: muitos sites grandes conhecem a lista de nós de saída do TOR e os bloqueiam ou exibem um captcha. O TOR é adequado para tarefas sem exigências de velocidade e com volumes baixos.


9. Trabalho com HTTPS/SSL

Por padrão, o requests verifica os certificados SSL por meio do pacote certifi. Na maioria das vezes, não é preciso configurar nada. Os problemas surgem em sites com certificados autoassinados ou expirados.

Desativar a verificação (somente para depurar)

python
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get(url, verify=False)   # NÃO serve para produção

Desativar a verificação é inseguro: abre a porta para um ataque MITM. Só é admissível localmente, para depurar.

O caminho correto: indicar sua própria CA

python
response = requests.get(url, verify="/path/to/custom-ca-bundle.crt")

Atualização dos certificados raiz

Se receber SSLCertVerificationError em sites normais, atualize o certifi:

bash
pip install --upgrade certifi

Os cookies são necessários para as sessões, a autenticação e para passar pelas páginas «de proteção» que colocam um token e redirecionam.

python
import requests

session = requests.Session()

# fazemos login: o servidor devolverá o cookie de sessão
session.post("https://example.com/login", data={"user": "u", "pass": "p"})

# as requisições seguintes já vão autenticadas
profile = session.get("https://example.com/profile")

A Session guarda e reenvia sozinha os cookies entre as requisições e, de quebra, reutiliza as conexões TCP (mais rápido) e os cabeçalhos comuns.

python
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)
python
response = requests.get(url)
for name, value in response.cookies.items():
    print(name, value)

11. Status da resposta e cabeçalhos

Controlar a resposta do servidor é obrigatório; caso contrário, você acabará parseando uma página de erro como se fosse «dado».

python
response = requests.get(url)

print(response.status_code)      # 200, 404, 403, 500 ...
print(response.reason)           # 'OK', 'Not Found'
print(response.headers["Content-Type"])
print(response.headers.get("Server"))
print(response.url)              # URL final após os redirecionamentos
print(response.elapsed)          # tempo de resposta

Tratamento correto dos status

python
if response.status_code == 200:
    parse(response.text)
elif response.status_code == 404:
    log("Página não encontrada")
elif response.status_code == 429:
    # Too Many Requests: estão nos freando
    wait = int(response.headers.get("Retry-After", 60))
    time.sleep(wait)
elif response.status_code in (403, 503):
    rotate_proxy()   # provavelmente um ban: trocamos de IP

O cabeçalho Retry-After indica quanto esperar antes de tentar de novo. Os status 403/503 costumam denunciar uma proteção anti-bots: ajudam a troca de proxy, outro User-Agent e uma pausa.


12. Armazenamento de URLs e filas (panorama)

Quando o scraper percorre um site inteiro, é preciso guardar em algum lugar as URLs «a visitar» e as «já visitadas». É o que se chama de frontier (fronteira de rastreamento).

A variante mais simples: estruturas em memória

python
from collections import deque

to_visit = deque(["https://example.com"])
visited = set()

while to_visit:
    url = to_visit.popleft()
    if url in visited:
        continue
    visited.add(url)
    # ... baixar, parsear, adicionar os links novos a to_visit

O set garante deduplicação instantânea; o deque funciona como fila FIFO.

Quando os dados crescem

  • Redis — fila compartilhada para vários workers, sobrevive a reinicializações. As listas e conjuntos do Redis são ideais para o rastreamento distribuído.
  • Banco de dados (PostgreSQL/SQLite) — tabela de URLs com status new / in_progress / done / failed; conveniente para resiliência e análise.
  • Filas de tarefas (Celery + broker, RabbitMQ) — quando o scraping está integrado a uma aplicação; veja «Web scraping com Django».
  • Filtro de Bloom — economiza memória com milhões de URLs: verificação probabilística de se «já vimos esta URL».

No Scrapy, o gerenciamento da fila, a deduplicação e as prioridades já vêm integrados de fábrica: é uma das principais razões para escolher esse framework nos projetos grandes. Mais detalhes em «Web scraping em Python com Scrapy».


13. Vantagens e desvantagens da implementação em Python

Vantagens:

  • Barreira de entrada baixa, código legível, protótipo rápido.
  • Um ecossistema riquíssimo: do requests ao Scrapy e ao Playwright.
  • Uma comunidade enorme: quase qualquer problema já está resolvido.
  • Integração simples com a análise de dados (pandas, numpy) e com os bancos de dados.

Desvantagens:

  • O GIL limita o processamento CPU-bound (contorna-se com async e multiprocessing).
  • O Python puro é mais lento que as linguagens compiladas no parsing pesado (salvam-no o lxml/selectolax, escritos em C).
  • Os sites dinâmicos com JS exigem motores de navegador pesados.
  • Fragilidade: quando o site muda o layout, os seletores quebram e é preciso manutenção.

14. Aspectos legais e éticos

O web scraping é uma ferramenta poderosa, e convém usá-la com responsabilidade:

  • Respeite o robots.txt e os termos de uso do site.
  • Não gere carga excessiva: adicione pausas e limite a concorrência.
  • Não colete dados pessoais sem base legal (lembre-se da LGPD).
  • Informe um User-Agent honesto quando fizer sentido e faça cache das respostas para não castigar o servidor além da conta.