Aller au contenu

Construire un détecteur de technologies web avec Scrapy

Ce projet a commencé de façon très simple : éviter d'ouvrir manuellement 15 onglets pour comprendre la stack d'une entreprise.

C'est probablement le projet qui m'a le plus appris sur le scraping réel, les signaux techniques, les limites du web moderne, et la différence entre un script qui marche et un système qui tient dans le temps.


Le problème de départ

Dans mes workflows de prospection B2B, je passais énormément de temps à faire toujours la même chose : ouvrir un site, inspecter le code source, regarder les scripts chargés, chercher des tags marketing, comprendre rapidement le niveau de maturité technique.

Très vite, j'ai remarqué qu'un site web raconte énormément de choses sur une entreprise : pas seulement quelle techno elle utilise, mais aussi comment elle travaille, sa culture produit, parfois même son niveau de dette technique.

Signal détecté Ce que ça raconte souvent
Klaviyo Stack e-commerce moderne
HubSpot Logique inbound / CRM
Segment Culture data
GTM très chargé Tracking devenu difficile à maintenir
Absence de CMP Faible maturité RGPD
Shopify + Yotpo + Gorgias Stack DTC assez classique

Au début, je faisais ça entièrement à la main. Et honnêtement, c'était vite infernal.


Pourquoi pas les outils existants

J'ai évidemment regardé les outils existants :

Outil Problème principal
BuiltWith BuiltWith Fermé, pas d'API exploitable proprement, cher à grande échelle
Wappalyzer Wappalyzer Orienté extension navigateur, peu adapté à l'automatisation
Klaviyo Klaviyo & co APIs propriétaires, résultats non enrichissables dans un workflow

Le vrai blocage

Les outils existants étaient soit :

  • Trop orientés "extension navigateur"
  • Trop fermés pour une intégration propre dans des workflows
  • Trop chers à grande échelle
  • Impossibles à enrichir ou à modifier rapidement
  • Peu ou pas exploitables en automatisation

Et surtout :

je voulais comprendre exactement comment la détection fonctionnait, pas juste consommer un résultat.

Je voulais pouvoir ajouter mes propres signatures, modifier les règles, comprendre les faux positifs, et brancher le tout dans d'autres systèmes.


Les débuts : tout tournait sur mon PC Windows

C'est important de le préciser, parce que beaucoup d'articles techniques donnent l'impression que tout commence avec Kubernetes, des workers distribués et une architecture cloud propre.

Dans mon cas :

Python
Scrapy
quelques JSON
beaucoup trop d'onglets ouverts

C'était largement suffisant pour démarrer.

Le but n'était pas de crawler Internet. Je voulais surtout comprendre jusqu'où je pouvais pousser un moteur de détection relativement simple.


La première version était ridicule

if "hubspot" in html:
    print("HubSpot détecté")

Pendant environ 15 minutes, j'ai trouvé ça génial.

Puis les problèmes ont commencé : scripts injectés dynamiquement, CDNs mutualisés, technos détectables uniquement via cookies, frameworks JS qui renvoient un HTML presque vide, faux positifs partout.

C'est là que le projet a changé de nature.

Le changement de perspective

Un site web n'est pas une page HTML.

C'est un ensemble de signaux faibles.

À partir de là, je n'essayais plus de répondre à :

Est-ce que je trouve "HubSpot" dans le HTML ?

mais plutôt :

Est-ce que plusieurs éléments indépendants suggèrent fortement HubSpot ?

Ça a complètement changé l'architecture du système.


Architecture globale

Le pipeline a progressivement évolué vers ça :

flowchart TD
    A[Liste de domaines] --> B[Spider Scrapy]
    B --> C[HTML] & D[Scripts] & E[Headers] & F[Cookies] & G[Meta tags]
    C & D & E & F & G --> H[Moteur de signatures]
    H --> I[Scoring]
    I --> J[Technologies détectées]
    J --> K[Export JSON] & L[Supabase] & M[n8n workflows]

Point important : Scrapy ne détecte rien. Il collecte uniquement des signaux. La logique métier est ailleurs.


