MARKETIA

Crawl4AI 0.9.5 testé pour de vrai : pages locales, JavaScript, extraction CSS et robots.txt (2026-10-18)

Test exécuté le 18 octobre 2026 (page rédigée le 18 octobre 2026).

Test d'usage réel mais limité : une machine, un jeu de test synthétique, aucune note ni classement. Les résultats ne se généralisent pas à d'autres données. Notre méthode.

Demander l'installation de Crawl4AI sur devisRéponse sous 5 jours ouvrés, sans engagement. Aucun paiement à cette étape.

En résumé

Le 2026-10-18, nous avons installé Crawl4AI 0.9.5 dans un environnement Python neuf, installé son navigateur, puis lancé un seul script sur un petit site de test que nous avons écrit et servi nous-mêmes sur 127.0.0.1 : aucun site tiers n'a été contacté pour le crawl. Ce test porte sur une machine, une version et trois pages minuscules : il décrit ce que nous avons observé, il ne mesure ni la robustesse ni la vitesse de Crawl4AI sur de vrais sites. Il complète l'avis MagicMate sur Crawl4AI, qui reste en grande partie une lecture des sources publiques.

Ce que nous avons observé :

  • Le paragraphe ajouté par JavaScript 500 ms après le chargement n'apparaît pas dans le Markdown avec la configuration par défaut (367 ms), et apparaît avec delay_before_return_html=1.5 (1 647 ms).
  • L'extraction par sélecteurs CSS (JsonCssExtractionStrategy) a renvoyé les 3 produits avec leur nom et leur prix, sans modèle de langage ni clé d'API.
  • Face à un robots.txt qui interdit /private/, la configuration par défaut a téléchargé et converti la page interdite (statut 200). Avec check_robots_txt=True, la page a été refusée (statut 403, message « Access denied by robots.txt ») et notre serveur n'a reçu aucune requête pour elle.
  • Installation : 134 s pour le paquet et ses dépendances, 40 s pour crawl4ai-setup (navigateur compris), 701 Mo pour l'environnement Python et 719 Mo pour les navigateurs téléchargés.

Protocole et commandes

Machine : Windows 11, Intel Core i5-10400F, Python 3.13.13, commandes lancées depuis Git Bash. Dans les commandes, $b désigne un dossier de travail neuf (sous-dossiers dl, src, venv, site, browsers, home). Les variables HOME et USERPROFILE ont été pointées vers $b/home et PLAYWRIGHT_BROWSERS_PATH vers $b/browsers, pour que la base locale, le cache et le navigateur restent dans ce dossier.

  1. Environnement isolé : python -m venv $b/venv.
  2. Version : l'API JSON de PyPI indique 0.9.5 comme version courante le 2026-10-18 (fichier crawl4ai-0.9.5-py3-none-any.whl, mis en ligne le 2026-10-14, 494 036 octets, SHA-256 publié ee2a2a2832d7bf4081289a4d2322be8111331960939e27dd0f921ed69b5e4db4).
  3. Téléchargement sans installation : $b/venv/Scripts/python.exe -m pip download crawl4ai==0.9.5 --no-deps -d $b/dl. Le SHA-256 calculé avec sha256sum est identique à celui de PyPI. Les dépendances n'ont pas été vérifiées une par une.
  4. Installation : $b/venv/Scripts/python.exe -m pip install $b/dl/crawl4ai-0.9.5-py3-none-any.whl (134 s mesurées avec time, dépendances téléchargées comprises). Les métadonnées du paquet indiquent la licence Apache-2.0. pip list affiche 99 lignes (en-têtes compris) et l'environnement pèse 701 Mo (du -sm).
  5. Navigateur : $b/venv/Scripts/crawl4ai-setup.exe (40 s). Il a téléchargé, d'abord via Playwright puis de nouveau via Patchright, Chrome for Testing 156 (version complète), Chrome Headless Shell 156.0.8078.4, FFmpeg et Winldd depuis cdn.playwright.dev, puis créé une base locale dans $b/home/.crawl4ai. Les navigateurs pèsent 719 Mo. Nous n'avons pas vérifié d'empreinte pour ce téléchargement.
  6. Site de test : trois fichiers que nous avons écrits (reproduits plus bas), servis par $b/venv/Scripts/python.exe -I $b/src/serve.py $b/site 8765 $b/server.log, qui journalise chaque requête reçue.
  7. Crawl : $b/venv/Scripts/python.exe -I $b/src/run_crawl.py http://127.0.0.1:8765 > $b/out.txt (4,9 s au total mesurées avec time, démarrage du navigateur compris). La variable CRAWL4AI_NO_CLOUD_NOTICE=1 masque seulement un bandeau d'information ; aucune clé ni compte du service en ligne n'a été utilisé.

