Guide Complet : Scraper Twitter/X avec des Proxies en 2025

Apprenez à extraire les données publiques de Twitter/X après les restrictions API. Guide technique avec exemples Python, Playwright et proxies résidentiels pour contourner les blocages.

Guide Complet : Scraper Twitter/X avec des Proxies en 2025

Avertissement important : Cet article traite exclusivement de l'accès aux données publiques de Twitter/X. Le scraping doit respecter les Conditions d'Utilisation de chaque plateforme et les lois applicables, notamment le CFAA aux États-Unis et le RGPD en Europe. Lisez toujours robots.txt et envisagez les APIs officielles avant de scraper.

Depuis les changements radicaux opérés par Elon Musk sur l'API de Twitter (devenu X), les équipes de développement, SEO et growth se retrouvent face à un dilemme : payer des tarifs API prohibitifs ou se tourner vers le web scraping. Si vous lisez cet article, vous avez probablement déjà rencontré des erreurs 429, des blocages IP, ou des CAPTCHAs interminables. Vous n'êtes pas seuls.

La suppression de l'accès gratuit à l'API de recherche Twitter en 2023 a poussé de nombreuses équipes vers le scraping web. Mais X défend agressivement ses données. Dans ce guide, nous verrons comment utiliser des proxies résidentiels pour accéder aux informations publiques de manière fiable et responsable.

Le paysage post-restrictions API de Twitter/X

Comprendre pourquoi le scraping est devenu nécessaire pour beaucoup commence par comprendre ce qui a changé dans l'écosystème API de X.

Les nouveaux tiers de l'API X

En 2023, X a restructuré son API en plusieurs tiers payants, éliminant pratiquement l'accès gratuit :

d>Oui
Tier API Prix mensuel Limite de tweets Accès recherche
Free 0€ 1,500 tweets/mois Non
Basic ~100€ 10,000 tweets/mois Limité
Pro ~5,000€ 1M tweets/mois
Enterprise Sur devis Illimité Complet

Pour une équipe de monitoring de sentiment analysant 50,000 tweets par jour, le tier Pro à 5,000€/mois devient rapidement insoutenable. C'est pourquoi le scraping web est redevenu une option viable pour accéder aux données publiques que n'importe quel utilisateur non connecté peut voir dans son navigateur.

La suppression de la recherche gratuite

Avant 2023, les développeurs pouvaient utiliser l'API v1.1 pour rechercher des tweets gratuitement avec des limites raisonnables. Cette fonctionnalité a été complètement retirée du tier gratuit. Résultat : les projets open-source, les chercheurs académiques et les startups bootstrapées se sont tournés vers le scraping de l'interface web.

Données accessibles via le web public

Toutes les données de X ne sont pas égales face au scraping. Comprendre la distinction entre contenu public et contenu nécessitant une authentification est crucial pour concevoir votre stratégie.

Contenu accessible sans connexion

Un utilisateur non connecté peut accéder à :

  • Profils utilisateurs publics : nom, bio, nombre de followers/following, localisation, lien du profil
  • Tweets individuels : texte, images, vidéos, nombre de likes/retweets/replies
  • Fils de réponses : les réponses à un tweet spécifique
  • Tendances (Trending) : les sujets tendance par région
  • Résultats de recherche : tweets récents correspondant à une requête (avec limitations)
  • Listes publiques : membres et tweets d'une liste

Contenu nécessitant une connexion

Certaines données sont derrière un « mur de connexion » :

  • Tweets d'utilisateurs ayant activé les paramètres de confidentialité
  • Historique complet des tweets (au-delà de la pagination web standard)
  • Recherches avancées avec filtres temporels précis
  • Communautés privées
  • Messages directs (éthiquement et légalement interdits au scraping)

Règle d'or : Si vous devez vous connecter pour voir le contenu, le scraper ne devrait pas y accéder. Concentrez-vous sur les données réellement publiques.

Pourquoi les proxies résidentiels sont indispensables

Si vous avez essayé de scraper X avec des proxies datacenter ou sans proxy du tout, vous connaissez le résultat : erreurs 429, CAPTCHAs répétés, blocages immédiats.

La détection agressive des IPs datacenter

X maintient des listes d'adresses IP associées aux datacenters et aux services de cloud. Ces plages sont :

  • Immédiatement flaggées comme suspectes
  • Soumises à des limites de taux plus strictes
  • Souvent bloquées avant même le premier tweet

Les proxies datacenter (AWS, DigitalOcean, Hetzner) sont pratiquement inutilisables pour le scraping X à grande échelle. Même les proxies datacenter « premium » sont rapidement identifiés.

L'avantage des IPs résidentielles