Pourquoi Scrapy

Performances natives

Même sur une machine locale classique, Scrapy tient des volumes très corrects : plusieurs centaines de pages/minute sans optimisation agressive, tant que le rendu JS reste désactivé.

Tout est déjà inclus : retries, throttling, concurrence, pipelines, middlewares.

Structure naturelle du projet

Très vite, le repo a commencé à ressembler à quelque chose de propre :

/spiders
/pipelines
/middlewares
/signatures
/settings

Scrapy force presque naturellement à structurer le projet correctement.

Outil Pourquoi écarté à ce stade
Playwright seul Trop lourd pour démarrer, coût trop élevé
Selenium Lent, verbeux, peu adapté au volume
requests + BeautifulSoup Bien pour du one-shot, pas pour de l'industriel
Crawl4AI Trop récent au moment du projet

Certains ont fini par entrer dans le pipeline, mais pas au départ.


Le problème du JavaScript moderne

C'est probablement la partie qui m'a le plus frustré.

Au début, je voulais absolument éviter Playwright : lourd, gourmand, complexifie tout.

Le problème, c'est qu'une partie du web moderne renvoie quasiment un shell vide. Sur certains sites Next.js, le crawler récupérait littéralement :

<div id="__next"></div>

Et pratiquement tout le reste arrivait après hydration.

Conséquence directe

Certaines technos devenaient totalement invisibles côté Scrapy pur.

Le compromis : un fallback Playwright conditionnel. Scrapy d'abord, Playwright uniquement si nécessaire.

flowchart LR
    A[Scrapy] --> B{Détection suffisante ?}
    B -->|Oui| C[Fin]
    B -->|Non| D[Fallback Playwright]
    D --> E[DOM rendu]
    E --> F[Analyse complémentaire]
from playwright.async_api import async_playwright

async def render_page(url):
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        await page.goto(url)
        html = await page.content()
        await browser.close()
        return html

Le plus difficile n'était pas ce code : c'était de décider quand déclencher le fallback, sur quels domaines, et avec quels timeouts.

Ce que ça change concrètement :

Mode Pages/min (approx.) RAM Scalabilité locale
Scrapy seul ~300-400 Faible Bonne
Scrapy + Playwright ~20-50 Élevée Difficile

C'est probablement l'arbitrage technique le plus important du projet.


La vraie difficulté : les signatures

Le scraping n'était finalement pas le problème principal. Le vrai sujet :

comment maintenir un moteur de signatures fiable.

{
  "name": "HubSpot",
  "html": "hubspot"
}

Résultat : faux positifs en masse, détections fragiles, aucune notion de confiance, impossible à maintenir.

{
  "name": "Klaviyo",
  "html": [
    "klaviyo"
  ],
  "script": [
    "static.klaviyo.com"
  ],
  "cookies": [
    "__kla_id"
  ],
  "confidence": {
    "html": 0.10,
    "script": 0.30,
    "cookie": 0.45
  }
}

Plusieurs types de signaux, plusieurs niveaux de confiance, plusieurs preuves indépendantes.

Le vrai progrès

Je ne détectais plus "une occurrence".

Je cumulais des preuves.


Le scoring réel

J'ai testé des approches plus statistiques, plus automatiques. Elles rendaient surtout le système plus difficile à debugger.

Aujourd'hui, le scoring reste volontairement lisible :

def compute_confidence(matches):
    score = 0

    for match in matches:
        if match["type"] == "cookie":
            score += 0.45  # (1)!
        elif match["type"] == "script":
            score += 0.30  # (2)!
        elif match["type"] == "html":
            score += 0.10  # (3)!

    return min(score, 1.0)  # (4)!
  1. Un cookie est très fiable : il faut l'avoir explicitement installé sur le site.
  2. Un script CDN est fiable mais peut être mutualisé (Cloudflare, jsdelivr…).
  3. Du HTML brut peut contenir le mot par hasard dans du contenu éditorial.
  4. On plafonne à 1.0 : au-delà de plusieurs signaux concordants, c'est quasi certain.

