Trois heures de build. Quarante leads qualifiés par run. Voici comment.
Le problème de départ
Deux heures par jour sur LinkedIn à chercher des prospects manuellement. Copier-coller des profils dans un spreadsheet. Chercher leur email sur Hunter. Écrire un message personnalisé. Recommencer.
C'est exactement le genre de tâche répétitive qui ne devrait pas exister en 2025.
L'architecture finale
LinkedIn Search
↓
Playwright Scraper (profils)
↓
Enrichissement GPT-4o (résumé + signaux d'achat)
↓
Scoring (0-100)
↓
Déduplication PostgreSQL
↓
Export CSV / webhook CRM
Étape 1 — Le scraper (45 min)
Playwright avec rotation de user-agent et délais aléatoires pour éviter la détection.
import asyncio
import random
from playwright.async_api import async_playwright
async def scrape_profile(url: str) -> dict:
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
viewport={"width": 1280, "height": 720},
)
page = await context.new_page()
# Délai humain aléatoire
await asyncio.sleep(random.uniform(2.5, 5.0))
await page.goto(url, wait_until="domcontentloaded")
name = await page.locator("h1").first.text_content()
title = await page.locator(".text-body-medium").first.text_content()
company = await page.locator(".inline-show-more-text").first.text_content()
await browser.close()
return {"name": name, "title": title, "company": company, "url": url}
Note légale : ce scraper est utilisé sur des profils publics LinkedIn pour usage personnel de prospection B2B, dans le respect des CGU de prospection commerciale.
Étape 2 — L'enrichissement IA (1h)
GPT-4o analyse chaque profil et retourne des signaux structurés avec Pydantic.
from pydantic import BaseModel
import instructor
import openai
class LeadSignals(BaseModel):
summary: str # Résumé 1 ligne
pain_points: list[str] # Problèmes probables
buy_signals: list[str] # Signaux d'achat détectés
fit_score: int # 0-100
opening_angle: str # Angle d'approche recommandé
client = instructor.from_openai(openai.OpenAI())
def enrich_lead(profile: dict) -> LeadSignals:
prompt = f"""
Profil LinkedIn :
Nom : {profile['name']}
Titre : {profile['title']}
Entreprise : {profile['company']}
Analyse ce prospect pour une offre d'automatisation de processus métier.
"""
return client.chat.completions.create(
model="gpt-4o",
response_model=LeadSignals,
messages=[{"role": "user", "content": prompt}],
)
Étape 3 — Le scoring (30 min)
Un score simple mais efficace basé sur 4 critères pondérés.
def score_lead(signals: LeadSignals, profile: dict) -> int:
score = 0
# Titre = décideur ? (+30)
decision_keywords = ["ceo", "cto", "directeur", "head of", "founder", "vp"]
if any(k in profile["title"].lower() for k in decision_keywords):
score += 30
# Signaux d'achat détectés (+40 max)
score += min(len(signals.buy_signals) * 10, 40)
# Score de fit IA (+30 max)
score += int(signals.fit_score * 0.3)
return min(score, 100)
Étape 4 — Les garde-fous anti-détection
Le point le plus fragile de tout le pipeline n'est ni le scraping ni l'IA — c'est de rester sous le radar. LinkedIn détecte les patterns de navigation trop réguliers. Trois règles simples ont suffi à tenir sur la durée :
- Volume plafonné : jamais plus de 80-100 profils scrapés par jour et par compte, réparti sur toute la journée plutôt qu'en rafale
- Délais variables :
random.uniform()partout, jamais unsleep()fixe — un comportement trop régulier est le signal le plus facile à détecter - Rotation de sessions : plusieurs comptes ou proxies résidentiels si le volume dépasse ce qu'un seul compte peut absorber sans risque de restriction
Ce sont les mêmes principes qui s'appliquent à n'importe quel scraping à grande échelle, au-delà de LinkedIn — le détail des techniques anti-détection est développé dans l'article sur le scraping éthique.
Étape 5 — Le message qui exploite l'opening_angle
C'est cette étape, ajoutée après coup, qui a fait la vraie différence sur le taux de réponse. Le opening_angle généré à l'étape 2 ne sert à rien s'il reste dans une base de données — il doit nourrir directement le message envoyé.
def generate_opening_message(profile: dict, signals: LeadSignals) -> str:
prompt = f"""
Rédige un message LinkedIn de premier contact, 3 phrases maximum, ton direct
et non commercial.
Prospect : {profile['name']}, {profile['title']} chez {profile['company']}
Angle d'approche à utiliser : {signals.opening_angle}
Point de douleur probable : {signals.pain_points[0] if signals.pain_points else 'non identifié'}
Ne pas vendre. Poser une question ouverte liée à l'angle d'approche.
"""
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": prompt}],
)
return response.choices[0].message.content
La différence avec un template classique ("Bonjour, je vois que vous travaillez chez X, souhaitez-vous échanger sur...") : chaque message référence un signal spécifique détecté sur le profil ou l'entreprise (une levée de fonds récente, une offre d'emploi qui trahit une croissance d'équipe, une techno spécifique repérée sur le site). Le prospect a l'impression — à juste titre — que le message a été écrit après une vraie recherche sur son cas, pas envoyé en masse à 200 profils similaires.
Résultats après 3 semaines
| Métrique | Avant | Après | | ------------------------- | ----- | ------ | | Temps prospection/semaine | 10h | 25 min | | Leads qualifiés/semaine | 15 | 40 | | Taux de réponse | 8% | 23% | | Deals signés (mois 1) | — | 3 |
Le taux de réponse a explosé parce que chaque message est maintenant personnalisé sur l'opening_angle généré par le modèle, pas sur un template générique.
Ce que je referais différemment
- Ajouter Apollo / Hunter en enrichissement avant GPT-4o pour avoir les emails directement
- Intégrer directement dans HubSpot via l'API plutôt qu'un export CSV
- Caching Redis sur les profils déjà enrichis — GPT-4o coûte quelques centimes par profil, inutile de payer deux fois pour le même prospect
Le code complet est disponible en tant que projet de référence — voir LeadMachine.