Empreintes SHA-256 des fichiers d'entrée et des scripts :

177d8a059e84834c95835fb64ce0cd8834da82c1d94b41bae9255c16f1b84a4c  src/run_crawl.py
af06bdb1c831a61a1007ad90535bcc977b39d4d07ea273895fa589bcaea8d59d  src/serve.py
6e4f22b6b90657c936279f8d963e4db5e74d690543538b7f9fa070aa1679c3fc  site/index.html
efc4db5408347a17597d7618522f084512d5d8d9ff46135129e293e4dee3355a  site/robots.txt
3edd0d204662bbad510c57b88aad4d4ee90fc986ab0ac41881db2e2d6d419fde  site/private/secret.html

Entrée site/robots.txt exacte :

User-agent: *
Disallow: /private/

Entrée site/index.html exacte :

<!doctype html>
<html lang="fr"><head><meta charset="utf-8"><title>Boutique de test</title></head>
<body>
<nav><a href="/index.html">Accueil</a> <a href="/private/secret.html">Zone privee</a></nav>
<h1>Catalogue de demonstration</h1>
<p>Ce texte est present dans le HTML initial.</p>
<ul id="produits">
  <li class="produit"><span class="nom">Theiere en fonte</span><span class="prix">34,90 EUR</span></li>
  <li class="produit"><span class="nom">Bol en gres</span><span class="prix">12,50 EUR</span></li>
  <li class="produit"><span class="nom">Plateau en bambou</span><span class="prix">19,00 EUR</span></li>
</ul>
<div id="dyn"></div>
<script>
setTimeout(function(){ document.getElementById('dyn').innerHTML = '<p class="ajoute">Paragraphe ajoute par JavaScript apres 500 ms.</p>'; }, 500);
</script>
</body></html>

Entrée site/private/secret.html exacte :

<!doctype html><html lang="fr"><head><meta charset="utf-8"><title>Zone privee</title></head>
<body><h1>Zone privee</h1><p>Page interdite par robots.txt dans ce test.</p></body></html>

Script exact serve.py (serveur local qui journalise les requêtes) :

# Usage: python -I serve.py <site_dir> <port> <logfile>
import sys, http.server, functools
root, port, logf = sys.argv[1], int(sys.argv[2]), sys.argv[3]
class H(http.server.SimpleHTTPRequestHandler):
    def log_message(self, fmt, *a):
        with open(logf, "a", encoding="utf-8") as f:
            f.write("%s %s\n" % (self.command, self.path) + "")
            f.write("   UA: %s\n" % self.headers.get("User-Agent", "-")[:60])
http.server.ThreadingHTTPServer(("127.0.0.1", port), functools.partial(H, directory=root)).serve_forever()

Script exact run_crawl.py :

# Usage: python -I run_crawl.py <base_url>
import sys, json, time, asyncio
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig, CacheMode, JsonCssExtractionStrategy
base = sys.argv[1]
schema = {"name": "produits", "baseSelector": "li.produit",
          "fields": [{"name": "nom", "selector": ".nom", "type": "text"},
                     {"name": "prix", "selector": ".prix", "type": "text"}]}
