🎯 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

Crawlee para Python: Fila, Dedupe e Renderizar uma Verdadeira Rastreamento

Alex Johnson
Alex Johnson

Senior Web Scraping Engineer

04-Aug-2026

TL;DR:

  • O Crawlee para Python oferece uma fila de requisições, desduplicação automática de URLs, enqueue_links() e um gravador de dataset, então um crawler paginado é uma única função manipuladora.
  • O armazenamento do Crawlee é compartilhado por processo, não por crawler. purge_on_start é True e ainda assim não isola dois crawlers em um único script.
  • Medido: dois crawlers idênticos com max_requests_per_crawl=2 em um único script. O primeiro terminou 2 requisições e escreveu 20 itens; o segundo terminou 3 requisições e reportou 50 itens em 5 páginas.
  • O BeautifulSoupCrawler não executa JavaScript. Em uma página renderizada pelo cliente, terminou a requisição e produziu 0 itens.
  • A classe base HttpClient do Crawlee possui quatro métodos. Implementando um que chama a API Universal Scraping Scrapeless retornou 10 itens daquela mesma página, com o manipulador de roteador inalterado.
  • O plano gratuito do Scrapeless cobre cada requisição neste guia.

O Crawlee para Python é a parte de um scraper que você normalmente escreveria por conta própria: a fila que contém URLs, o conjunto que impede que você busque uma duas vezes, o limitador de concorrência, e o gravador que coloca resultados no disco. Você fornece uma função manipuladora que recebe uma página analisada.

Como o Crawlee controla a fila e o armazenamento, seus padrões decidem como são os seus resultados — e dois deles produzem números errados de maneiras que nenhuma exceção lhe informará.

Este guia constrói um crawler funcional contra um site ao vivo, mede o que o padrão de armazenamento faz a um segundo crawler, e depois troca o transporte para que a mesma função manipuladora funcione em uma página que é renderizada no navegador.

O Que o Crawlee Oferece

O Crawlee fornece várias classes de crawler que compartilham uma interface. A que você escolhe decide como a página é analisada:

  • BeautifulSoupCrawler e ParselCrawler buscam via HTTP e entregam ao seu manipulador uma árvore analisada.
  • HttpCrawler oferece a resposta bruta sem análise.
  • PlaywrightCrawler e AdaptivePlaywrightCrawler controlam um navegador real.

Todos aceitam o mesmo roteador, as mesmas configurações de concorrência e o mesmo armazenamento. Trocar entre eles altera o objeto de contexto que seu manipulador recebe, razão pela qual a mudança de um crawler HTTP para um crawler de navegador não é uma alteração de uma linha só.

Instalação

bash Copy
pip install 'crawlee[beautifulsoup]'

Os extras importam — o pacote base crawlee não inclui o Beautiful Soup. A execução de verificação usou crawlee 1.9.0 com beautifulsoup4 4.15.0 no Python 3.12.

Seu Primeiro Crawler

Um crawler é uma classe mais uma função manipuladora decorada. A função manipuladora recebe um contexto que carrega a página analisada, a requisição, e os métodos para empurrar dados e enfileirar mais URLs.

python Copy
def build(*, storage_dir: str | None = None, http_client=None, max_requests: int = 3):
    crawler = BeautifulSoupCrawler(
        http_client=http_client,
        max_requests_per_crawl=max_requests,
        concurrency_settings=ConcurrencySettings(desired_concurrency=2, max_concurrency=2),
        configuration=Configuration(storage_dir=storage_dir) if storage_dir else None,
    )

    @crawler.router.default_handler
    async def handler(context: BeautifulSoupCrawlingContext) -> None:
        for quote in context.soup.select("div.quote"):
            await context.push_data({
                "text": quote.select_one("span.text").get_text(strip=True),
                "author": quote.select_one("small.author").get_text(strip=True),
                "url": context.request.url,
            })
        await context.enqueue_links(selector="li.next a")

    return crawler

context.soup é um objeto Beautiful Soup, então os seletores existentes continuam iguais. context.push_data() anexa ao dataset. context.enqueue_links(selector=...) encontra âncoras que correspondem a esse seletor, resolve cada uma em relação à página atual usando as regras de base-URL em o Padrão de URL WHATWG, e adiciona os resultados à fila — já deduplicados, então um link "next" que aponta para uma página visitada não custa nada.