Les proxies résidentiels utilisent des adresses IP attribuées à de vrais foyers par des FAI. Pour X, votre trafic ressemble à celui d'un utilisateur domestique normal :

  • Rotation naturelle : chaque requête peut venir d'une nouvelle IP résidentielle
  • Réputation IP élevée : les FAI maintiennent des IPs « propres »
  • Distribution géographique : simulez des utilisateurs de différents pays/régions
  • Contournement des limites IP : distribuez les requêtes sur des milliers d'adresses

Les proxies mobiles (4G/5G) offrent une protection encore plus élevée, car les plages IP mobiles sont intrinsèquement dynamiques et difficiles à bloquer sans affecter les utilisateurs légitimes.

Implémentation Python avec Playwright et proxies résidentiels

Passons à la pratique. X utilise une Single Page Application (SPA) qui charge les données via des endpoints GraphQL internes. Voici comment les extraire de manière structurée.

Configuration du proxy résidentiel

Avec ProxyHat, configurez votre proxy résidentiel rotatif :

# Configuration du proxy résidentiel ProxyHat
PROXY_HOST = "gate.proxyhat.com"
PROXY_PORT = 8080
PROXY_USER = "your_username-country-US"  # Rotation par pays
PROXY_PASS = "your_password"

# URL du proxy HTTP
PROXY_URL = f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"

Pour une rotation par requête, ajoutez un flag de session dans le nom d'utilisateur :

# Session aléatoire pour rotation par requête
import random
import string

def generate_session_id(length=8):
    return ''.join(random.choices(string.ascii_lowercase + string.digits, k=length))

session_id = generate_session_id()
PROXY_USER = f"your_username-country-US-session-{session_id}"
PROXY_URL = f"http://{PROXY_USER}:{PROXY_PASS}@gate.proxyhat.com:8080"

Scraper un profil utilisateur avec Playwright

Voici un script complet pour extraire les données d'un profil public :

import asyncio
from playwright.async_api import async_playwright
import json
import re

class TwitterProfileScraper:
    def __init__(self, proxy_url=None):
        self.proxy_url = proxy_url
        
    async def scrape_profile(self, username: str) -> dict:
        async with async_playwright() as p:
            # Lancement du navigateur avec proxy
            browser_args = {}
            if self.proxy_url:
                browser_args = {"proxy": {"server": self.proxy_url}}
            
            browser = await p.chromium.launch(
                headless=True,
                args=['--disable-blink-features=AutomationControlled']
            )
            
            context = await browser.new_context(
                user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
                viewport={'width': 1920, 'height': 1080},
                **browser_args
            )
            
            page = await context.new_page()
            
            try:
                # Navigation vers le profil
                url = f"https://x.com/{username}"
                response = await page.goto(url, wait_until='networkidle', timeout=30000)
                
                # Attendre le chargement du contenu
                await page.wait_for_selector('[data-testid="primaryColumn"]', timeout=15000)
                
                # Extraction des données du profil depuis le DOM
                profile_data = await self._extract_profile_data(page)
                
                # Capture des payloads GraphQL si disponibles
                graphql_data = await self._intercept_graphql(page)
                
                if graphql_data:
                    profile_data['raw_graphql'] = graphql_data
                
                return profile_data
                
            except Exception as e:
                return {"error": str(e), "username": username}
            finally:
                await browser.close()
    
    async def _extract_profile_data(self, page) -> dict:
        data = {}
        
        try:
            # Nom d'affichage
            name_el = await page.query_selector('[data-testid="UserName"]')
            if name_el:
                data['display_name'] = await name_el.inner_text()
            
            # Bio
            bio_el = await page.query_selector('[data-testid="UserDescription"]')
            if bio_el:
                data['bio'] = await bio_el.inner_text()
            
            # Stats (followers, following, tweets)
            stats = await page.query_selector_all('[data-testid="primaryColumn"] a[href*="/"] span')
            # Logique d'extraction des stats...
            
            # Localisation
            location_el = await page.query_selector('[data-testid="UserLocation"]')
            if location_el:
                data['location'] = await location_el.inner_text()
                
            # URL du profil
            url_el = await page.query_selector('[data-testid="UserUrl"]')
            if url_el:
                data['profile_url'] = await url_el.inner_text()
                
        except Exception as e:
            data['extraction_error'] = str(e)
            
        return data
    
    async def _intercept_graphql(self, page) -> dict:
        """Intercepte les réponses GraphQL de X"""
        graphql_responses = []
        
        async def handle_response(response):
            if 'graphql' in response.url:
                try:
                    json_data = await response.json()
                    graphql_responses.append({
                        'url': response.url,
                        'data': json_data
                    })
                except:
                    pass
        
        page.on('response', handle_response)
        
        # Recharger pour capturer les requêtes
        await page.reload(wait_until='networkidle')
        
        return graphql_responses[0] if graphql_responses else None

