🎯 Um navegador em nuvem personalizável e anti-detecção alimentado por Chromium desenvolvido internamente, projetado para rastreadores web e agentes de IA. 👉Experimente agora
De volta ao blog

Scrapy Web Scraping: Crie um Spider que Lida com Páginas JavaScript

Daniel Kim
Daniel Kim

Lead Scraping Automation Engineer

04-Aug-2026

Resumo:

  • Scrapy é um framework de rastreamento, não um cliente HTTP. Ele oferece um agendador, um filtro de duplicatas, um downloader assíncrono e um pipeline de itens, então uma aranha permanece com cerca de vinte linhas mesmo quando percorre cem páginas.
  • Uma aranha é uma classe com três partes obrigatórias: um name, uma URL inicial e um método parse que gera dicionários. Todo o resto é configuração.
  • Scrapy nunca executa JavaScript. A mesma aranha que retorna 10 itens de uma página renderizada pelo servidor retorna 0 itens de sua versão renderizada pelo cliente, porque o HTML que chega contém um contêiner vazio e uma tag de script.
  • Um middleware de downloader corrige isso sem tocar na aranha. Renderizando a página anteriormente e devolvendo ao Scrapy uma HtmlResponse comum restaurou todos os 10 itens enquanto o método parse permaneceu idêntico em bytes.
  • Fixar versões é mais importante do que o habitual aqui. A camada TLS do Scrapy está em cima do Twisted e do pyOpenSSL, e duas combinações específicas falham em todos os downloads HTTPS com erros que nomeiam certificados em vez de dependências.
  • Comece de graça. A API Universal Scraping utilizada na troca tem um nível gratuito, então você pode executar toda a comparação neste post sem um plano pago.

Scrapy separa as partes de um rastreamento que você se importa das partes que não se importa. Você escreve um seletor. Scrapy lida com a fila de requisições, a concorrência, a deduplicação, a detecção de codificação e a serialização.

Então, você aponta para uma página construída por um framework de front-end e obtém um arquivo vazio.

Este guia constrói uma aranha funcional, quebrando-a propositalmente contra uma página renderizada em JavaScript, e a conserta com um middleware de downloader — a peça do Scrapy que permite que você mude como as páginas são buscadas sem mudar como são analisadas.

O que Scrapy oferece que um Loop de Requisições não oferece

Scrapy é um motor de rastreamento com uma opinião sobre estrutura. Um loop feito manualmente sobre uma lista de URLs funciona até que você precise das coisas que o Scrapy já possui:

  • Um agendador com um filtro de duplicatas. As requisições são enfileiradas, deduplicadas por impressão digital e dispatchadas com concorrência controlada.
  • Downloads assíncronos sem sintaxe assíncrona. Scrapy roda no reator Twisted, então muitas requisições estão em voo enquanto seu método parse lê como um código síncrono comum.
  • Seletores embutidos. response.css() e response.xpath() vêm do Parsel, a mesma biblioteca de seletores coberta no guia de seletores CSS e XPath.
  • Exportação de feeds. -O results.json grava JSON, JSON Lines, CSV ou XML sem código de serialização.
  • Configurações de polidez. ROBOTSTXT_OBEY, DOWNLOAD_DELAY e AUTOTHROTTLE_ENABLED são configurações em vez de algo que você implementa. O primeiro deles lê o arquivo descrito na norma do Protocolo de Exclusão de Robôs.

O custo é que o Scrapy tem uma forma que você precisa aprender. O retorno chega por volta da terceira página de um rastreamento.

Instale o Scrapy

Instale em um ambiente virtual limpo e fixe explicitamente a pilha TLS:

bash Copy
python3 -m venv .venv
source .venv/bin/activate
pip install "scrapy==2.17.0" "twisted==26.4.0" "pyopenssl==25.3.0"

Essas três fixações são deliberadas. O suporte a HTTPS do Scrapy está em camadas sobre o Twisted, que por sua vez chama o pyOpenSSL, e os dois modos de falha documentados no final deste post vêm daquela pilha e não do próprio Scrapy.

Confirme as versões antes de escrever qualquer código de aranha:

bash Copy
python3 -c "import importlib.metadata as m; print(m.version('scrapy'), m.version('twisted'), m.version('pyopenssl'))"

Escreva sua Primeira Aranha

Uma aranha do Scrapy é uma classe com um nome, uma lista de URLs iniciais e um método parse que recebe uma resposta e gera itens. Salve isso como quotes_spider.py:

python Copy
import scrapy


