Comment scraper les sites web protégés par Cloudflare dans n8n ?

{“output”:“Je construis un flux de synthèse quotidienne qui récupère du contenu à partir de blogs de l’industrie HVAC. Trois de mes sources (HouseCall Pro, Jobber Academy, HVAC Informed) sont protégées par Cloudflare. Lorsque mon nœud HTTP Request accède à ces URL, il reçoit une page de défi Cloudflare 403 "Un instant…" au lieu du contenu réel.\n\nCes sites n’ont pas non plus de flux RSS, donc les nœuds RSS Feed Read ne fonctionnent pas.\n\n\nCe que j’ai essayé :\n\n* Nœud HTTP Request (GET) — retourne le HTML de défi Cloudflare\n\n* Nœud RSS Feed Read — retourne des erreurs d’analyse XML (les pages ne sont pas du RSS)\n\n\nQuestions :\n\n1. Existe-t-il un moyen de contourner la protection bot Cloudflare nativement dans n8n ?\n\n2. Dois-je utiliser Apify, ScrapingBee ou BrightData pour ces sources ?\n\n3. Existe-t-il un nœud communautaire qui gère les sites protégés par Cloudflare ?\n\n4. L’ajout d’en-têtes de type navigateur (User-Agent, Accept-Language, etc.) aiderait-il ?”}

@Asim_Arman a fait une vérification rapide de tes 3 sources avant de recommander quoi que ce soit et il y a de bonnes nouvelles pour l’une d’elles — HouseCall Pro n’est pas vraiment sur Cloudflare et son /feed/ fonctionne parfaitement si tu ajoutes simplement un vrai header User-Agent de navigateur au nœud HTTP Request. testé avec un User-Agent Mozilla/5.0 contre Housecall Pro et récupéré un XML RSS valide. ton 403 là-bas est presque certainement dû au fait que le nœud HTTP Request de n8n utilise par défaut un User-Agent générique (ou aucun) et le site a un filtre UA basique. bascule sur RSS Feed Read pointant sur Housecall Pro avec un override User-Agent et c’est réglé pour celui-ci, pas besoin de scraping.

Jobber (getjobber.com) est réellement sur Cloudflare — header cf-ray présent dans les réponses — et il n’y a pas de RSS pour la section academy. HVAC Informed n’expose pas non plus de RSS. ces deux-là ont réellement besoin d’un service de scraping.

sur la question de contourner Cloudflare nativement, il n’y a pas vraiment moyen de le faire dans n8n. leur challenge nécessite d’exécuter du JS pour résoudre la preuve de travail plus l’empreinte comportementale, et le nœud HTTP Request est un client http simple sans moteur de navigateur. les libs style cloudscraper de 2022-2023 sont toutes cassées contre la Cloudflare actuelle aussi, donc les quelques nœuds communautaires qui les enveloppent ne fonctionnent pas non plus.

les headers style navigateur corrigent les filtres UA basiques (c’est d’ailleurs ce que le 403 de HouseCall Pro était ironiquement) mais ne font rien contre le vrai challenge Cloudflare — ils font l’empreinte du TLS, JA3, le timing des requêtes, et l’exécution JS au-delà des simples headers.

pour ScrapingBee vs Apify vs BrightData — pour ton cas d’usage (digest quotidien bas volume, juste 2 sites nécessitant du vrai scraping) ScrapingBee ou Firecrawl est le plus propre. comparaison approximative :

{
  "scrapingbee": {"per_request": "~$0.001-0.005", "fit": "volume bas-moyen, simple, rapide"},
  "scraperapi": {"per_request": "~$0.001-0.003", "fit": "comparable à scrapingbee"},
  "firecrawl": {"per_request": "~$0.001", "fit": "scrape-en-markdown, output prête pour LLM"},
  "brightdata": {"per_request": "~$0.005-0.02", "fit": "cibles les plus difficiles, cher mais plus fiable"},
  "apify": {"per_request": "varie", "fit": "flux multi-pages complexes"}
}

ScrapingBee appelé via HTTP Request ressemble à :

{
  "method": "GET",
  "url": "https://app.scrapingbee.com/api/v1/",
  "qs": {
    "api_key": "<your_key>",
    "url": "https://www.getjobber.com/academy/",
    "render_js": "true",
    "premium_proxy": "true"
  }
}