# Utilisation
async def main():
    proxy_url = "http://user-country-US-session-abc123:pass@gate.proxyhat.com:8080"
    scraper = TwitterProfileScraper(proxy_url)
    result = await scraper.scrape_profile("elonmusk")
    print(json.dumps(result, indent=2, ensure_ascii=False))

asyncio.run(main())

Exemple Node.js avec Puppeteer

Pour les équipes JavaScript, voici une approche similaire avec Puppeteer :

const puppeteer = require('puppeteer');

async function scrapeTwitterProfile(username, proxyUrl) {
    const browser = await puppeteer.launch({
        headless: 'new',
        args: [
            `--proxy-server=${proxyUrl}`,
            '--no-sandbox',
            '--disable-blink-features=AutomationControlled'
        ]
    });
    
    const page = await browser.newPage();
    
    await page.setUserAgent(
        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
    );
    
    // Intercepter les réponses GraphQL
    const graphqlData = [];
    page.on('response', async (response) => {
        if (response.url().includes('graphql') && response.url().includes('UserByScreenName')) {
            try {
                const data = await response.json();
                graphqlData.push(data);
            } catch (e) {}
        }
    });
    
    try {
        await page.goto(`https://x.com/${username}`, {
            waitUntil: 'networkidle0',
            timeout: 30000
        });
        
        await page.waitForSelector('[data-testid="primaryColumn"]', { timeout: 15000 });
        
        // Extraction des données...
        const profileData = await page.evaluate(() => {
            const getName = () => document.querySelector('[data-testid="UserName"]')?.innerText;
            const getBio = () => document.querySelector('[data-testid="UserDescription"]')?.innerText;
            const getLocation = () => document.querySelector('[data-testid="UserLocation"]')?.innerText;
            
            return {
                display_name: getName(),
                bio: getBio(),
                location: getLocation()
            };
        });
        
        return {
            ...profileData,
            graphql: graphqlData[0] || null
        };
        
    } catch (error) {
        return { error: error.message };
    } finally {
        await browser.close();
    }
}

// Usage
const proxyUrl = 'http://user-country-US-session-xyz:pass@gate.proxyhat.com:8080';
scrapeTwitterProfile('elonmusk', proxyUrl).then(console.log);

Gestion des limites de taux et erreurs 429

X applique plusieurs couches de protection contre le scraping. Comprendre ces mécanismes est essentiel pour construire un système robuste.

Types de limitation chez X

Type Déclencheur Symptôme Solution
IP-level Trop de requêtes depuis une IP Erreur 429 Rotation d'IP résidentielle
Session-level Comportement suspect d'une session CAPTCHA répétés Rotation de cookies/sessions
Account-level Activité suspecte d'un compte Blocage temporaire Éviter les comptes (scraping anonyme)
Sliding window Pattern temporel détecté Limitation progressive Délais aléatoires, rate limiting

Stratégie de backoff exponentiel

import asyncio
import random
from typing import Optional

class RateLimitHandler:
    def __init__(self, max_retries: int = 3, base_delay: float = 2.0):
        self.max_retries = max_retries
        self.base_delay = base_delay
        self.consecutive_429s = 0
    
    async def execute_with_backoff(self, request_func, *args, **kwargs):
        """Exécute une requête avec gestion automatique des 429"""
        for attempt in range(self.max_retries):
            try:
                result = await request_func(*args, **kwargs)
                self.consecutive_429s = 0  # Reset on success
                return result
                
            except Exception as e:
                if '429' in str(e) or 'rate' in str(e).lower():
                    self.consecutive_429s += 1
                    
                    # Délai exponentiel avec jitter
                    delay = self.base_delay * (2 ** attempt) + random.uniform(0, 1)
                    
                    # Si trop de 429 consécutifs, pause longue
                    if self.consecutive_429s > 5:
                        delay *= 3
                        print(f"Rate limit critique, pause prolongée: {delay}s")
                    
                    print(f"Rate limit détecté, attente {delay:.1f}s (tentative {attempt + 1})")
                    await asyncio.sleep(delay)
                    
                else:
                    raise e
        
        raise Exception(f"Échec après {self.max_retries} tentatives")

# Utilisation
handler = RateLimitHandler(max_retries=5, base_delay=2.0)

async def safe_scrape(scraper, username):
    return await handler.execute_with_backoff(
        scraper.scrape_profile,
        username
    )

Rotation intelligente des sessions proxy

import itertools
from dataclasses import dataclass
from typing import List

@dataclass
class ProxySession:
    session_id: str
    country: str
    last_used: float
    request_count: int
    is_blocked: bool = False