ConcurrencySettings rejeita uma max_concurrency abaixo de sua desired_concurrency, então defina ambos quando você diminuí-la.

Executado contra três páginas de um site de citações ao vivo:

text Copy
  estático
    requisições finalizadas : 3
    itens do dataset        : 30
    páginas distintas       : 3
    primeira citação       : “O mundo como o criamos é um processo do nosso pensamento
    primeiro autor         : Albert Einstein

Três requisições, dez citações cada, três URLs de origem distintas. A função manipuladora nunca construiu uma URL ou rastreou um conjunto visitado.

Para Onde os Dados Vão

push_data() grava em um dataset sob ./storage, e crawler.get_data() lê os dados de volta:

python Copy
async def report(label, crawler, start_url):
    await crawler.run([start_url])
    data = await crawler.get_data()
    print(f"  {label}")
    print(f"    requisições finalizadas : {crawler.statistics.state.requests_finished}")
    print(f"    itens do dataset        : {data.count}")
python Copy
print(f"    páginas distintas : {len({i['url'] for i in data.items})}")
return data

crawler.statistics.state.requests_finished é a contagem que o Crawlee realmente completou, que vale a pena imprimir ao lado da contagem do conjunto de dados. Quando essas duas não concordam com o que você espera, a razão geralmente é a seção seguinte.

O Armazenamento Vive Mais Que Seu Crawler

Configuration().purge_on_start é True. Isso parece uma garantia de que cada execução começa a partir de um conjunto de dados vazio e uma fila vazia. Não é — a purga acontece uma vez, quando o armazenamento é aberto pela primeira vez no processo, então um segundo crawler construído no mesmo script se junta ao armazenamento que o primeiro deixou para trás.

Dois crawlers, construídos pela mesma função, ambos limitados a duas requisições, ambos começando a partir da mesma URL:

python Copy
await report("crawler A", build(max_requests=2), "https://quotes.toscrape.com/")
await report("crawler B", build(max_requests=2), "https://quotes.toscrape.com/")
text Copy
  crawler A
    requisições finalizadas : 2
    itens do conjunto de dados : 20
    páginas distintas : 2
  crawler B
    requisições finalizadas : 3
    itens do conjunto de dados : 50
    páginas distintas : 5

O Crawler B foi configurado para duas requisições e finalizou três. Seu conjunto de dados relata 50 itens em 5 páginas distintas, que incluem tudo que o Crawler A escreveu. Nada foi levantado, e ambas as execuções foram registradas como bem-sucedidas.

A URL inicial dada ao Crawler B já havia sido visitada, então a deduplicação a descartou, enquanto a página que o Crawler A havia enfileirado e nunca atingiu ainda estava aguardando. O limite e o conjunto de dados que um crawler reporta são ambas propriedades do armazenamento compartilhado, não daquele crawler.

Dê a cada crawler seu próprio diretório de armazenamento quando eles compartilham um processo. Esse é um argumento que o construtor acima aceita exatamente por essa razão:

python Copy
        configuration=Configuration(storage_dir=storage_dir) if storage_dir else None,

Re-execute as mesmas três etapas com o storage_dir definido por crawler e as contagens se tornam aquelas que você configurou. Um crawler por processo é a outra resposta, e a mais simples para produção.

https://quotes.toscrape.com/js/ constrói seu DOM a partir de um array JavaScript. BeautifulSoupCrawler o busca sem reclamações:

text Copy
  javascript
    requisições finalizadas : 1
    itens do conjunto de dados : 0
    páginas distintas : 0

Uma requisição finalizada, zero itens. A marcação que o Crawlee recebeu não contém elementos div.quote, e um crawler HTTP não tem nada que os criaria.

A resposta documentada é PlaywrightCrawler, o que significa uma dependência de navegador, um objeto de contexto diferente e uma reescrita da análise do manipulador. A mudança mais restrita é manter o BeautifulSoupCrawler e substituir apenas seu transporte, que o Crawlee suporta através do parâmetro http_client.

