Scraping por linguagem 8 min de leitura

Web scraping em Python com Scrapy

Introdução ao framework Scrapy: spiders, pipelines, middlewares e configurações que transformam um script avulso em um scraper de nível industrial.

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

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

  1. Arquitetura do Scrapy
  2. Como a página é baixada: Spider e Request
  3. Parsing do conteúdo: seletores
  4. Acentos e codificações no Scrapy
  5. Concorrência e velocidade
  6. Proxies
  7. Scraping através do TOR
  8. HTTPS/SSL
  9. Trabalho com cookies
  10. Status da resposta e cabeçalhos
  11. Filas e deduplicação de URLs
  12. Item Pipeline: armazenamento dos dados
  13. 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

bash
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.

python
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:

bash
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:

python
# 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:

python
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:

python
# 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.0

AutoThrottle é 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:

python
yield scrapy.Request(url, meta={"proxy": "http://user:pass@ip:port"})

Para rotacionar um pool, um pacote pronto é mais cômodo:

bash
pip install scrapy-rotating-proxies
python
# 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:

python
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):

python
# settings.py
DOWNLOADER_CLIENT_TLS_METHOD = "TLS"
# para certificados autoassinados é possível configurar a fábrica de contexto

Na maioria dos casos, o SSL «simplesmente funciona». Os princípios gerais da segurança da conexão, no hub, seção «HTTPS/SSL».


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:

python
yield scrapy.Request(url, cookies={"sessionid": "abc123"})

Para fazer login, o FormRequest cai bem:

python
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:

python
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:

python
# 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:
bash
scrapy crawl catalog -s JOBDIR=crawls/catalog-1

Para 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:

python
# 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()
python
# 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.