class ProxyRotationManager:
    def __init__(self, base_user: str, password: str, countries: List[str]):
        self.base_user = base_user
        self.password = password
        self.countries = countries
        self.sessions: List[ProxySession] = []
        self.max_requests_per_session = 50
        self.min_session_age = 60  # secondes entre rotation
    
    def get_proxy_url(self) -> str:
        """Retourne l'URL proxy pour la session optimale"""
        import time
        current_time = time.time()
        
        # Filtrer les sessions disponibles
        available = [
            s for s in self.sessions
            if not s.is_blocked
            and s.request_count < self.max_requests_per_session
            and (current_time - s.last_used) > self.min_session_age
        ]
        
        if not available:
            # Créer une nouvelle session
            session_id = self._generate_session_id()
            country = self._select_country()
            new_session = ProxySession(
                session_id=session_id,
                country=country,
                last_used=current_time,
                request_count=0
            )
            self.sessions.append(new_session)
            available = [new_session]
        
        # Sélectionner la session la moins utilisée
        session = min(available, key=lambda s: s.request_count)
        session.request_count += 1
        session.last_used = current_time
        
        return self._build_proxy_url(session)
    
    def _build_proxy_url(self, session: ProxySession) -> str:
        username = f"{self.base_user}-country-{session.country}-session-{session.session_id}"
        return f"http://{username}:{self.password}@gate.proxyhat.com:8080"
    
    def _generate_session_id(self) -> str:
        import random, string
        return ''.join(random.choices(string.ascii_lowercase + string.digits, k=12))
    
    def _select_country(self) -> str:
        # Rotation entre les pays pour distribution
        import random
        return random.choice(self.countries)
    
    def mark_blocked(self, session_id: str):
        """Marque une session comme bloquée"""
        for s in self.sessions:
            if s.session_id == session_id:
                s.is_blocked = True

# Configuration pour le scraping X
rotation_manager = ProxyRotationManager(
    base_user="votre_username",
    password="votre_password",
    countries=["US", "GB", "DE", "FR", "CA"]
)

Cadre légal et considérations éthiques

Le scraping de données publiques existe dans une zone grise légale qu'il est crucial de comprendre.

Cas juridiques récents

hiQ Labs v. LinkedIn (2022) : La Cour d'appel du 9ème circuit a statué que le scraping de données publiques ne viole pas le CFAA (Computer Fraud and Abuse Act). Cette décision a établi un précédent important : les données accessibles publiquement sans authentification peuvent généralement être scrapées.

Cependant, X a poursuivi plusieurs entités pour scraping en 2024, invoquant :

  • Violation des Conditions d'Utilisation
  • Surcharge de serveurs (doS involontaire)
  • Accès non autorisé malgré les mesures techniques

Bonnes pratiques légales

  1. Respectez robots.txt : X utilise des directives pour limiter le crawling. Lisez-les et comprenez-les.
  2. Limitez vos taux : Évitez de surcharger les serveurs. 1-2 requêtes/seconde maximum par IP.
  3. Données publiques uniquement : Ne tentez jamais d'accéder à des contenus nécessitant une connexion.
  4. Usage non concurrentiel : Ne créez pas un clone direct de X avec les données scrapées.
  5. RGPD/CCPA : Si vous traitez des données personnelles d'européens, respectez les réglementations sur la protection des données.

Quand utiliser l'API officielle plutôt que le scraping

Scénario API recommandée Raison
Application commerciale à grande échelle API Enterprise Stabilité, support légal, SLA
Recherche académique API Academic (si disponible) Accès complet, citations possibles
Monitoring ponctuel Scraping avec proxies Coût réduit pour volume limité
Dashboard temps réel API Pro Fiabilité, streaming
Preuve de concept / MVP Scraping Validation avant investissement API

Points clés à retenir

  • Les proxies résidentiels sont obligatoires pour scraper X : les IPs datacenter sont immédiatement bloquées.
  • X charge ses données via GraphQL : interceptez ces payloads pour des données structurées plutôt que parser le HTML.
  • Les limites de taux sont agressives : implémentez rotation d'IP, backoff exponentiel, et délais aléatoires.
  • Respectez le cadre légal : données publiques uniquement, respectez robots.txt, limitez vos taux.
  • Considérez l'API officielle pour les projets à long terme nécessitant stabilité et support.

Le scraping de Twitter/X reste techniquement possible avec les bons outils et stratégies. Les proxies résidentiels comme ceux proposés par ProxyHat permettent de distribuer vos requêtes sur des milliers d'adresses IP légitimes, évitant les blocages tout en maintenant un taux de succès élevé.

Pour des cas d'usage plus avancés comme le tracking SERP ou le monitoring de prix e-commerce, les mêmes principes de rotation et de gestion des limites s'appliquent. Consultez notre page de tarification pour trouver le plan adapté à votre volume de requêtes.

Prêt à commencer ?

Accédez à plus de 50M d'IPs résidentielles dans plus de 148 pays avec filtrage IA.

Voir les tarifsProxies résidentiels
← Retour au Blog