class QuotesSpider(scrapy.Spider):
    name = "quotes"
    start_urls = ["https://quotes.toscrape.com/"]

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
                "tags": quote.css("div.tags a.tag::text").getall(),
            }

        next_page = response.css("li.next a::attr(href)").get()
        if next_page:
            yield response.follow(next_page, callback=self.parse)

response.css("div.quote") retorna uma lista de seletores, então o loop itera sobre elementos em vez de strings. ::text é o pseudo-elemento do Scrapy para o nó de texto, e .get() retorna a primeira correspondência enquanto .getall() retorna todas as correspondências — que é o motivo pelo qual tags é uma lista e author não é. response.follow aceita o href relativo diretamente, resolvendo-o em relação à URL atual, então não há chamada para urljoin.
O último bloco é paginação. Retornar uma solicitação de parse a coloca de volta no agendador, e apontar seu callback para parse percorre toda a lista. As formas gerais que esse padrão cobre estão expostas no guia sobre paginação em web scraping.

Execute Sem um Projeto

scrapy startproject gera um pacote com configurações, pipelines e um diretório de spiders. Você ainda não precisa de nada disso. runspider executa um único arquivo, e -s substitui qualquer configuração da linha de comando:

bash Copy
scrapy runspider quotes_spider.py -O quotes.json \
  -s LOG_LEVEL=ERROR \
  -s CLOSESPIDER_PAGECOUNT=3

-O trunca o arquivo de saída, enquanto -o anexa a ele — uma distinção que vale a pena internalizar cedo. CLOSESPIDER_PAGECOUNT=3 limita a coleta a três páginas, o que mantém a execução do tutorial educada e repetível.

Esse comando gravou 30 itens: dez citações por página em três páginas, com o primeiro registro lendo Albert Einstein e tags ['change', 'deep-thoughts', 'thinking', 'world'].

Vinte linhas de spider, três páginas coletadas, paginação seguida, JSON no disco. Esta é a parte em que o Scrapy é realmente bom.

Onde o Scrapy Para: Páginas Renderizadas em JavaScript

Aponte a spider idêntica para o gêmeo renderizado pelo cliente do mesmo site alterando uma linha:

python Copy
    start_urls = ["https://quotes.toscrape.com/js/"]

Então execute novamente:

bash Copy
scrapy runspider quotes_spider.py -O js.json -s LOG_LEVEL=ERROR -s CLOSESPIDER_PAGECOUNT=1

O resultado é um array vazio. Zero itens, nenhum erro, código de saída 0.

Nada está errado com o seletor. A página retornou HTTP 200 e o Scrapy a analisou corretamente — a marcação que recebeu simplesmente não tem elementos div.quote nela. As citações são escritas no DOM por um script após o carregamento, e a execução de scripts é um comportamento do navegador definido pelo modelo de script do HTML Standard. O Scrapy é um cliente HTTP com um analisador HTML anexado. Ele busca bytes; não executa um mecanismo JavaScript, e as próprias diretrizes do Scrapy sobre conteúdo carregado dinamicamente afirmam isso diretamente.

Fique atento a esse zero silencioso. Uma coleta de uma página JavaScript se parece idêntica, tanto na saída quanto no código de saída, a uma coleta de uma página sem nada.

As respostas habituais envolvem um navegador: scrapy-playwright controla o Chromium por solicitação, e o Splash executa um serviço de renderização junto com a coleta. Ambos funcionam, e ambos significam que cada solicitação agora carrega o custo de memória e inicialização de um navegador, além de um segundo tempo de execução a ser implantado.

Mover a renderização completamente para fora da máquina deixa o modelo de solicitação do Scrapy intocado.

Renderize a Montante com um Middleware de Downloader

Um middleware de downloader fica entre o motor do Scrapy e seu downloader, e tem exatamente o gancho que este problema precisa. O comportamento é especificado: quando process_request() retorna um objeto Request, a referência do middleware de downloader afirma que "o Scrapy parará de chamar métodos process_request() e reprogramará a solicitação retornada." Seu correspondente process_response() então retorna um Response de volta na cadeia.

Assim, o middleware pode trocar cada solicitação de saída por um POST para um ponto de extremidade de renderização, e depois desembrulhar a resposta em um HtmlResponse comum contendo a URL original. A spider nunca descobre que algo aconteceu.

Salve isso como scrapeless_middleware.py:

python Copy
import json
import os

from scrapy.http import HtmlResponse

UNLOCKER = "https://api.scrapeless.com/api/v2/unlocker/request"


