Quando é preciso percorrer não uma página, mas um site inteiro — milhares e milhões de URLs —, um script caseiro logo vira um emaranhado de filas, novas tentativas e rotação de proxies. O Scrapy resolve tudo isso por você: é um framework assíncrono completo para o crawling, com filas integradas, deduplicação, pipelines de processamento e um sistema de middlewares.
Este artigo dá continuidade ao material panorâmico «Web scraping com Python». Se você precisa de uma coleta pontual de um par de páginas, requests + BeautifulSoup dão conta; o Scrapy mostra todo o seu potencial em grande escala.
Sumário
- Arquitetura do Scrapy
- Como a página é baixada: Spider e Request
- Parsing do conteúdo: seletores
- Acentos e codificações no Scrapy
- Concorrência e velocidade
- Proxies
- Scraping através do TOR
- HTTPS/SSL
- Trabalho com cookies
- Status da resposta e cabeçalhos
- Filas e deduplicação de URLs
- Item Pipeline: armazenamento dos dados
- Prós e contras
1. Arquitetura do Scrapy
O Scrapy é construído sobre um motor assíncrono (Twisted) e se compõe de peças interconectadas:
- Spider — a sua classe: de quais URLs partir e como processar as respostas.
- Scheduler — a fila de requisições, com deduplicação e prioridades.
- Downloader — baixa as páginas de forma assíncrona.
- Middlewares — interceptadores de requisições/respostas (proxies, cabeçalhos, novas tentativas).
- Item Pipeline — processamento e gravação dos dados extraídos.
Você escreve apenas o Spider e o Pipeline; de todo o resto o framework se encarrega.
Criação do projeto
pip install scrapy
scrapy startproject myparser
cd myparser
scrapy genspider example example.com
2. Como a página é baixada: Spider e Request
No Scrapy não se escreve o loop de requisições à mão: você devolve (yield) objetos Request e o motor os executa de forma assíncrona.
import scrapy
class CatalogSpider(scrapy.Spider):
name = "catalog"
start_urls = ["https://example.com/catalog"]
custom_settings = {
"USER_AGENT": "Mozilla/5.0 (compatible; MyBot/1.0)",
"DOWNLOAD_DELAY": 1.0, # pausa entre requisições
"ROBOTSTXT_OBEY": True, # respeitamos o robots.txt
}
def parse(self, response):
# extraímos as fichas de produto
for card in response.css(".product-card"):
yield {
"title": card.css(".title::text").get(),
"price": card.css(".price::text").get(),
"url": response.urljoin(card.css("a::attr(href)").get()),
}
# passamos para a página seguinte
next_page = response.css("a.next::attr(href)").get()
if next_page:
yield response.follow(next_page, callback=self.parse)response.follow completa automaticamente a URL relativa e coloca a requisição na fila. Assim a paginação fica descrita em um par de linhas. Execução:
scrapy crawl catalog -o products.json
3. Parsing do conteúdo: seletores
Por baixo, o Scrapy usa a biblioteca parsel (baseada no lxml), que aceita tanto CSS quanto XPath:
# CSS
response.css("h1::text").get()
response.css(".price::text").getall()
response.css("a::attr(href)").getall()
# XPath
response.xpath("//h1/text()").get()
response.xpath('//div[@class="price"]/text()').get()
# expressões regulares diretamente no seletor
response.css(".price::text").re_first(r"\d+")get() retorna o primeiro resultado (ou None); getall(), uma lista. É mais cômodo que o lxml «puro» graças ao tratamento seguro dos elementos ausentes. Mais detalhes sobre XPath em «Web scraping em Python com lxml».
Se na verdade a página chama uma API e retorna JSON em vez de HTML, no Scrapy isso é processado diretamente com response.json(), sem seletor nenhum. Como limpar e estruturar essas respostas é mostrado em detalhe em «Parsing de JSON».
Items e ItemLoader
Nos projetos estruturados convém descrever os dados como Item e preenchê-los por meio de ItemLoader com processadores de limpeza (corte de espaços, conversão de tipos). Para spiders simples bastam os dicionários comuns, como no exemplo acima.
4. Acentos e codificações no Scrapy
O mais habitual é o Scrapy detectar sozinho a codificação correta a partir dos cabeçalhos e do <meta charset>: os acentos e o ç «simplesmente funcionam». Se aparecerem caracteres corrompidos, é possível indicar a codificação de forma explícita ao criar a resposta ou decodificar o corpo manualmente:
def parse(self, response):
# reler o corpo forçando a codificação correta
text = response.body.decode("windows-1252", errors="replace")
sel = scrapy.Selector(text=text)A teoria geral do problema das codificações está no hub, seção «Codificações». Confira também FEED_EXPORT_ENCODING = "utf-8" na configuração, para que os caracteres acentuados não acabem virando sequências \uXXXX no JSON final.
5. Concorrência e velocidade
A grande vantagem do Scrapy é a assincronia de fábrica. Dezenas de requisições são executadas ao mesmo tempo, sem threads. Regula-se pela configuração:
# settings.py
CONCURRENT_REQUESTS = 16 # requisições simultâneas no total
CONCURRENT_REQUESTS_PER_DOMAIN = 8 # por domínio
DOWNLOAD_DELAY = 0.5 # pausa base
AUTOTHROTTLE_ENABLED = True # ajuste automático da velocidade
AUTOTHROTTLE_TARGET_CONCURRENCY = 4.0AutoThrottle é uma função especialmente inteligente: o Scrapy reduz a velocidade por conta própria se o servidor começa a responder mais devagar, equilibrando rapidez e cortesia. Isso poupa você de ajustar os atrasos manualmente. Como tudo é assíncrono, não é preciso multithread — compare com a abordagem de «Scraping assíncrono em Python», que o Scrapy implementa «debaixo do capô».
6. Proxies
A forma mais simples é indicar o proxy no meta da requisição:
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})Para rotacionar um pool, um pacote pronto é mais cômodo:
pip install scrapy-rotating-proxies# settings.py
ROTATING_PROXY_LIST = [
"ip1:port",
"ip2:port",
"ip3:port",
]
DOWNLOADER_MIDDLEWARES = {
"rotating_proxies.middlewares.RotatingProxyMiddleware": 610,
"rotating_proxies.middlewares.BanDetectionMiddleware": 620,
}O middleware rotaciona os proxies por conta própria, detecta os «banidos» e exclui os que estão mortos. A teoria geral, no hub, seção «Proxies».
7. Scraping através do TOR
O TOR se conecta como proxy SOCKS5 através do meta:
yield scrapy.Request(url, meta={"proxy": "socks5h://127.0.0.1:9050"})A troca do nó de saída via stem (sinal NEWNYM) está descrita no hub, seção «TOR». Na prática, com o Scrapy opta-se com mais frequência por proxies pagos com rotação: são mais rápidos e banidos com menos frequência que os nós de saída do TOR.
8. HTTPS/SSL
Por padrão, o Scrapy verifica os certificados. Se for preciso relaxar a checagem para um site problemático (só com pleno conhecimento de causa):
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# para certificados autoassinados é possível configurar a fábrica de contextoNa maioria dos casos, o SSL «simplesmente funciona». Os princípios gerais da segurança da conexão, no hub, seção «HTTPS/SSL».
9. Trabalho com cookies
Os cookies vêm ativados por padrão no Scrapy (COOKIES_ENABLED = True): o motor mantém a sessão entre requisições automaticamente. Para passar cookies manualmente:
yield scrapy.Request(url, cookies={"sessionid": "abc123"})Para fazer login, o FormRequest cai bem:
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={"username": "user", "password": "pass"},
callback=self.after_login,
)from_response recolhe sozinho os campos ocultos do formulário (incluindo o token CSRF), o que elimina a típica dor de cabeça da autenticação.
10. Status da resposta e cabeçalhos
O acesso ao status e aos cabeçalhos é feito através do objeto response:
def parse(self, response):
print(response.status) # 200, 404 ...
print(response.headers.get("Content-Type"))Por padrão, o Scrapy processa apenas os 2xx e deixa passar os 4xx/5xx. As novas tentativas são governadas pelo RetryMiddleware integrado:
# settings.py
RETRY_ENABLED = True
RETRY_TIMES = 3
RETRY_HTTP_CODES = [429, 500, 502, 503, 504, 403]O status 429 (Too Many Requests) o Scrapy sabe respeitar junto com o cabeçalho Retry-After. A lógica dos códigos de status em geral, no hub, seção «Status e cabeçalhos».
11. Filas e deduplicação de URLs
É aqui que o Scrapy se mostra especialmente forte: o que em um scraper caseiro precisa ser construído à mão (veja o hub, seção «Filas») aqui vem integrado:
- O Scheduler mantém a fila de requisições com prioridades.
- O Dupefilter descarta automaticamente as URLs já vistas (pela impressão digital da requisição).
- A fila pode ir para o disco (
JOBDIR), para retomar um percurso interrompido:
scrapy crawl catalog -s JOBDIR=crawls/catalog-1Para o crawling distribuído em várias máquinas existe o scrapy-redis: uma fila e um dupefilter compartilhados no Redis, de modo que vários workers podem percorrer um mesmo site em conjunto.
12. Item Pipeline: armazenamento dos dados
Os itens extraídos passam pelo pipeline, onde são validados, limpos e gravados:
# pipelines.py
import pymongo
class MongoPipeline:
def open_spider(self, spider):
self.client = pymongo.MongoClient("mongodb://localhost:27017")
self.db = self.client["scraping"]
def process_item(self, item, spider):
self.db["products"].update_one(
{"url": item["url"]}, {"$set": dict(item)}, upsert=True
)
return item
def close_spider(self, spider):
self.client.close()# settings.py
ITEM_PIPELINES = {"myparser.pipelines.MongoPipeline": 300}Para uma exportação simples não é preciso pipeline: a opção -o products.csv (ou .json, .jsonl) grava o resultado diretamente.
13. Prós e contras do Scrapy
Prós:
- Assincronia, filas, deduplicação e novas tentativas, tudo de fábrica.
- Alto desempenho com grandes volumes de páginas.
- Arquitetura limpa: Spider, Middleware e Pipeline bem separados.
- Extensões prontas para usar: rotação de proxies, scrapy-redis, auto-throttling.
- Trabalhos retomáveis e exportação cômoda para qualquer formato.
Contras:
- Curva de entrada alta: é preciso entender a arquitetura e o Twisted.
- Exagerado para um par de páginas (aí é mais simples requests + BeautifulSoup).
- Os sites com JavaScript exigem integração adicional (scrapy-playwright ou Splash).
- O modelo assíncrono do Twisted soa pouco familiar diante do asyncio moderno.
Para os sites dinâmicos adiciona-se o scrapy-playwright, que renderiza as páginas com um navegador real. E, se o conteúdo é simples e estático e já existe uma infraestrutura Django, às vezes é mais simples scrapear a partir do Django.