HttpClient possui quatro métodos, e apenas dois deles precisam de trabalho real. Um objeto de resposta que satisfaça o tipo estrutural HttpResponse do Crawlee — um protocolo no sentido da especificação do protocolo de digitação do Python — envolve o HTML renderizado. Ele precisa expor um código de status e cabeçalhos porque o Crawlee os trata da maneira que a especificação semântica HTTP os define:

python Copy
class RenderedResponse:
    """Adapta uma string HTML renderizada ao protocolo HttpResponse do Crawlee."""

    def __init__(self, body: bytes, status_code: int = 200) -> None:
        self._body = body
        self._status_code = status_code

    @property
    def http_version(self) -> str:
        return "HTTP/1.1"

    @property
    def status_code(self) -> int:
        return self._status_code

    @property
    def headers(self) -> HttpHeaders:
        return HttpHeaders({"content-type": "text/html; charset=utf-8"})

    async def read(self) -> bytes:
        return self._body

    async def read_stream(self) -> AsyncIterator[bytes]:
        raise RuntimeError("streaming não é suportado por este cliente")
        yield b""

O próprio cliente chama a API Universal de Scraping Scrapeless. Esse endpoint renderiza a página e retorna o HTML como uma string. A chamada bloqueante passa por asyncio.to_thread para que não bloqueie o loop de eventos que a documentação da tarefa asyncio descreve:

python Copy
class ScrapelessHttpClient(HttpClient):
    """Roteia cada requisição Crawlee através da API Universal de Scraping."""

    def __init__(self, *, proxy_country: str = "US") -> None:
        super().__init__()
        self._proxy_country = proxy_country
        self._token = os.environ["SCRAPELESS_API_KEY"]

    def _render(self, url: str) -> bytes:
python Copy
payload = {
            "actor": "unlocker.webunlocker",
            "input": {"url": url, "proxy_country": self._proxy_country, "js_render": True},
        }
        request = urllib.request.Request(
            UNLOCKER,
            data=json.dumps(payload).encode(),
            headers={"Content-Type": "application/json", "x-api-token": self._token},
        )
        with urllib.request.urlopen(request, timeout=180) as response:
            return json.loads(response.read().decode())["data"].encode("utf-8")

    async def crawl(self, request, *, session=None, proxy_info=None, statistics=None,
                    timeout: timedelta | None = None) -> HttpCrawlingResult:
        body = await asyncio.to_thread(self._render, request.url)
        return HttpCrawlingResult(http_response=RenderedResponse(body))

    async def send_request(self, url, *, method="GET", headers=None, payload=None,
                           session=None, proxy_info=None, timeout=None) -> HttpResponse:
        body = await asyncio.to_thread(self._render, url)
        return RenderedResponse(body)

    def stream(self, url, **kwargs):
        raise NotImplementedError("este cliente não transmite")

    async def cleanup(self) -> None:
        return None

Passe para o mesmo crawler e execute a mesma página:

text Copy
  javascript+api
    requisições concluídas : 1
    itens do conjunto de dados : 10
    páginas distintas : 1
    primeira citação : “O mundo que criamos é um processo do nosso pensamento

Dez itens da página que produziram zero. O manipulador do roteador, os seletores, a chamada do conjunto de dados e enqueue_links estão todos intactos — a fila e a deduplicação do Crawlee continuam funcionando, porque apenas o objeto que busca bytes foi substituído. Mantenha a chave no ambiente como SCRAPELESS_API_KEY; o guia de início rápido da Universal Scraping API lista os outros parâmetros da requisição. Se você precisar de roteamento proxy para o cliente HTTP padrão, o guia de proxy do Crawlee cobre essa configuração.

Começar leva um minuto — crie uma conta gratuita na Scrapeless e o plano gratuito cobre tudo aqui.

Execute

bash Copy
export SCRAPELESS_API_KEY="sua-chave-api"
python3 crawlee_demo.py

A saída completa da execução de verificação:

text Copy
crawlee 1.9.0 | beautifulsoup4 4.15.0
purge_on_start default: True
--- site estático, cliente HTTP padrão, armazenamento isolado ---
  estático
    requisições concluídas : 3
    itens do conjunto de dados : 30
    páginas distintas : 3
    primeira citação : “O mundo que criamos é um processo do nosso pensamento
    primeiro autor : Albert Einstein
--- site javascript, cliente HTTP padrão, armazenamento isolado ---
  javascript
    requisições concluídas : 1
    itens do conjunto de dados : 0
    páginas distintas : 0
--- site javascript, ScrapelessHttpClient, armazenamento isolado ---
  javascript+api
    requisições concluídas : 1
    itens do conjunto de dados : 10
    páginas distintas : 1
    primeira citação : “O mundo que criamos é um processo do nosso pensamento
--- dois crawlers, um processo, armazenamento padrão ---
  crawler A
    requisições concluídas : 2
    itens do conjunto de dados : 20
    páginas distintas : 2
  crawler B
    requisições concluídas : 3
    itens do conjunto de dados : 50
    páginas distintas : 5

Solução de Problemas

O conjunto de dados tem mais itens do que esta execução produziu. O armazenamento é compartilhado por processo. Defina Configuration(storage_dir=...) por crawler, ou exclua ./storage entre as execuções, ou execute um crawler por processo.

desired_concurrency não pode ser maior que max_concurrency. ConcurrencySettings valida o par na construção. Reduzir max_concurrency sozinho aumenta; defina desired_concurrency para corresponder.

ModuleNotFoundError: Nenhum módulo chamado 'bs4'. O pacote base não possui parser. Instale crawlee[beautifulsoup] ou crawlee[parsel].

ImportError em HttpHeaders. Ele é exportado do pacote de nível superior crawlee, não de um submódulo.

Zero itens e uma requisição concluída. A página é renderizada do lado do cliente. Imprima await context.http_response.read() e procure um valor que você possa ver na página; se o valor estiver ausente, nenhum seletor o encontrará.

Conclusão

O valor do Crawlee é a maquinaria em torno do seu manipulador: uma fila, deduplicação, concorrência limitada e um conjunto de dados. Essa maquinaria também é a coisa a se observar, porque ela mantém o estado que sobrevive ao objeto do crawler. As duas medições neste guia provêm desse fato — um segundo crawler em um processo relatando 50 itens quando buscou muito menos e uma página renderizada pelo cliente retornando um zero limpo.
Ambos são diagnosticáveis em uma linha. Imprima requests_finished junto com a contagem do conjunto de dados em cada execução; quando eles discordarem da sua configuração, observe o armazenamento antes dos seletores. E quando a contagem for zero porque a marcação chegou vazia, a menor correção é mudar o transporte e deixar o manipulador em paz.

Pronto para tentar? Comece com o plano gratuito da Scrapeless e veja o preço atual para volumes maiores.

FAQ

Q: Qual classe de crawler do Crawlee devo começar?

Comece com o BeautifulSoupCrawler se os dados estiverem no HTML servido, porque isso custa uma requisição HTTP por página e fornece uma árvore analisada familiar. Mude para ParselCrawler se preferir XPath, HttpCrawler se quiser os bytes brutos, e um crawler do Playwright somente quando a página realmente precisar de um navegador.

Q: Como o Crawlee é diferente de escrever o loop eu mesmo?

O Crawlee fornece a fila de requisições, deduplicação de URLs, concorrência limitada e persistência do conjunto de dados. Na execução acima, enqueue_links(selector="li.next a") percorreu três páginas sem que o manipulador construísse uma única URL ou rastreasse quais páginas havia visto.

Q: Por que meu conjunto de dados contém resultados de uma execução anterior?

Porque o armazenamento do Crawlee é compartilhado por processo e purge_on_start é acionado uma vez quando o armazenamento é aberto pela primeira vez, não por crawler. Dois crawlers em um script compartilham um conjunto de dados e uma fila de requisições. Dê a cada um um Configuration(storage_dir=...), ou execute um crawler por processo.

Q: Preciso mudar para PlaywrightCrawler para páginas JavaScript?

Não. O PlaywrightCrawler é uma opção, mas muda a classe do crawler e o contexto que seu manipulador recebe. Implementar a interface HttpClient do Crawlee muda apenas como os bytes são recuperados, que é por isso que o manipulador neste guia passou de 0 itens para 10 sem uma edição.

Q: Onde o Crawlee escreve sua saída?

Por padrão, em ./storage, com conjuntos de dados em storage/datasets/. crawler.get_data() lê o conjunto de dados de volta no mesmo processo, e Configuration(storage_dir=...) move toda a árvore para outro lugar.

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