async def main():
    async with AsyncWebCrawler(config=BrowserConfig(headless=True, verbose=False)) as c:
        # 1 : defaut, sans attente
        t = time.perf_counter()
        r = await c.arun(base + "/index.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, verbose=False))
        print("[1 defaut] ok=%s status=%s ms=%d" % (r.success, r.status_code, (time.perf_counter()-t)*1000))
        print("md contains 'Paragraphe ajoute':", "Paragraphe ajoute" in str(r.markdown))
        # 2 : attente 1.5 s pour laisser le JS s'executer
        t = time.perf_counter()
        r = await c.arun(base + "/index.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, delay_before_return_html=1.5, verbose=False))
        print("[2 delai 1.5s] ok=%s status=%s ms=%d" % (r.success, r.status_code, (time.perf_counter()-t)*1000))
        print("md contains 'Paragraphe ajoute':", "Paragraphe ajoute" in str(r.markdown))
        print("--- MARKDOWN ---"); print(str(r.markdown)); print("--- FIN ---")
        # 3 : extraction CSS sans LLM
        r = await c.arun(base + "/index.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, extraction_strategy=JsonCssExtractionStrategy(schema), verbose=False))
        print("[3 css] extracted_content:", r.extracted_content)
        # 4 : page interdite par robots.txt, check_robots_txt par defaut
        r = await c.arun(base + "/private/secret.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, verbose=False))
        print("[4 robots defaut] ok=%s status=%s err=%r md_has_secret=%s" % (r.success, r.status_code, r.error_message, "Zone privee" in str(r.markdown)))
        # 5 : check_robots_txt=True
        r = await c.arun(base + "/private/secret.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, check_robots_txt=True, verbose=False))
        print("[5 robots actif] ok=%s status=%s err=%r md_has_secret=%s" % (r.success, r.status_code, r.error_message, "Zone privee" in str(r.markdown)))
        r = await c.arun(base + "/index.html", config=CrawlerRunConfig(cache_mode=CacheMode.BYPASS, check_robots_txt=True, verbose=False))
        print("[6 robots actif, page autorisee] ok=%s status=%s" % (r.success, r.status_code))
asyncio.run(main())

Résultats mesurés

Sortie brute de run_crawl.py (out.txt, recopiée sans modification) :

[1 defaut] ok=True status=200 ms=367
md contains 'Paragraphe ajoute': False
[2 delai 1.5s] ok=True status=200 ms=1647
md contains 'Paragraphe ajoute': True
--- MARKDOWN ---
[Accueil](http://127.0.0.1:8765/index.html) [Zone privee](http://127.0.0.1:8765/private/secret.html)
# Catalogue de demonstration
Ce texte est present dans le HTML initial.
  * Theiere en fonte34,90 EUR
  * Bol en gres12,50 EUR
  * Plateau en bambou19,00 EUR


Paragraphe ajoute par JavaScript apres 500 ms.

--- FIN ---
[3 css] extracted_content: [
    {
        "nom": "Theiere en fonte",
        "prix": "34,90 EUR"
    },
    {
        "nom": "Bol en gres",
        "prix": "12,50 EUR"
    },
    {
        "nom": "Plateau en bambou",
        "prix": "19,00 EUR"
    }
]
[4 robots defaut] ok=True status=200 err='' md_has_secret=True
[5 robots actif] ok=False status=403 err='Access denied by robots.txt' md_has_secret=False
[6 robots actif, page autorisee] ok=True status=200

Journal brut de notre serveur local (server.log, une ligne par requête puis le début du User-Agent, tronqué à 60 caractères par notre script) :

GET /index.html
   UA: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/11
GET /index.html
   UA: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/11
GET /index.html
   UA: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/11
GET /private/secret.html
   UA: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/11
GET /robots.txt
   UA: Python/3.13 aiohttp/3.14.5
GET /index.html
   UA: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/11

Lecture de ces sorties :

ÉtapeConfigurationRésultat observé
1Par défautStatut 200 en 367 ms ; le paragraphe ajouté par JavaScript est absent du Markdown
2delay_before_return_html=1.5Statut 200 en 1 647 ms ; le paragraphe ajouté par JavaScript est présent
3JsonCssExtractionStrategy3 objets JSON (nom, prix), sans modèle de langage
4Page interdite par robots.txt, par défautStatut 200, contenu converti
5Même page, check_robots_txt=TrueStatut 403, Access denied by robots.txt, aucune requête reçue par le serveur pour cette page
6Page autorisée, check_robots_txt=TrueStatut 200

Observations complémentaires, toutes tirées de ces sorties ou du code installé :

  • Dans le Markdown, le nom et le prix d'un produit sont collés (« Theiere en fonte34,90 EUR »), car les deux éléments span ne sont pas séparés par un espace dans notre HTML. L'extraction CSS les a séparés.
  • Les 2 liens du menu sont conservés dans le Markdown, y compris celui vers la zone interdite par robots.txt.
  • Le fichier robots.txt n'a été demandé qu'une seule fois, à l'étape 5, avec un User-Agent différent (Python/3.13 aiohttp/3.14.5) de celui du navigateur (une chaîne Chrome qui annonce Linux alors que la machine est sous Windows). À l'étape 6, aucune seconde demande n'a été reçue. Le code installé (utils.py) fixe une durée de cache de 7 jours pour ce fichier ; nous n'avons pas testé son expiration.
  • La documentation du paramètre dans le code installé (async_configs.py) indique « Default: False » pour check_robots_txt, ce qui correspond à l'étape 4.

Limites du test

  • Une seule machine (Windows 11, processeur i5-10400F), une seule version (Crawl4AI 0.9.5, Chrome Headless Shell 156.0.8078.4).
  • Trois pages minuscules écrites par nous, servies depuis la même machine : aucun réseau réel, aucun site tiers, aucune page lourde, aucun blocage anti-robot, aucune authentification. Les durées (367 ms, 1 647 ms, 4,9 s) sont celles d'un seul passage, sans répétition.
  • Le Markdown obtenu n'a été jugé que sur ce petit exemple. Nous n'avons pas mesuré sa qualité sur des pages réelles.
  • Le test sur robots.txt porte sur un fichier très simple (une seule règle Disallow). Nous n'avons pas essayé les règles plus complexes, ni la directive Crawl-delay.
  • Les empreintes ne sont vérifiées que pour le fichier .whl de Crawl4AI : les autres paquets Python et le navigateur téléchargé ne l'ont pas été.
  • La mémoire utilisée n'a pas été mesurée.
  • Le nombre de requêtes GET /index.html dans le journal serveur publié dépend du cache du navigateur : en re-exécution, le navigateur n'en émet qu'une (les étapes 2, 3 et 6 réutilisent son cache). Ce nombre n'est donc pas reproductible à l'identique. Cela ne change pas la conclusion sur robots.txt : avec check_robots_txt=True, la page /private/ n'est jamais demandée.

Ce que nous n'avons pas testé

  • Le crawl de sites réels, de plusieurs pages à la suite, la profondeur de crawl, le crawl en parallèle, les limites de débit et la politesse envers les serveurs.
  • L'extraction par modèle de langage (qui suppose un fournisseur et en général une clé), la génération automatique de schémas, le Markdown filtré pour agents.
  • Le mode furtif, les proxys, les sessions authentifiées, les pages qui exigent des clics ou des défilements.
  • Le Docker, le serveur d'API, l'offre en ligne payante.
  • Les deux signalements de sécurité cités dans l'avis : nous ne les avons pas reproduits.
  • La légalité du crawl d'un site donné (conditions d'utilisation, droit des bases de données, RGPD) : ce test technique n'en dit rien.
  • D'autres systèmes d'exploitation et versions, y compris toute version postérieure à la 0.9.5.

Sources

  • Notre test du 2026-10-18, décrit ci-dessus (Crawl4AI 0.9.5, empreinte du fichier .whl vérifiée).
  • https://github.com/unclecode/crawl4ai
  • https://pypi.org/project/crawl4ai/0.9.5/ (version, date de publication et empreinte SHA-256 du fichier)
  • Code source installé de la version 0.9.5 (async_configs.py, async_webcrawler.py, utils.py) pour la valeur par défaut de check_robots_txt et la durée de cache de robots.txt.
  • Avis MagicMate sur Crawl4AI pour la lecture des sources publiques, de la licence et des signalements de sécurité.

Aller plus loin

Recevoir la newsletter Marketia