l’état final c’est un RSS Feed Read pour HouseCall Pro, deux appels HTTP ScrapingBee pour Jobber et HVAC Informed, puis parse → digest. beaucoup moins cher que de traiter les 3 comme protégés par Cloudflare quand seul un l’est réellement.

Merci beaucoup ! Je vais essayer cela et te tenir au courant

De rien ! Dis-moi comment ça marche !

J’aimerais ajouter qu’Apify dispose du Smart Article Extractor qui peut (généralement) détecter automatiquement les liens d’articles sur une page, puis les scraper un par un.

Il émule aussi un navigateur pour contourner Cloudflare. C’est un acteur à usage unique, donc un appel quotidien à trois blogs rentrera probablement dans leur plan gratuit.

Pour des cas plus généraux (hors articles), vous pouvez obtenir du contenu structuré en utilisant leur Website Content Crawler et en les passant ensuite par un petit modèle IA, comme haiku ou gpt minis.

Pour les sites plus complexes comme Jobber et HVAC Informed, si tu utilises une instance n8n auto-hébergée avec Docker et que tu préfères éviter les solutions payantes, une alternative intéressante consiste à exécuter SeleniumBase en mode Undetected ChromeDriver (UC) au sein d’un conteneur Docker distinct. Le mode UC (Undetected ChromeDriver) de SeleniumBase est actuellement l’un des outils open source les plus robustes pour contourner les protections Cloudflare.

