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
- O que é web scraping e do que ele se compõe
- Panorama de bibliotecas e soluções
- Como obtemos a página
- Bibliotecas para parsear o conteúdo
- Resolução de problemas com codificações e caracteres especiais
- Uso de multithreading
- Uso de proxies
- Scraping através do TOR
- Trabalho com HTTPS/SSL
- Trabalho com cookies
- Status da resposta e cabeçalhos
- Armazenamento de URLs e filas
- Vantagens e desvantagens da implementação
- 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:
- Obtenção da página — requisição HTTP ao servidor e recebimento da resposta (HTML, XML, JSON).
- Parsing do conteúdo — extração dos dados necessários da estrutura por meio de seletores (CSS, XPath) ou expressões regulares.
- Normalização e armazenamento — conversão dos dados para um formato uniforme e gravação em um arquivo ou banco de dados.
- 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:
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.textPontos-chave:
- User-Agent: defina sempre — muitos sites bloqueiam as requisições que chegam com o
python-requestspadrã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:
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
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").textO 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
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:
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
response = requests.get(url)
response.encoding = "utf-8" # ou "windows-1252" em sites antigos
html = response.textSolução 2: detecção automática
response = requests.get(url)
response.encoding = response.apparent_encoding # detectada a partir do conteúdo
html = response.textO 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>:
from bs4 import BeautifulSoup
response = requests.get(url)
soup = BeautifulSoup(response.content, "lxml") # .content, não .textresponse.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
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
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
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
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:
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 seuO 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:
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)
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
response = requests.get(url, verify=False) # NÃO serve para produçãoDesativar 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
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:
pip install --upgrade certifi
10. Trabalho com cookies
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.
Session guarda os cookies automaticamente
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.
Passar cookies manualmente
cookies = {"sessionid": "abc123", "csrftoken": "xyz789"}
response = requests.get(url, cookies=cookies)Ler os cookies recebidos
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».
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 respostaTratamento correto dos status
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 IPO 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
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_visitO 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.