Ce n'est pas "intelligent". Mais c'est stable, lisible, et facile à faire évoluer.


Exemple réel de sortie

{
  "domain": "example-store.com",
  "technologies": [
    {
      "name": "Shopify",
      "confidence": 0.97,
      "signals": ["cdn.shopify.com", "_shopify_y", "shopify-checkout"]
    },
    {
      "name": "Klaviyo",
      "confidence": 0.91,
      "signals": ["static.klaviyo.com", "__kla_id"]
    }
  ],
  "meta": {
    "crawl_time_ms": 1840,
    "javascript_rendered": false,
    "redirects": 1
  }
}

C'est là que le projet sort du simple "détecteur". À partir de ces signaux, on peut :

  • scorer des entreprises selon leur maturité tech,
  • enrichir des workflows IA,
  • construire des ICP,
  • détecter des changements de stack dans le temps,
  • générer des signaux commerciaux.

Ce qui a cassé plusieurs fois

Les données web sont extrêmement sales.

Redirects absurdes, certificats invalides, timeouts, protections anti-bot, HTML cassés, encodages étranges : on passe beaucoup plus de temps à gérer les cas limites qu'à écrire la logique métier.

Capture du terminal Scrapy en train de crawler, les warnings non-200 sont la norme, pas l'exception.

Les faux positifs ont été un enfer

Exemple classique : le mot segment peut vouloir dire Segment CDP, un mot dans du contenu, ou un slug d'URL.

Même problème avec certains CDNs mutualisés (Cloudflare, jsdelivr, Google APIs).

Détecter moins de technos mais avec des signaux fiables est largement préférable.


Ce qui m'a surpris après plusieurs milliers de domaines

Observations

  • Énormément de sites sont cassés ou à moitié fonctionnels
  • Beaucoup plus de WordPress que prévu, dans tous les secteurs
  • Les stacks marketing sont très répétitives par secteur d'activité
  • Certains sites chargent des dizaines de scripts morts ou hérités
  • Le web moderne est beaucoup plus fragile que je ne l'imaginais

Ce que je referais autrement

Ce que j'avais :

signatures.json
signatures_v2.json
signatures_final_final.json

Ce que je ferais :

/signatures
    /crm
        hubspot.json
        salesforce.json
    /marketing
        klaviyo.json
        mailchimp.json
    /analytics
        segment.json
        gtm.json

Avec validation, tests, scoring et versionning propre dès le départ.

J'ai longtemps testé manuellement sur des domaines au hasard.

Erreur classique.

Ce que je construirais aujourd'hui :

  • une liste de domaines de référence avec stack connue,
  • des snapshots HTML,
  • des tests de régression automatisés.

Les signatures vieillissent extrêmement vite. Une techno peut changer de CDN, de script, ou de domaine du jour au lendemain.


Ce qui reste encore bricolé

Le rendu JS coûte cher

Playwright améliore considérablement les résultats. Mais RAM, CPU et temps de crawl explosent vite. Aujourd'hui, le fallback reste difficile à scaler proprement.

Certaines technos restent invisibles

Le système voit surtout ce qui fuite côté frontend.

Tout ce qui est backend, infra, outils internes ou APIs reste beaucoup plus difficile à détecter. C'est une limite structurelle du scraping côté HTML.


Ce que ce projet m'a réellement appris

Je pensais construire un détecteur de technos. En réalité, j'ai surtout construit un système de collecte de signaux web.

C'est très différent.

Le projet m'a surtout appris :

  • la robustesse d'abord,
  • les arbitrages entre précision et coût,
  • les limites réelles du scraping,
  • et l'importance de garder des systèmes simples et lisibles.

Un pipeline compréhensible et stable vaut souvent mieux qu'un système "intelligent" impossible à maintenir.


Ce que j'ai envie d'explorer ensuite

  • Scoring probabiliste basé sur l'historique
  • Évolution des stacks dans le temps (monitoring continu)
  • Clustering de technologies par secteur
  • Embeddings sur les signaux détectés
  • Détection de changements de maturité technique