Tu peux alors configurer ce conteneur pour faire tourner tes scripts de scraping, puis les déclencher directement depuis n8n grâce au nœud Execute Command (ou à l’aide d’un outil personnalisé si tu as structuré ton flux autour d’un Agent IA n8n.

Une chose que personne n’a encore mentionnée : avant de recourir à un service de scraping, vérifiez si ces sites disposent d’un sitemap ou d’un flux Atom caché sous un chemin non standard.

Essayez d’accéder à ces URLs avant de payer quoi que ce soit :

  • /sitemap.xml
  • /sitemap_index.xml
  • /feed/
  • /atom.xml
  • /blog/feed/ ou /blog/rss/

De nombreux blogs alimentés par WordPress/HubSpot (que HouseCall Pro et Jobber utilisent tous les deux) ont les flux activés mais non liés dans le <head>. Le nœud RSS Read fonctionnera parfaitement si vous accédez à la bonne URL.

Pour ceux qui n’ont vraiment pas de flux et où Cloudflare vous bloque :

Approche la moins chère de qualité production : Firecrawl ou ScrapingBee via le nœud HTTP Request.

HTTP Request → POST https://api.firecrawl.dev/v1/scrape
Headers: Authorization: Bearer <YOUR_KEY>
Body: { “url”: “https://target.com/blog”, “formats”: [“markdown”] }

Retourne du markdown propre — pas besoin d’analyse HTML. Exécutez-le sur un Schedule Trigger une fois par jour. Avec 3 sites × 1 requête/jour, vous resterez bien en deçà des limites du tier gratuit.

Renforcement pour la production (la partie que la plupart des tutoriels omettent) :

  1. Ajoutez une nouvelle tentative avec backoff exponentiel sur le nœud HTTP Request — les APIs de scraping vous retournent occasionnellement des 429
  2. Stockez le hash de la dernière URL traitée dans une table Postgres afin de ne pas retraiter le même article demain (idempotence)
  3. Ajoutez un workflow Error Trigger — si votre digest échoue silencieusement le lundi, vous voulez le savoir avant vendredi

La partie « il suffit de scraper » est facile. La faire fonctionner de façon fiable chaque jour pendant 6 mois sans défaillances silencieuses, c’est là que la plupart des workflows de digest quotidiens se cassent la figure.

Hé, j’apprécie vraiment le détail de ton explication, achamm. C’était super utile et j’ai fini par suivre la plupart de tes conseils.

Ce que j’ai suivi exactement :

  • HouseCall Pro - confirmé que ce n’était pas Cloudflare, j’ai ajouté l’en-tête User-Agent au nœud HTTP Request, je l’ai pointé sur /feed/, j’ai parsé à travers un nœud XML, puis un nœud Code. Ça marche parfaitement maintenant.

  • Jobber et HVAC Informed - j’ai utilisé Firecrawl à la place de ScrapingBee (même gamme de prix que tu as mentionnée, j’ai juste préféré le format markdown pour passer à Claude).

    Approche en deux étapes : scraper d’abord la page de listing pour extraire les URLs des articles, puis scraper chaque article individuellement via une deuxième requête HTTP.

Ce qu’on a ajouté au-delà de tes recommandations :

  • Un nœud Parse Code après chaque requête Firecrawl pour normaliser toutes les sources dans des champs identiques (title, content, link, feedSource) avant la fusion

Merci beaucoup mec, content de t’avoir dans la communauté

De rien, on est content d’avoir pu t’aider !

Si tu as la possibilité d’auto-héberger un outil (par exemple via Docker sur le même serveur que ton n8n) et que tu veux éviter les limites de quota des plateformes payantes (comme Apify, ScrapingBee ou Firecrawl), tu devrais regarder du côté de FlareSolverr.
C’est un proxy open-source qui lance un navigateur headless en arrière-plan pour résoudre le défi Cloudflare et te renvoyer le HTML brut de la page. Tu n’as qu’à l’interroger localement avec un simple nœud HTTP Request dans n8n.
Cependant FlareSolverr te renvoie le code HTML brut . Tu devras donc ajouter un nœud HTML ou un nœud Code juste après pour parser et structurer toi-même les données en JSON.
un petit exemple:

{
“nodes”: [
{
“parameters”: {
“jsCode”: “const html = $json.solution.response;\n\nfunction getText(html, startMarker, endMarker) {\n const start = html.indexOf(startMarker);\n if (start === -1) return ‘’;\n const end = html.indexOf(endMarker, start);\n if (end === -1) return ‘’;\n return html.substring(start + startMarker.length, end).trim();\n}\n\nfunction getMetaContent(html, name) {\n const regex = new RegExp(<meta name=\"${name}\" content=\"([^\"]+)\", ‘i’);\n const match = html.match(regex);\n return match ? match[1] : ‘’;\n}\n\nlet title = getText(html, ‘

’, ‘’);\n// nettoie le titre des balises\ntitle = title.replace(/<a[^>]>([^<])<\/a>/, ‘$1’).trim();\n\nconst description = getMetaContent(html, ‘description’);\n\nlet content = getText(html, ‘
’, ‘
’);\n// nettoie le contenu\ncontent = content\n .replace(/<script[^>]>[\s\S]?<\/script>/gi, ‘’)\n .replace(/<style[^>]>[\s\S]?<\/style>/gi, ‘’)\n .replace(/<[^>]+>/g, ’ ')\n .replace(/\s+/g, ’ ')\n .trim();\n\nreturn [{\n title: title,\n description: description,\n content: content\n}];”
},
“type”: “n8n-nodes-base.code”,
“typeVersion”: 2,
“position”: [
-912,
4480
],
“id”: “a6275095-c4ed-427f-a791-620bcd83710b”,
“name”: “Extract from HTML”
},
{
“parameters”: {
“method”: “POST”,
“url”: “=http://flaresolverr:8191/v1”,
“sendHeaders”: true,
“headerParameters”: {
“parameters”: [
{
“name”: “Content-Type”,
“value”: “application/json”
}
]
},
“sendBody”: true,
“bodyParameters”: {
“parameters”: [
{
“name”: “cmd”,
“value”: “request.get”
},
{
“name”: “url”,
“value”: “={{ $json.url }}”
},
{
“name”: “maxTimeout”,
“value”: 90000
}
]
},
“options”: {
“timeout”: 120000
}
},
“type”: “n8n-nodes-base.httpRequest”,
“typeVersion”: 4.4,
“position”: [
-1120,
4480
],
“id”: “4c884f8a-09c7-4c13-b7e8-ac38098b00f6”,
“name”: “FlareSolverr”,
“retryOnFail”: true
}
],
“connections”: {
“Extract from HTML”: {
“main”: [

]
},
“FlareSolverr”: {
“main”: [
[
{
“node”: “Extract from HTML”,
“type”: “main”,
“index”: 0
}
]
]
}
},
“pinData”: {},
“meta”: {
“templateCredsSetupCompleted”: true,
“instanceId”: “03428eda2f2a76af33393f7fa59419dea9e2e74e0d630500b36df8cf5b8e161d”
}
}