class ScrapelessMiddleware:
    """Renderizar cada solicitação a montante, e então entregar ao Scrapy um HtmlResponse comum."""

    def __init__(self, token, country):
        self.token = token
        self.country = country

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            os.environ["SCRAPELESS_API_KEY"],
            crawler.settings.get("SCRAPELESS_PROXY_COUNTRY", "US"),
        )

    def process_request(self, request, spider):
        if request.meta.get("scrapeless"):
            return None
        payload = {
            "actor": "unlocker.webunlocker",
            "input": {
                "url": request.url,
                "proxy_country": self.country,
                "js_render": True,
            },
        }
        return request.replace(
            url=UNLOCKER,
            method="POST",
            body=json.dumps(payload),
            headers={"Content-Type": "application/json", "x-api-token": self.token},
            meta={**request.meta, "scrapeless": True, "origin_url": request.url},
            dont_filter=True,
        )
```python
def process_response(self, request, response, spider):
        if not request.meta.get("scrapeless"):
            return response
        rendered = json.loads(response.text)["data"]
        return HtmlResponse(
            url=request.meta["origin_url"],
            body=rendered,
            encoding="utf-8",
            request=request,
        )

A flag scrapeless em request.meta impede a recursão infinita. Sem ela, o POST reprogramado retornaria a process_request e seria embrulhado novamente. dont_filter=True é necessário porque cada requisição renderizada agora tem como alvo o mesmo URL de endpoint, e o filtro de duplicatas descartaria todos, exceto o primeiro.

origin_url é o que torna a troca invisível. A HtmlResponse é construída com o endereço real da página em vez do da API, então response.url está correto e response.follow continua a resolver links relativos contra a base correta. A requisição que chega na rede é um POST, de acordo com especificação semântica HTTP, enquanto a resposta que o aranha vê é um documento HTML comum.

Por fim, a chave da API é lida da variável de ambiente SCRAPELESS_API_KEY dentro de from_crawler, então nenhuma credencial é escrita em um arquivo de configurações. A documentação completa dos parâmetros está na referência da API de Scraping Universal, e o comportamento de renderização por trás de js_render é abordado no guia de renderização de páginas.

Conecte e Execute a Mesma Aranha Novamente

Exporte a chave e, em seguida, habilite o middleware com uma configuração. PYTHONPATH=. permite que runspider importe um módulo do diretório de trabalho:

bash Copy
export SCRAPELESS_API_KEY="sua_chave_api"

PYTHONPATH=. scrapy runspider quotes_spider.py -O js_unlocked.json \
  -s LOG_LEVEL=ERROR \
  -s CLOSESPIDER_PAGECOUNT=1 \
  -s 'DOWNLOADER_MIDDLEWARES={"scrapeless_middleware.ScrapelessMiddleware": 543}'

Essa execução produziu 10 itens, com o primeiro registro novamente lendo Albert Einstein e tags ['change', 'deep-thoughts', 'thinking', 'world'] — os mesmos registros que a página renderizada pelo servidor fornece gratuitamente.

quotes_spider.py não mudou em nenhuma linha entre essa execução e a que falhou. Os seletores, a paginação, a forma do item e a exportação do feed sobreviveram a uma mudança completa na forma como as páginas são buscadas, o que é o argumento para colocar a renderização em um middleware em vez de na aranha.

Começar não precisa de cartão — o plano gratuito cobre uma execução desse tamanho.

Prove Todos os Três Casos em Um Script

Três comandos separados são fáceis de executar de forma inconsistente. Este script executa as três varreduras em um único processo e imprime uma comparação, para que a afirmação acima possa ser verificada de uma só vez:

python Copy
import json
import os

import scrapy
from scrapy import signals
from scrapy.crawler import CrawlerProcess
from scrapy.http import HtmlResponse

UNLOCKER = "https://api.scrapeless.com/api/v2/unlocker/request"


class ScrapelessMiddleware:
    """Renderiza cada requisição upstream, depois entrega ao Scrapy uma HtmlResponse comum."""

    def __init__(self, token, country):
        self.token = token
        self.country = country

    @classmethod
    def from_crawler(cls, crawler):
        return cls(
            os.environ["SCRAPELESS_API_KEY"],
            crawler.settings.get("SCRAPELESS_PROXY_COUNTRY", "US"),
        )

    def process_request(self, request, spider):
        if request.meta.get("scrapeless"):
            return None
        payload = {
            "actor": "unlocker.webunlocker",
            "input": {"url": request.url, "proxy_country": self.country, "js_render": True},
        }
        return request.replace(
            url=UNLOCKER,
            method="POST",
            body=json.dumps(payload),
            headers={"Content-Type": "application/json", "x-api-token": self.token},
            meta={**request.meta, "scrapeless": True, "origin_url": request.url},
            dont_filter=True,
        )

    def process_response(self, request, response, spider):
        if not request.meta.get("scrapeless"):
            return response
        rendered = json.loads(response.text)["data"]
        return HtmlResponse(
            url=request.meta["origin_url"],
            body=rendered,
            encoding="utf-8",
            request=request,
        )


class QuotesSpider(scrapy.Spider):
    name = "quotes"

    def __init__(self, url, **kwargs):
        super().__init__(**kwargs)
python Copy
self.start_urls = [url]

    def parse(self, response):
        for quote in response.css("div.quote"):
            yield {
                "text": quote.css("span.text::text").get(),
                "author": quote.css("small.author::text").get(),
                "tags": quote.css("div.tags a.tag::text").getall(),
            }


class ScrapelessQuotesSpider(QuotesSpider):
    name = "quotes-scrapeless"
    custom_settings = {"DOWNLOADER_MIDDLEWARES": {ScrapelessMiddleware: 543}}


def main():
    import importlib.metadata as md

    print(
        "scrapy", md.version("scrapy"),
        "| twisted", md.version("twisted"),
        "| pyopenssl", md.version("pyopenssl"),
    )

    jobs = [
        ("página estática, Scrapy simples", QuotesSpider, "https://quotes.toscrape.com/"),
        ("página javascript, Scrapy simples", QuotesSpider, "https://quotes.toscrape.com/js/"),
        ("página javascript, middleware Scrapeless", ScrapelessQuotesSpider, "https://quotes.toscrape.com/js/"),
    ]
    collected = {label: [] for label, _, _ in jobs}

    # Scrapy mantém manipuladores de sinal de forma fraca, então mantenha uma referência forte a cada um.
    handlers = []

    def collector(label):
        def on_item(item, response, spider):
            collected[label].append(item)
        handlers.append(on_item)
        return on_item

    process = CrawlerProcess({
        "LOG_LEVEL": "ERROR",
        "SCRAPELESS_PROXY_COUNTRY": "US",
    })
    for label, spider_cls, url in jobs:
        crawler = process.create_crawler(spider_cls)
        crawler.signals.connect(collector(label), signal=signals.item_scraped)
        process.crawl(crawler, url=url)
    process.start()

    for label, _, _ in jobs:
        items = collected[label]
        print(f"{label}: {len(items)} itens")
        if items:
            print(f"  primeiro autor: {items[0]['author']}")
            print(f"  primeiras tags: {items[0]['tags']}")

    print("método parse compartilhado por ambas as aranhas:", ScrapelessQuotesSpider.parse is QuotesSpider.parse)


if __name__ == "__main__":
    main()

Executá-lo imprime:

text Copy
scrapy 2.17.0 | twisted 26.4.0 | pyopenssl 25.3.0
página estática, Scrapy simples: 10 itens
  primeiro autor: Albert Einstein
  primeiras tags: ['change', 'deep-thoughts', 'thinking', 'world']
página javascript, Scrapy simples: 0 itens
página javascript, middleware Scrapeless: 10 itens
  primeiro autor: Albert Einstein
  primeiras tags: ['change', 'deep-thoughts', 'thinking', 'world']
método parse compartilhado por ambas as aranhas: True

ScrapelessQuotesSpider é um subclasse de QuotesSpider e não adiciona nada além de custom_settings, que é a razão pela qual a linha final é True: ambas as execuções despacharam o mesmo objeto da função parse. O 0 no meio é o problema do JavaScript, e o 10 abaixo dele é a solução, medida contra o parser idêntico.

custom_settings em uma classe de aranha restringe as configurações a essa aranha, que é o que permite que um processo execute execuções com e sem o middleware. Scrapy também mantém receptores de sinal por referência fraca, então uma closure coletora criada em linha é coletada pelo garbage collector antes que a execução termine e silenciosamente não registra nada — a lista handlers existe para mantê-los.

Resolução de Problemas

Todas as requisições HTTPS falham com 'X509' object has no attribute 'get_extension'. Twisted é muito antigo para o pyOpenSSL instalado. Twisted 24.3.0, que várias distribuições Linux ainda empacotam, chama um método que versões atuais do pyOpenSSL não fornecem mais. Instalar twisted==26.4.0 resolve isso. Isso aparece com mais frequência quando o Scrapy é instalado em um Python do sistema em vez de um ambiente virtual.

Todas as requisições HTTPS falham com a verificação do certificado falhou. Scrapy 2.17.0 emparelhado com Twisted 26.4.0 e pyOpenSSL 26.3.0 falha na verificação do certificado em sites públicos normais. Fixar pyopenssl==25.3.0 resolve isso, que é por isso que a etapa de instalação nomeia todas as três versões.

O middleware nunca é executado. runspider não adiciona o diretório de trabalho ao caminho de importação, então a classe nomeada em DOWNLOADER_MIDDLEWARES não pode ser importada. Prefixe o comando com PYTHONPATH=., ou mova a aranha para um projeto criado por scrapy startproject, onde a resolução de módulo é tratada para você.

Todas as requisições depois da primeira são descartadas. O filtro de duplicatas está identificando o endpoint de renderização, que é idêntico para cada página. dont_filter=True na requisição de substituição é o que impede isso.

A aranha gera itens, mas response.follow constrói URLs erradas. A HtmlResponse foi construída com o endpoint da API como sua URL em vez do endereço original da página. Links relativos são resolvidos contra response.url, então origin_url precisa ser carregado através de request.meta.

Conclusão

A divisão de trabalho do Scrapy é o que vale a pena a curva de aprendizado. A aranha possui o que os dados significam; o downloader possui como os bytes chegam. Manter esses separados é o motivo pelo qual uma página que não retornou nada pôde ser feita para retornar dez registros sem editar um único seletor.
Construa a aranha primeiro contra o que quer que o servidor envie. Quando os seletores retornarem vazios, verifique se a marcação alguma vez os continha antes de acessar um navegador. Se não contivesse, renderizar a montante através de um middleware mantém a raspagem assíncrona, mantém a implantação em um único processo Python e deixa o código de análise que você já testou exatamente como estava.

Pronto para executar isso contra seus próprios alvos? Crie uma conta gratuita na Scrapeless, exporte sua chave e adicione o middleware em uma aranha existente. Veja a página do produto Universal Scraping API para a superfície de renderização e a página de preços para limites de planos.

FAQ

P: O Scrapy pode raspar sites renderizados em JavaScript por conta própria?

Não. O Scrapy busca HTML via HTTP e o analisa, sem um mecanismo JavaScript no pipeline. Uma página que constrói seu conteúdo do lado do cliente chega como um contêiner vazio mais uma tag de script, e os seletores não correspondem a nada. A renderização precisa acontecer em outro lugar — em um navegador anexado à raspagem ou a montante em uma API de renderização cuja saída é alimentada de volta através de um middleware de download.

P: Qual é a diferença entre um middleware de downloader e um middleware de aranha?

Um middleware de downloader está entre o motor e o downloader, portanto, vê cada solicitação antes de ser enviada e cada resposta antes de ser analisada — o lugar certo para proxies, cabeçalhos e renderização. Um middleware de aranha está entre o motor e a aranha, lidando com os itens e solicitações que seus callbacks produzem. Mudar como uma página é buscada pertence a um middleware de downloader.

P: Eu preciso de scrapy startproject, ou um único arquivo é suficiente?

Um único arquivo executado com scrapy runspider é suficiente para uma aranha, e cada comando neste guia o utiliza. Crie um projeto assim que precisar de configurações compartilhadas, pipelines de itens, várias aranhas ou implantação — o layout do projeto lhe dá um módulo de configurações e caminhos de importação que se resolvem sem PYTHONPATH.

P: Por que minha aranha retorna zero itens sem mensagem de erro?

Porque uma correspondência de seletor vazia não é um erro no Scrapy. As causas mais comuns são conteúdo injetado por JavaScript após o carregamento, um seletor escrito contra uma marcação que o inspetor de um navegador mostra mas a resposta bruta não contém, ou uma resposta que retornou uma página intersticial com HTTP 200. Imprima len(response.text) e procure no corpo por uma string que você espera antes de assumir que o seletor está errado.

P: O middleware desacelera a raspagem?

Cada solicitação se torna uma busca renderizada em vez de uma bruta, então a latência por solicitação aumenta. No entanto, o modelo de concorrência do Scrapy não muda: as solicitações ainda passam pelo mesmo agendador e pelo mesmo limite de CONCURRENT_REQUESTS, sem processo de navegador por solicitação. Ative o middleware apenas para os domínios que precisam dele e deixe o restante no downloader simples.

P: Como mantenho minha chave de API fora da base de código?

Leia-a do ambiente dentro de from_crawler, como o middleware aqui faz com os.environ["SCRAPELESS_API_KEY"]. A chave nunca aparece em um arquivo de configurações, portanto, nada sensível é comprometido e o mesmo código roda em desenvolvimento e produção com credenciais diferentes.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo