Testei o Crawl4AI e descobri vários problemas

Testei o Crawl4AI e descobri vários problemas

O Crawl4AI é uma biblioteca de web scraping que facilita a extração de dados da web de maneira eficiente e estruturada. Ele foi projetado para ser uma alternativa moderna a outras ferramentas populares, como Scrapy, BeautifulSoup e Selenium, oferecendo diversas funcionalidades, como:

Facilidade de uso – Fornece uma API simples para configurar e rodar scrapers sem complicações.
Suporte a JavaScript – Pode renderizar páginas dinâmicas, algo essencial para sites que carregam conteúdo via AJAX.
Mecanismos anti-bloqueio – Suporta proxies, user-agents rotativos e delays automáticos para evitar detecção e bloqueios.
Uso de LLM - Permite o uso de LLM para processar os dados coletados.

Mas será que o Crawl4AI entrega tudo isso na prática?

Para responder a essa pergunta, fiz um teste: criei um scraper para coletar informações de um Pokémon em uma Pokédex online. Vamos conferir os resultados!

Scraper de teste

O desafio era construir um scraper simples para extrair informações detalhadas de um Pokémon diretamente da página da Pokédex. Os dados que tentei coletar foram:

  • id → ID do pokémon na pokédex

  • name → Nome do pokémon

  • height → Altura

  • weight → Peso

  • category → Categoria do pokémon

  • abillities → Habilidades do pokémon

  • types → Tipos do pokémon

  • weakness → Fraquezas do pokémon

  • image_src → URL da imagem do pokémon

O código completo está disponível no repositório: https://github.com/subipranuvem/crawl4ai-test.

No próximo tópico, vou detalhar os pontos positivos e negativos que encontrei ao usar o Crawl4AI nesse teste.

Pontos Positivos

Instalação

A instalação é realmente fácil de ser feita, sem nenhum problema, basta seguir a documentação:

pip install crawl4ai && \
crawl4ai-setup && \
crawl4ai-doctor

É bem simples, mas lembre-se de executar esses passos caso você tenha que montar uma imagem Docker com o Crawl4AI.

Resultado em Markdown

Se você precisa de um resultado estruturado em Markdown, então essa biblioteca vai fazer mágica pra você!

Esse simples pedaço de código já consegue emitir um markdown com todas as informações que seriam necessárias para se obter do pokémon:

import asyncio

from crawl4ai import AsyncWebCrawler, CacheMode, CrawlerRunConfig


async def main():
    async with AsyncWebCrawler(verbose=True) as crawler:
        result = await crawler.arun(
            url="https://sg.portal-pokemon.com/play/pokedex/0981",
            cache_mode=CacheMode.DISABLED,
            config=CrawlerRunConfig(
                cache_mode=CacheMode.DISABLED,
                simulate_user=True,
                magic=True,
            ),
        )

        if result.markdown_v2:
            print(result.markdown_v2)


if __name__ == "__main__":
    asyncio.run(main())

Resultado:

...
[ ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/arrow_left_btn.png) ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/0980>) 0980 Clodsire
0981 <---------------- id
Farigiraf <---------------- name
[ ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/arrow_right_btn.png) ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/0982>) Dudunsparce 0982
![](https://sg.portal-pokemon.com/play/resources/pokedex/img/pokemon_bg.png) ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/pokemon_circle_bg.png) ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/pm/566c8ecfa9f9fddf539ca05a7ae8c86ac3465f5b.png)
Type
[ Normal ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/normal#result>) <---------------- type
[ Psychic ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/psychic#result>) <---------------- type
Weakness
[ Bug ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/bug#result>) <---------------- weakness
[ Dark ](https://sg.portal-pokemon.com/play/pokedex/</play/pokedex/dark#result>) <---------------- weakness
Height 3.2 m <---------------- height
Category Long Neck Pokémon <---------------- category
Weight 160.0 kg <---------------- weight
Gender ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/icon_male.png) / ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/icon_female.png)
Ability Cud Chew ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/icon_question.png) Armor Tail ![](https://sg.portal-pokemon.com/play/resources/pokedex/img/icon_question.png) <---------------- abillities
Versions
...

Mas os pontos positivos, infelizmente, acabam por aqui.

Pontos negativos

Durante o desenvolvimento deste projeto, um dos maiores desafios foi lidar com a documentação do Crawl4AI. Muitos links estavam quebrados e vários exemplos simplesmente não funcionavam, o que tornou o aprendizado e a implementação mais demorados do que o esperado.

A seguir, destaco os principais problemas que encontrei ao usar essa biblioteca.

Configuração do Cache

Um problema inesperado surgiu ao rodar o scraper: mesmo após o término do programa, os resultados continuavam os mesmos em execuções subsequentes. Isso aconteceu devido à configuração padrão do cache, que manteve os dados armazenados.

Para resolver isso, foi necessário desativar o cache explicitamente, adicionando a propriedade cache_mode=CacheMode.DISABLED na configuração do crawler:

result = await crawler.arun(
    url="https://sg.portal-pokemon.com/play/pokedex/0981",
    cache_mode=CacheMode.DISABLED, # <---------- HERE
    config=CrawlerRunConfig(
        cache_mode=CacheMode.DISABLED,
        ...),
    ...,
)

A documentação do Crawl4AI sugere o uso de CacheMode.BYPASS, que também evita esse problema.

Aqui, admito que foi um mix de falta de atenção e desconhecimento sobre o funcionamento do cache da biblioteca. Não imaginei que, mesmo fechando e reabrindo o terminal, os dados antigos ainda seriam retornados. Provavelmente, isso acontece devido ao navegador utilizado pelo crawler.

Essa peculiaridade só ficou evidente quando removi a propriedade abillities_info do schema, mas ela continuava aparecendo nos dados extraídos, indicando que a resposta estava sendo reaproveitada do cache.

import asyncio
import json

from crawl4ai import AsyncWebCrawler, CrawlerRunConfig, JsonXPathExtractionStrategy


async def main():
    async with AsyncWebCrawler() as crawler:
        schema = {
            "name": "Pokémon information",
            "baseSelector": "//div[@class='pokemon-detail']",
            "fields": [
                {
                    "name": "id",
                    "selector": ".//p[@class='pokemon-slider__main-no size-28']",
                    "type": "text",
                },
                ...,
                # {
                #     "name": "abillities_info",
                #     "selector": ".//span[@class='pokemon-info__value pokemon-info__value--body size-14']/span",
                #     "type": "list",
                #     "fields": [
                #         {
                #             "name": "item",
                #             "type": "text",
                #         },
                #     ],
                # },
            ],
        }

        config = CrawlerRunConfig(
            extraction_strategy=JsonXPathExtractionStrategy(schema, verbose=True),
            disable_cache=True,
        )

        result = await crawler.arun(
            url="https://sg.portal-pokemon.com/play/pokedex/0981",
            config=config,
        )

        data = json.loads(result.extracted_content)
        print(json.dumps(data, indent=2) if data else "No data found.")


if __name__ == "__main__":
    asyncio.run(main())

# Output
[INIT].... → Crawl4AI 0.4.248
[FETCH]... ↓ https://sg.portal-pokemon.com/play/pokedex/0981... | Status: True | Time: 0.01s
[COMPLETE] ● https://sg.portal-pokemon.com/play/pokedex/0981... | Status: True | Total: 0.01s
[
  {
    "id": "0981",
    "name": "Farigiraf",
    "height": "3.2 m",
    "weight": "160.0 kg",
    "category": "Long Neck Pok\u00e9mon", 
    "abillities_info": [], # <---------- This is not set in schema
    "image_src": ""
  }
]

Modelos não intuitivos

A extração de dados com XPath no Crawl4AI exige algumas "artimanhas" que complicam a criação de um modelo de dados mais limpo e intuitivo.

Um exemplo disso é a extração de listas. Em vez de permitir uma lista simples de strings, a biblioteca exige que os dados sejam estruturados como uma lista de objetos, tornando o modelo mais verboso e desnecessariamente complexo.

Veja o problema na prática:

❌ Modelo exigido pelo Crawl4AI (menos elegante)

# Schema
schema = {
    "fields": [
        {
            "name": "types",
            "selector": ".//div[contains(@class,'pokemon-type__type')]//span",
            "type": "list",
            "fields": [ # Unnecessary...
                {
                    "name": "item",
                    "type": "text",
                },
            ],
        },
    ]
}

🔍 Resultado obtido

{
    "types": [
        {
            "item": "Normal"
        },
        {
            "item": "Psychic"
        }
    ]
}

✅ Resultado esperado (mais limpo e intuitivo)

{
    "types": [
        "Normal"
        "Psychic"
    ]
}

Esse comportamento obriga o desenvolvedor (eu e você) a fazer um pós-processamento dos dados para obter um formato mais adequado, o que poderia ser evitado com uma abordagem mais flexível por parte da biblioteca.

Exemplos não consistentes

Ao longo do desenvolvimento do scraper, encontrei pequenos, mas frustrantes desafios devido a inconsistências na documentação do Crawl4AI. Alguns exemplos funcionavam perfeitamente, enquanto outros simplesmente não rodavam, sem nenhuma explicação clara sobre o motivo.

Um exemplo disso é a configuração da estratégia de extração via XPath. Dependendo de onde o parâmetro extraction_strategy é declarado, o código pode funcionar ou falhar silenciosamente.

❌ Código que NÃO funciona

result = await crawler.arun(
    url="https://sg.portal-pokemon.com/play/pokedex/0981",
    config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS),
    extraction_strategy=JsonXPathExtractionStrategy(schema, verbose=True),
)

✅ Código que funciona

result = await crawler.arun(
    url="https://sg.portal-pokemon.com/play/pokedex/0981",
    config=CrawlerRunConfig(
        cache_mode=CacheMode.BYPASS,
        extraction_strategy=JsonXPathExtractionStrategy(schema, verbose=True),
    ),
)

E não, não foi falta de atenção. A própria documentação contém exemplos contraditórios, como você pode ver na página abaixo:

Exemplo com a extraction_strategy com extração em CSS.

Exemplo com a extraction_strategy com extração em CSS, mas com a configuração dentro do CrawlerConfig

Por azar, o scraper foi configurado com o exemplo que, por algum motivo, impede a coleta correta dos dados estruturados.

Scraper obtém os itens apenas usando o HTML renderizado

Agora você deve estar se perguntando: se o scraper consegue capturar os dados diretamente do HTML renderizado, isso não deveria ser um ponto positivo?

Não necessariamente. Principalmente quando algumas informações só estão disponíveis na versão bruta da página, antes da manipulação do JavaScript.

Um exemplo disso foi quando tentei extrair as descrições das habilidades dos Pokémon. Essas informações estavam presentes no HTML bruto (raw), antes do carregamento do JavaScript. No entanto, como o parser do Crawl4AI opera sobre o HTML já renderizado, os dados já haviam sido removidos da página antes mesmo de serem processados.

A imagem abaixo mostra claramente que a informação existe no HTML raw:

Esses dados estavam dentro de elementos <transition>, que são eliminados assim que o JavaScript entra em ação. Como resultado, tornaram-se inacessíveis sem interagir diretamente com a versão renderizada da página.

Minha tentativa de contornar esse problema foi modificar o DOM via JavaScript, inserindo novos elementos <p> na esperança de torná-los visíveis novamente e, assim, conseguir extraí-los. Mas não funcionou.

O mais frustrante? Se eu tivesse usado apenas a biblioteca requests combinada com lxml, teria conseguido capturar esses dados sem nenhuma dor de cabeça.

Extração de dados usando LLM (A mágica do Crawl4AI)

A ideia era utilizar essa funcionalidade com o Google Gemini, já que é um modelo de LLM com acesso gratuito à API.
(Mais informações sobre as cotas de requisições aqui / Como obter sua chave de API aqui.)

Definição do Modelo

A primeira etapa foi definir um modelo utilizando Pydantic:

from pydantic import BaseModel

class Pokemon(BaseModel):
    id: str
    name: str
    height: str
    weight: str
    category: str
    abillities: list[str]
    types: list[str]
    weakness: list[str]
    image_src: str

O Problema com a Documentação

Ao consultar a documentação do Crawl4AI, não havia nenhuma indicação de que o Gemini era suportado.
O que foi informado, no entanto, é que o framework suporta modelos do LightLLM.

Ótimo, certo? Errado.

O link fornecido levava para uma página inexistente.

O jeito foi recorrer ao Google e encontrar a documentação do LiteLLM por conta própria. Lá, havia uma lista com os modelos suportados... mas o Gemini não estava entre eles.

Tentativa no Escuro

Sem saber se a configuração estava correta ou não, segui com a seguinte configuração:

llm_strategy = LLMExtractionStrategy(
        provider="google/gemini-2.0-flash",
        api_token="<your_token>",
        schema=Pokemon.model_json_schema(),
        extraction_type="schema",
        instruction="Extract the informations about the pokémon",
        chunk_token_threshold=1400,
        overlap_rate=0.1,
        apply_chunking=True,
        input_format="html",
        extra_args={"temperature": 0.1, "max_tokens": 1000},
        verbose=True,
    )

O resultado? Erro.

Mas, ironicamente, foi a melhor coisa que poderia ter acontecido.

A mensagem de erro trouxe um link extremamente útil: docs.litellm.ai/docs/providers, onde finalmente encontrei o suporte ao Google Gemini: docs.litellm.ai/docs/providers/gemini.

Agora me diga, não era mais fácil ter colocado isso direto na documentação? 🤔

Resultados da Coleta com LLM

A coleta foi dolorosamente lenta e ineficiente:

Tempo total: 148 segundos (2 minutos e 28 segundos) Alto consumo de tokens: Se você paga por tokens, seu bolso vai sentir. A própria documentação do Crawl4AI alerta sobre os motivos para não usar LLM para esse tipo de extração:

✅ Custo elevado

✅ Baixa velocidade

✅ Não escalável para milhares/milhões de requisições

E sabe o que é mais curioso?

Os dados obtidos via LLM foram praticamente idênticos aos extraídos pelo scraper com XPath – só que com um modelo de dados um pouco mais coerente.

[INIT].... → Crawl4AI 0.4.248
[FETCH]... ↓ https://sg.portal-pokemon.com/play/pokedex/0981... | Status: True | Time: 24.20s
[SCRAPE].. ◆ Processed https://sg.portal-pokemon.com/play/pokedex/0981... | Time: 24ms
[LOG] Call LLM for https://sg.portal-pokemon.com/play/pokedex/0981 - block index: 0
[LOG] Call LLM for https://sg.portal-pokemon.com/play/pokedex/0981 - block index: 1
[LOG] Extracted 1 blocks from URL: https://sg.portal-pokemon.com/play/pokedex/0981 block index: 0
[LOG] Extracted 1 blocks from URL: https://sg.portal-pokemon.com/play/pokedex/0981 block index: 1
[EXTRACT]. ■ Completed for https://sg.portal-pokemon.com/play/pokedex/0981... | Time: 124.10343073799959s
[COMPLETE] ● https://sg.portal-pokemon.com/play/pokedex/0981... | Status: True | Total: 148.33s

=== Token Usage Summary ===
Type                   Count
------------------------------
Completion               368
Prompt                15,632
Total                 16,000

=== Usage History ===
Request #    Completion       Prompt        Total
------------------------------------------------
1                   155          837          992
2                   213       14,795       15,008
[
  {
    "id": "N/A",
    "name": "N/A",
    "height": "N/A",
    "weight": "N/A",
    "category": "N/A",
    "abillities": [],
    "types": [],
    "weakness": [],
    "image_src": "N/A",
    "error": false
  },
  # <---- HERE, almost the same data as XPath Strategy
  {  
    "id": "0981",
    "name": "Farigiraf",
    "height": "3.2 m",
    "weight": "160.0 kg",
    "category": "Long Neck Pok\u00e9mon",
    "abillities": [
      "Cud Chew",
      "Armor Tail"
    ],
    "types": [
      "Normal",
      "Psychic"
    ],
    "weakness": [
      "Bug",
      "Dark"
    ],
    "image_src": "https://sg.portal-pokemon.com/play/resources/pokedex/img/pm/566c8ecfa9f9fddf539ca05a7ae8c86ac3465f5b.png",
    "error": false
  }
]

Minha recomendação? Evite o uso de LLM para esse tipo de tarefa.

Outros problemas

Existem ainda alguns problemas menores que me deparei executando outros testes, sendo eles:

1 - Imagem Docker desatualizada

A versão atual da imagem do Crawl4AI no Docker está desatualizada, o que pode gerar inconsistências.
A boa notícia é que, segundo o repositório oficial, uma correção já está a caminho.

2 - API REST sem suporte a XPath

A API REST não suporta a coleta de dados estruturados via XPath, sendo possível apenas pelo código. Ao tentar realizar uma consulta na API passando a estratégia json_xpath para extração, o seguinte erro foi retornado:

{
    "detail": [
        {
            "type": "enum",
            "loc": [
                "body",
                "extraction_config",
                "type"
            ],
            "msg": "Input should be 'basic', 'llm', 'cosine' or 'json_css'",
            "input": "xpath",
            "ctx": {
                "expected": "'basic', 'llm', 'cosine' or 'json_css'"
            }
        }
    ]
}

Considerações finais

Infelizmente, minha experiência com o Crawl4AI não foi das melhores. A documentação é confusa, cheia de exemplos inconsistentes e referências externas que simplesmente não funcionam.

Você lembra daquele dilema: Ler 5 minutos da documentação ou ficar no Google 5 horas pra descobrir o erro? Pois é, não se aplica aqui….

De todas as ferramentas de scraping que já testei, essa foi, de longe, a mais frustrante. Tarefas simples e triviais acabam se tornando um verdadeiro quebra-cabeça.

Mas isso significa que a biblioteca é ruim?

Não necessariamente. Para quem precisa extrair informações pontuais de uma página e processá-las com um LLM, o Crawl4AI pode ser útil. No entanto, se o objetivo for lidar com grandes volumes de dados, a estratégia de usar LLMs torna-se inviável devido ao custo e ao tempo de execução.

Além disso, a biblioteca ainda está em desenvolvimento, o que significa que você pode perder um bom tempo tentando resolver problemas que outras ferramentas, como requests e lxml, resolveriam sem complicação.

E se a ideia for usar um LLM localmente, alternativas como o Ollama podem ser ainda mais lentas, especialmente se o seu hardware não for potente o suficiente. Isso só piora a experiência geral.

No estado atual, o Crawl4AI não é uma opção confiável para projetos em produção. Métodos, APIs e funcionalidades ainda estão mudando rapidamente, o que pode gerar instabilidade.

Minha recomendação? Fique de olho nos updates e no changelog. Quem sabe, no futuro, com uma documentação mais robusta e melhorias na usabilidade, o Crawl4AI possa realmente valer a pena.