Retour au blog
Guides
Sorin-Gabriel MaricaLast updated on Apr 30, 202620 min read

Web Scraping avec PHP : Un guide pratique des bibliothèques, du code et des meilleures pratiques

Web Scraping avec PHP : Un guide pratique des bibliothèques, du code et des meilleures pratiques
En bref : PHP est un langage tout à fait adapté au web scraping, grâce à des extensions intégrées telles que cURL et DOMDocument, ainsi qu’à un riche écosystème Composer comprenant Guzzle, Symfony DomCrawler et Symfony Panther pour la navigation en mode headless. Ce guide vous accompagne tout au long du processus : récupération des pages, analyse du code HTML, stockage des résultats au format CSV/JSON/MySQL, gestion des erreurs et contournement des blocages.

Le web scraping avec PHP consiste à récupérer des pages web par programmation et à extraire des données structurées de leur code HTML à l'aide de scripts et de bibliothèques PHP. Si vous écrivez déjà du code PHP dans le cadre de votre travail quotidien, il n'y a aucune raison de changer de langage juste pour extraire des données de sites web. PHP est livré avec des liaisons cURL et un parseur DOM intégré prêts à l'emploi, et Composer vous donne accès à des clients HTTP éprouvés, à des moteurs de sélection CSS et même à des navigateurs sans interface graphique.

Ce tutoriel s'adresse aux développeurs PHP de niveau intermédiaire qui souhaitent un guide pratique axé sur le code. Vous commencerez par des appels cURL de bas niveau, passerez à des bibliothèques de plus haut niveau comme Guzzle et Symfony HttpBrowser, aborderez les pages rendues en JavaScript avec Symfony Panther, et terminerez par des aspects liés à la production tels que le stockage des données, la gestion des erreurs et la prévention de l'ajout aux listes de blocage. Chaque exemple de ce tutoriel de web scraping en PHP suit un scénario unique (le scraping d’un site public de listes de livres) afin que vous puissiez suivre le flux de travail complet de bout en bout plutôt que de passer d’un extrait de code isolé à un autre.

Pourquoi PHP est un excellent choix pour le web scraping

PHP n'est peut-être pas le premier langage qui vient à l'esprit lorsqu'on pense au scraping, mais il présente plusieurs avantages pratiques. Premièrement, si votre pile existante fonctionne déjà sous PHP, l'ajout d'un scraper n'implique aucune nouvelle dépendance d'exécution. Votre équipe peut maintenir le code, votre pipeline de déploiement reste inchangé et vous évitez la charge cognitive liée au changement de contexte vers un autre langage.

Deuxièmement, les extensions intégrées de PHP sont étonnamment bien adaptées à cette tâche. L' curl gère les requêtes HTTP, dom et libxml vous offre un analyseur HTML/XML conforme aux normes, et mbstring résout les problèmes d’encodage des caractères. Vous n’avez rien à installer de plus pour un scraping de base.

Troisièmement, l'écosystème Composer comble toutes les lacunes restantes. Guzzle fournit un client HTTP moderne avec prise en charge des middlewares. Symfony DomCrawler ajoute des requêtes de sélecteurs CSS à DOMDocument. Symfony Panther pilote une véritable instance de Chrome ou de Firefox pour les pages riches en JavaScript. Les outils sont matures et activement maintenus.

Qu'en est-il de PHP par rapport à Python pour le scraping ? Python dispose d'une communauté plus importante dédiée au scraping et de bibliothèques telles que Beautiful Soup et Scrapy, mais cela ne fait pas de PHP un mauvais choix. Si PHP est le langage que vous maîtrisez le mieux, vous écrirez un scraper fonctionnel plus rapidement que si vous utilisiez un langage que vous êtes encore en train d'apprendre. Le meilleur langage de scraping est celui que vous pouvez déboguer à 2 heures du matin.

Aperçu des bibliothèques de scraping PHP

Avant d'écrire du code, il est utile de savoir quels outils existent et quand utiliser chacun d'entre eux. Le tableau ci-dessous compare les principales bibliothèques de scraping PHP selon les critères les plus importants : ce qu'elles font, si elles gèrent JavaScript et le temps nécessaire pour les maîtriser.

Bibliothèque / Outil

Objectif

Prise en charge de JS

Courbe d'apprentissage

État de maintenance

cURL (ext-curl)

Requêtes HTTP de bas niveau

Non

Faible

Intégré, toujours disponible

Guzzle

Client HTTP avec middleware, asynchrone

Non

Faible à moyen

Maintenu activement

DOMDocument + DOMXPath

Analyse HTML/XML, requêtes XPath

Non

Moyen

Intégré

Symfony DomCrawler

Sélecteur CSS et requêtes XPath

Non

Faible

Maintenu activement

Goutte (obsolète)

Exploration combinée HTTP + DOM

Non

Faible

Obsolète, utilisez HttpBrowser

Symfony HttpBrowser

Successeur de Goutte, même API

Non

Faible

Maintenu activement

Symfony Panther

Navigateur sans interface graphique (Chrome/Firefox)

Oui

Moyen à élevé

Maintenu activement

Service d'API de scraping

Couche de gestion des requêtes et d'analyse

Dépend du fournisseur

Très faible

Géré en externe

Quelques points à noter. Goutte a été pendant des années la bibliothèque de scraping « tout-en-un » de référence, mais elle est désormais obsolète. À l'heure où nous écrivons ces lignes, la migration recommandée est vers Symfony HttpBrowser, qui fournit une API presque identique s'appuyant sur les composants BrowserKit et HttpClient de Symfony. Si vous démarrez un nouveau projet, ignorez complètement Goutte et passez directement à HttpBrowser.

Pour la plupart des tâches de scraping de pages statiques, Guzzle (pour la récupération) associé à Symfony DomCrawler (pour l'analyse) constitue une combinaison solide et légère. Réservez Symfony Panther aux pages qui nécessitent véritablement l'exécution de JavaScript, car le lancement d'un navigateur sans interface graphique est nettement plus lent et plus gourmand en ressources.

Configuration de votre environnement de scraping PHP

Commençons par les prérequis. Vous avez besoin de PHP 8.1 ou plus récent (pour la prise en charge des énumérations et des fibres dans les bibliothèques modernes), de Composer et de quelques extensions.

Vérifiez votre version de PHP et les extensions chargées :

php -v
php -m | grep -E 'curl|dom|mbstring|json'

Si l'une de ces quatre extensions manque, activez-la dans votre php.ini ou installez-les via le gestionnaire de paquets de votre système (par exemple, sudo apt install php-curl php-xml php-mbstring sur Debian/Ubuntu).

Ensuite, créez un répertoire de projet et récupérez les bibliothèques que vous utiliserez tout au long de ce tutoriel :

mkdir php-scraper && cd php-scraper
composer init --no-interaction
composer require guzzlehttp/guzzle symfony/dom-crawler symfony/css-selector symfony/browser-kit symfony/http-client

Cette seule composer require ligne vous fournit Guzzle pour HTTP, DomCrawler pour l'analyse syntaxique et Symfony HttpBrowser pour le workflow de crawling combiné. Nous ajouterons Symfony Panther plus tard lorsque nous aurons besoin d'un support pour les navigateurs sans interface graphique.

Créez un scrape.php fichier et ajoutez l'autochargeur Composer en haut :

<?php
require __DIR__ . '/vendor/autoload.php';

Vous êtes prêt à récupérer votre première page.

Récupération de pages avec cURL

L'extension cURL de PHP est l'outil HTTP de plus bas niveau de votre boîte à outils. Elle est verbeuse, mais elle vous offre un contrôle total sur chaque détail de la requête, ce qui est utile lorsque vous devez imiter l'empreinte digitale d'un navigateur spécifique ou déboguer des problèmes de connexion.

Voici une requête GET basique qui récupère la page d'accueil d'un catalogue de livres public (nous utiliserons http://books.toscrape.com comme cible de démonstration tout au long de cet article) :

$ch = curl_init();
curl_setopt_array($ch, [
    CURLOPT_URL            => 'http://books.toscrape.com/',
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_HTTPHEADER     => [
        'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
        'Accept-Language: en-US,en;q=0.9',
    ],
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_COOKIEJAR      => '/tmp/cookies.txt',
    CURLOPT_COOKIEFILE     => '/tmp/cookies.txt',
]);

$html = curl_exec($ch);

if (curl_errno($ch)) {
    echo 'cURL error: ' . curl_error($ch);
}

curl_close($ch);

Quelques points à noter. CURLOPT_COOKIEJAR et CURLOPT_COOKIEFILE permettent la persistance des cookies d'une requête à l'autre, ce qui est essentiel pour les flux de scraping en plusieurs étapes où le serveur suit l'état de la session. Définir un User-Agent en-tête donne à votre requête l'apparence d'un trafic de navigateur ordinaire plutôt que d'un simple script PHP. De plus, CURLOPT_FOLLOWLOCATION gère automatiquement les redirections 301/302, vous évitant ainsi de devoir les suivre manuellement.

Pour une requête POST (par exemple, l'envoi d'un formulaire de recherche), remplacez par CURLOPT_POST => true et ajoutez CURLOPT_POSTFIELDS par les données de votre formulaire. Le reste du code standard reste inchangé.

cURL fonctionne, mais il est suffisamment bas niveau pour que vous finissiez par écrire des wrappers pour les en-têtes, les tentatives de reconnexion et la gestion des erreurs. C'est là que Guzzle entre en jeu.

Récupération de pages avec Guzzle

Guzzle encapsule la couche cURL (ou stream) de PHP dans une API propre et orientée objet. Installez-le via Composer si ce n'est pas déjà fait, puis récupérez la même page :

use GuzzleHttp\Client;

$client = new Client([
    'timeout' => 30,
    'headers' => [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
        'Accept-Language' => 'en-US,en;q=0.9',
    ],
]);

$response = $client->get('http://books.toscrape.com/');
$html = (string) $response->getBody();

Le code standard est nettement réduit. Guzzle vous offre également des hooks de middleware pour la journalisation, la logique de réessai et l'injection d'en-têtes, ce qui signifie que vous pouvez centraliser les préoccupations transversales au lieu de disperser curl_setopt partout.

Requêtes simultanées avec les promesses Guzzle

Lorsque vous devez extraire plusieurs pages, envoyer les requêtes une par une est extrêmement lent. Guzzle prend en charge la concurrence basée sur les promesses via sa Pool , qui vous permet d'envoyer plusieurs requêtes en parallèle tout en contrôlant le niveau de concurrence.

use GuzzleHttp\Client;
use GuzzleHttp\Pool;
use GuzzleHttp\Psr7\Request;

$client = new Client(['timeout' => 30]);

$urls = [
    'http://books.toscrape.com/catalogue/page-1.html',
    'http://books.toscrape.com/catalogue/page-2.html',
    'http://books.toscrape.com/catalogue/page-3.html',
];

$requests = function () use ($urls) {
    foreach ($urls as $url) {
        yield new Request('GET', $url);
    }
};

$pool = new Pool($client, $requests(), [
    'concurrency' => 5,
    'fulfilled'   => function ($response, $index) {
        echo "Page $index fetched: " . $response->getStatusCode() . "\n";
    },
    'rejected'    => function ($reason, $index) {
        echo "Page $index failed: " . $reason->getMessage() . "\n";
    },
]);

$pool->promise()->wait();

Avec un niveau de concurrence de 5, Guzzle envoie jusqu’à cinq requêtes simultanément au lieu d’attendre que chacune d’entre elles soit terminée. Pour un scraping de 50 pages, cela peut réduire le temps d’exécution total de quelques minutes à quelques secondes. Selon la documentation de Guzzle sur les requêtes concurrentes, l’API Pool utilise le multi-handle de cURL en arrière-plan, de sorte que le gain de performance est réel, et pas seulement une simplification syntaxique.

Analyse du HTML : DOMDocument et XPath

Une fois que vous disposez du code HTML brut sous forme de chaîne, vous devez en extraire les données structurées. La classe intégrée DOMDocument charge le HTML dans une structure arborescente, et DOMXPath vous permet d'interroger cet arbre à l'aide d'expressions XPath.

libxml_use_internal_errors(true); // suppress malformed-HTML warnings

$doc = new DOMDocument();
$doc->loadHTML($html);

$xpath = new DOMXPath($doc);

// Select every book title on the page
$titles = $xpath->query('//article[@class="product_pod"]//h3/a/@title');

foreach ($titles as $node) {
    echo $node->nodeValue . "\n";
}

L' libxml_use_internal_errors(true) appel est important. Le code HTML réel n'est presque jamais du XML valide, et sans ce drapeau, PHP émettra des avertissements pour chaque balise non fermée ou attribut mal apparié. Les supprimer vous permet d'analyser des pages désordonnées sans saturer vos journaux.

XPath est puissant pour les requêtes complexes. Vous souhaitez récupérer tous les livres dont le prix est inférieur à 20 £ ? Vous pouvez combiner des axes et des prédicats :

$products = $xpath->query('//article[@class="product_pod"]');

foreach ($products as $product) {
    $title = $xpath->query('.//h3/a/@title', $product)->item(0)->nodeValue;
    $price = $xpath->query('.//p[@class="price_color"]', $product)->item(0)->textContent;

    $numericPrice = (float) str_replace('£', '', $price);
    if ($numericPrice < 20.00) {
        echo "$title: $price\n";
    }
}

DOMDocument associé à XPath vous offre un contrôle total et aucune dépendance externe. Le compromis réside dans la verbosité : même une simple requête nécessite plusieurs lignes de configuration. C’est là que Symfony DomCrawler prend tout son sens.

Analyse du HTML : Symfony DomCrawler et les sélecteurs CSS

Symfony DomCrawler s'appuie sur DOMDocument mais expose une API beaucoup plus conviviale. Au lieu d'écrire du code XPath à la main, vous pouvez utiliser des sélecteurs CSS (que la plupart des développeurs web connaissent déjà) et enchaîner les méthodes à la manière de jQuery.

use Symfony\Component\DomCrawler\Crawler;

$crawler = new Crawler($html);

$crawler->filter('article.product_pod')->each(function (Crawler $node) {
    $title = $node->filter('h3 a')->attr('title');
    $price = $node->filter('.price_color')->text();
    echo "$title: $price\n";
});

Comparez cela à la version DOMXPath ci-dessus. L'intention est identique, mais le code DomCrawler est deux fois plus court et plus facile à lire. La filter() méthode accepte tout sélecteur CSS valide, text() renvoie le contenu textuel et attr() extrait la valeur d'un attribut.

Quand faut-il utiliser les sélecteurs CSS plutôt que XPath pour le scraping ? Les sélecteurs CSS couvrent 90 % des cas pratiques et sont plus intuitifs pour quiconque écrit du code front-end. XPath l'emporte lorsque vous devez remonter la hiérarchie (sélectionner un parent en fonction du texte d'un enfant), effectuer des fonctions de chaîne de caractères à l'intérieur de la requête ou naviguer entre les nœuds frères. Une bonne règle de base : commencez par les sélecteurs CSS et passez à XPath uniquement lorsque le CSS ne permet pas d'exprimer ce dont vous avez besoin.

Pourquoi l'utilisation de l'expression régulière (Regex) est risquée pour l'analyse HTML

Il est tentant de se tourner vers preg_match() lorsque vous n'avez besoin que d'une seule valeur d'une page. Résistez à cette envie. Le HTML n'est pas un langage régulier, et l'extraction basée sur les expressions régulières échoue dès que le balisage subit des modifications mineures : un nouvel attribut, un changement de style de guillemets ou un espace supplémentaire.

// Fragile — breaks if class order changes or attributes are added
preg_match('/<h3 class="title">(.+?)<\/h3>/', $html, $match);

Un parseur DOM gère toutes ces variations avec aisance. Réservez les expressions régulières au texte véritablement plat (fichiers journaux, lignes CSV) et utilisez DOMDocument ou DomCrawler pour tout ce qui provient d'un document HTML.

Construire un scraper complet avec Goutte et son successeur

Goutte était la bibliothèque qui rendait le web scraping en PHP accessible. Elle combinait le client HTTP de Guzzle avec DomCrawler de Symfony en une seule classe, vous permettant de récupérer et d'analyser en un seul appel. Cependant, Goutte a été officiellement dépréciée. Ses mainteneurs recommandent de migrer vers Symfony HttpBrowser, qui est fourni dans le composant Symfony BrowserKit et offre une API presque identique.

Voici un scraper complet construit avec Symfony HttpBrowser qui récupère des listes de livres sur plusieurs pages :

use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\BrowserKit\HttpBrowser;

$browser = new HttpBrowser(HttpClient::create([
    'headers' => [
        'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
    ],
]));

$books = [];
$url = 'http://books.toscrape.com/catalogue/page-1.html';

while ($url) {
    $crawler = $browser->request('GET', $url);

    $crawler->filter('article.product_pod')->each(function ($node) use (&$books) {
        $books[] = [
            'title' => $node->filter('h3 a')->attr('title'),
            'price' => $node->filter('.price_color')->text(),
            'stock' => trim($node->filter('.availability')->text()),
        ];
    });

    // Follow the "next" pagination link, or stop
    $nextLink = $crawler->filter('li.next a');
    $url = $nextLink->count() > 0
        ? 'http://books.toscrape.com/catalogue/' . $nextLink->attr('href')
        : null;
}

echo count($books) . " books collected.\n";

Remarquez comment fonctionne la logique de pagination. Après avoir analysé chaque page, le scraper vérifie si un lien « suivant » existe. Si c'est le cas, le scraper le suit et répète le processus. Sinon, $url est défini sur null et la boucle s'arrête. Ce modèle est réutilisable pour toute liste paginée.

La migration depuis Goutte est minime. Si votre code existant utilise $goutte = new \Goutte\Client(), remplacez-le par $browser = new HttpBrowser(HttpClient::create()). Le request(), filter()et selectLink() méthodes restent inchangées. La couche HTTP sous-jacente passe de Guzzle à Symfony HttpClient, ce qui vous offre une prise en charge native de l'asynchronisme et une meilleure intégration avec le reste de l'écosystème Symfony.

Autre avantage de HttpBrowser : il suit automatiquement les cookies et les sessions d'une requête à l'autre. Lorsque vous appelez $browser->request() à plusieurs reprises, le client se comporte comme une véritable session de navigateur, en conservant les cookies sans configuration supplémentaire.

Extraction de pages rendues par JavaScript avec Symfony Panther

Les scrapers de pages statiques échouent lorsque le contenu dont vous avez besoin est injecté par JavaScript après le chargement initial de la page. Les applications monopages, les flux à défilement infini et les grilles de produits à chargement différé nécessitent tous un véritable moteur de navigateur pour s’afficher. Symfony Panther comble cette lacune en pilotant Chrome ou Firefox via le protocole WebDriver.

Installez Panther et un binaire ChromeDriver :

composer require symfony/panther
# Panther can auto-detect a locally installed ChromeDriver,
# or you can install one explicitly:
composer require dbrekelmans/bdi
vendor/bin/bdi detect drivers

Extrayez maintenant une page qui repose sur un rendu de contenu dynamique avec PHP :

use Symfony\Component\Panther\Client as PantherClient;

$panther = PantherClient::createChromeClient();
$crawler = $panther->request('GET', 'https://example.com/dynamic-page');

// Wait until the data container is visible in the DOM
$panther->waitFor('.results-container', 10);

$crawler->filter('.results-container .item')->each(function ($node) {
    echo $node->filter('.item-title')->text() . "\n";
});

$panther->quit();

La waitFor() méthode met l'exécution en pause jusqu'à ce que le sélecteur CSS spécifié apparaisse dans le DOM rendu, avec un délai d'expiration (10 secondes ici) pour éviter les blocages infinis. Ceci est essentiel pour le scraping de contenu dynamique avec PHP, car le code HTML dont vous avez besoin peut ne pas exister du tout dans la réponse initiale.

Panther est puissant mais coûteux. Chaque requête lance un véritable processus de navigateur, consommant de la mémoire et du CPU. Ne l'utilisez que lorsque le rendu JavaScript est véritablement nécessaire. Pour les pages qui chargent des données via un simple appel XHR/API, il est souvent plus rapide de trouver ce point de terminaison API dans l'onglet Réseau de votre navigateur et de l'appeler directement avec Guzzle.

Utilisation d'une API de scraping pour une extraction sans intervention

À un certain stade, le coût technique lié à la maintenance de votre propre scraper (rotation de proxys, résolution de CAPTCHA, empreinte digitale du navigateur, logique de réessai) dépasse le coût de l'externalisation de cette infrastructure vers un service dédié. C'est là que l'API de scraping prend tout son sens.

Le modèle d'intégration est simple. Vous envoyez une URL au point de terminaison de l'API, et celle-ci renvoie le code HTML de la page (ou un JSON structuré) avec toute la gestion anti-bot effectuée côté serveur :

$client = new \GuzzleHttp\Client();

$response = $client->get('https://api.webscrapingapi.com/v1', [
    'query' => [
        'api_key' => 'YOUR_API_KEY',
        'url'     => 'http://books.toscrape.com/',
    ],
]);

$html = (string) $response->getBody();
// Parse $html with DomCrawler as usual

Quand une API de scraping est-elle préférable à une approche « DIY » ? Envisagez-la lorsque vous effectuez du scraping à grande échelle (des milliers de pages par jour), que vous ciblez des sites dotés de défenses anti-bot agressives, ou lorsque votre équipe n'a pas le temps de gérer les pools de proxys et l'infrastructure de navigation. Le compromis réside dans le rapport entre le coût par requête et les heures d'ingénierie.

Un service géré se distingue également par la réduction de la charge de maintenance. Lorsqu'un site cible modifie sa pile anti-bot, le fournisseur d'API de scraping met à jour son infrastructure. Votre code reste inchangé. Si vous évaluez différentes options, recherchez un fournisseur qui ne facture que les réponses réussies afin de ne pas payer pour les requêtes ayant échoué.

Stockage des données extraites : CSV, JSON et MySQL

La collecte des données ne représente que la moitié du travail. Vous devez les stocker dans un format exploitable par les processus en aval (analyses, pipelines d'apprentissage automatique, tableaux de bord).

Le CSV est l'option la plus simple et fonctionne bien pour les données plates et tabulaires :

$fp = fopen('books.csv', 'w');
fputcsv($fp, ['Title', 'Price', 'Stock']); // header row

foreach ($books as $book) {
    fputcsv($fp, [$book['title'], $book['price'], $book['stock']]);
}

fclose($fp);

JSON préserve les structures imbriquées et est plus facile à importer dans les API et les bases de données NoSQL :

file_put_contents(
    'books.json',
    json_encode($books, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
);

MySQL via PDO est le bon choix lorsque vous avez besoin d'un stockage relationnel interrogeable :

$pdo = new PDO('mysql:host=127.0.0.1;dbname=scraper', 'user', 'pass', [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);

$stmt = $pdo->prepare(
    'INSERT INTO books (title, price, stock) VALUES (:title, :price, :stock)'
);

foreach ($books as $book) {
    $stmt->execute([
        ':title' => $book['title'],
        ':price' => $book['price'],
        ':stock' => $book['stock'],
    ]);
}

L'utilisation de requêtes préparées avec PDO n'est pas facultative. Elle vous protège contre les injections SQL, qui constituent un risque réel lors de l'insertion de texte généré par l'utilisateur ou extrait de sources externes dans une base de données.

Pour les données orientées documents ou les schémas qui changent fréquemment, MongoDB est une autre option viable. Le mongodb/mongodb package Composer fournit une méthode simple insertMany() qui accepte directement des tableaux de tableaux associatifs. Le choix entre un stockage relationnel et un stockage de documents dépend de la structure de vos données extraites et de ce qui les utilisera.

Gestion des erreurs, tentatives de reprise et journalisation

Un scraper qui fonctionne sur votre ordinateur portable n’est pas le même qu’un scraper qui s’exécute de manière fiable en production. Les délais d’attente réseau, les réponses 5xx, les réinitialisations de connexion et les erreurs de limitation de débit sont inévitables lorsque vous effectuez des milliers de requêtes HTTP. Intégrer la résilience dans votre scraper dès le départ vous évite une perte de données silencieuse.

Enveloppez chaque appel HTTP dans un try-catch avec un back-off exponentiel :

function fetchWithRetry(\GuzzleHttp\Client $client, string $url, int $maxRetries = 3): string
{
    for ($attempt = 1; $attempt <= $maxRetries; $attempt++) {
        try {
            $response = $client->get($url);
            return (string) $response->getBody();
        } catch (\GuzzleHttp\Exception\GuzzleException $e) {
            if ($attempt === $maxRetries) {
                throw $e;
            }
            $wait = (int) pow(2, $attempt); // 2s, 4s, 8s
            sleep($wait);
        }
    }
}

Pour la journalisation structurée, Monolog est la norme de facto dans l'écosystème PHP. L'ajout d'un gestionnaire de fichiers rotatifs ne nécessite que deux lignes :

use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;

$log = new Logger('scraper');
$log->pushHandler(new RotatingFileHandler('logs/scraper.log', 7, Logger::INFO));

$log->info('Fetching page', ['url' => $url]);
$log->error('Request failed', ['url' => $url, 'error' => $e->getMessage()]);

Enregistrez l'URL de chaque requête, le code d'état et toute exception. Lorsqu'une tâche de scraping échoue à la page 847 sur 1 000, les journaux sont la seule chose qui vous indiquera ce qui s'est passé. C'est ce souci de la préparation à la production qui distingue un prototype d'un pipeline fiable.

Éviter les blocages : proxys, en-têtes et limitation de débit

Les sites web n'apprécient pas que des bots saturent leurs serveurs. Si votre scraper envoie des centaines de requêtes identiques par minute depuis une seule adresse IP, attendez-vous à être bloqué. Un scraping respectueux est à la fois une obligation éthique et une nécessité pratique pour les projets de longue durée.

Faites tourner les chaînes User-Agent afin que chaque requête ne soit pas identifiée comme provenant du même client :

$userAgents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 13_5) AppleWebKit/605.1.15',
    'Mozilla/5.0 (X11; Linux x86_64; rv:115.0) Gecko/20100101 Firefox/115.0',
];

$headers = ['User-Agent' => $userAgents[array_rand($userAgents)]];

Ajoutez des délais aléatoires entre les requêtes pour éviter les schémas temporels prévisibles :

function politeDelay(int $minMs = 1000, int $maxMs = 3000): void
{
    usleep(random_int($minMs, $maxMs) * 1000);
}

Respectez robots.txt par programmation. Avant de scraper un domaine, récupérez son robots.txt et vérifiez si votre chemin d'accès cible est interdit. Vous pouvez analyser cela manuellement ou utiliser une bibliothèque telle que spatie/robots-txt:

// Pseudocode — check before scraping
$robots = file_get_contents('http://example.com/robots.txt');
if (str_contains($robots, 'Disallow: /private/')) {
    echo "Skipping disallowed path.\n";
}

La rotation des proxys est la défense la plus efficace contre le blocage basé sur l'adresse IP. Si vous effectuez un scraping à un volume significatif, acheminer les requêtes via un pool de proxys résidentiels rend votre trafic pratiquement impossible à distinguer de celui des utilisateurs organiques. Vous pouvez configurer Guzzle pour utiliser un proxy avec une seule option :

$client = new \GuzzleHttp\Client([
    'proxy' => 'http://user:pass@proxy-host:port',
]);

La combinaison de toutes ces techniques (en-têtes variés, délais de courtoisie, respect du fichier robots.txt et rotation des proxys) vous offre les meilleures chances d'effectuer un scraping fiable sans être signalé.

Considérations juridiques et éthiques

Le web scraping se situe dans une zone grise juridique qui varie selon les juridictions. Quelques principes s'appliquent de manière générale.

Le fichier robots.txt est une norme volontaire, et non un contrat légal, mais l'ignorer affaiblit tout argument de bonne foi que vous pourriez avancer en cas de contestation. Considérez-le comme une règle de base que vous respectez toujours.

Les conditions d'utilisation du site cible peuvent interdire explicitement l'accès automatisé. Enfreindre ces conditions peut vous exposer à des poursuites pour rupture de contrat, en particulier aux États-Unis depuis des affaires telles que hiQ Labs c. LinkedIn, qui ont clarifié que le scraping de données accessibles au public ne constitue pas nécessairement une violation du Computer Fraud and Abuse Act, mais n'ont pas abordé la question de l'application des conditions d'utilisation.

Le RGPD s'applique si vous récupérez des données à caractère personnel appartenant à des résidents de l'UE (noms, adresses e-mail, détails de profil). En vertu du RGPD, le web scraping peut constituer un traitement de données, ce qui signifie que vous devez disposer d'une base légale (généralement un intérêt légitime) et traiter ces données conformément aux exigences du RGPD : limitation de la finalité, minimisation de la conservation et respect des demandes d'accès des personnes concernées. En cas de doute, consultez un professionnel du droit, en particulier si votre scraping cible du contenu généré par les utilisateurs.

Les principes éthiques de base sont simples : ne procédez pas au scraping à un rythme qui nuit aux performances du site cible, ne collectez pas de données pour lesquelles vous n'avez pas d'utilisation légitime, et soyez transparent quant à vos intentions lorsque cela est possible.

Points clés

  • Choisissez l'outil adapté au type de page. Utilisez Guzzle et DomCrawler pour le HTML statique, Symfony Panther pour le contenu rendu en JavaScript, et une API de scraping lorsque l'infrastructure anti-bot dépasse les capacités de votre configuration maison.
  • Goutte est obsolète. Lancez vos nouveaux projets avec Symfony HttpBrowser, qui offre le même workflow de crawling, soutenu par des composants Symfony activement maintenus.
  • Renforcez la résilience dès le premier jour. Les tentatives de reconnexion avec délai d'attente exponentiel, la journalisation structurée et la validation des entrées ne sont pas facultatives dans les scrapers de production.
  • Stockez les données dans le format dont vos consommateurs en aval ont besoin. CSV pour une analyse rapide, JSON pour les API et les bases de données de documents, MySQL/PDO pour les requêtes relationnelles.
  • Effectuez le scraping de manière courtoise et légale. Faites tourner les en-têtes et les proxys, respectez robots.txt, ajoutez des délais entre les requêtes et comprenez les implications du RGPD en matière de collecte de données personnelles.

FAQ

Quel langage est le mieux adapté aux projets de scraping web : PHP ou Python ?

Aucun des deux n'est objectivement supérieur. Python dispose d'un écosystème de scraping plus vaste (Beautiful Soup, Scrapy, liaisons Selenium), ce qui signifie plus de tutoriels et de réponses de la communauté. PHP intègre de solides extensions HTTP et DOM, et les bibliothèques Composer telles que Guzzle et DomCrawler sont de qualité production. Choisissez le langage que votre équipe maîtrise le mieux. Un scraper PHP bien écrit surpassera à chaque fois un scraper Python mal entretenu.

PHP peut-il scraper des applications monopages riches en JavaScript ?

Oui, mais vous avez besoin d'un navigateur sans interface graphique. Symfony Panther contrôle Chrome ou Firefox via le protocole WebDriver et peut afficher des pages entièrement dynamiques. Pour les cas plus simples où la page récupère des données depuis un point de terminaison XHR, vous pouvez ignorer complètement le navigateur et appeler directement ce point de terminaison API avec un client HTTP, ce qui est plus rapide et utilise moins de ressources.

Le web scraping est-il légal, et comment s'applique le RGPD ?

La légalité dépend de la juridiction, des conditions d'utilisation du site cible et du type de données collectées. Le scraping de données non personnelles accessibles au public est généralement autorisé dans de nombreuses juridictions. Le RGPD s'applique lorsque vous traitez des données à caractère personnel de résidents de l'UE, ce qui nécessite une base légale telle qu'un intérêt légitime. Vérifiez toujours les conditions d'utilisation du site cible et consultez un conseiller juridique avant de procéder au scraping de données à caractère personnel à grande échelle.

Comment éviter que mon adresse IP soit bloquée lors du scraping avec PHP ?

Combinez plusieurs techniques : alternez les chaînes User-Agent, ajoutez des délais aléatoires entre les requêtes (1 à 3 secondes est une fourchette raisonnable), respectez les robots.txt directives et acheminer le trafic via un pool de proxys tournants. Évitez d'envoyer des rafales de requêtes à partir d'une seule adresse IP. Si vous effectuez un scraping à haut débit, un service de proxy géré ou d'API de scraping se chargera de la rotation et de la protection contre la détection à votre place.

Comment gérer les pages protégées par connexion lors d'un scraping avec PHP ?

Envoyez les identifiants via une requête POST (ou via un formulaire avec Symfony HttpBrowser) et conservez le cookie de session résultant pour les requêtes suivantes. Avec HttpBrowser, les cookies de session sont automatiquement conservés. Avec cURL brut, définissez CURLOPT_COOKIEJAR et CURLOPT_COOKIEFILE sur le même chemin. Vérifiez toujours que votre connexion n'a pas déclenché de CAPTCHA ou de vérification à deux facteurs, et sachez que le scraping derrière une connexion peut avoir des implications juridiques plus strictes selon les conditions d'utilisation du site.

Conclusion

Le scraping Web avec PHP est un workflow pratique et bien pris en charge une fois que vous savez quelles bibliothèques utiliser. Commencez par cURL ou Guzzle pour la récupération, ajoutez DomCrawler ou DOMXPath pour l'analyse, et passez à Symfony Panther uniquement lorsque le rendu JavaScript est inévitable. Conservez vos données dans le format attendu par vos utilisateurs, intégrez une logique de réessai et de journalisation à l'ensemble, et effectuez toujours le scraping de manière respectueuse.

Les exemples de ce tutoriel couvrent l'ensemble du cycle de vie : de la requête HTTP brute à la gestion de la pagination, en passant par la récupération simultanée, le stockage des données et les stratégies anti-blocage. Chaque technique correspond à un véritable enjeu de production, et non à une simple démonstration théorique.

Si vous passez plus de temps à lutter contre les défenses anti-bot qu'à écrire de la logique de parsing, il peut être judicieux de confier l'infrastructure de requêtes à un service tel que l'API Scraper de WebScrapingAPI, qui gère la rotation des proxys, les CAPTCHA et les tentatives de reprise, afin que vous puissiez vous concentrer sur le code d'extraction de données qui compte vraiment.

À propos de l'auteur
Sorin-Gabriel Marica, Développeur full-stack @ WebScrapingAPI
Sorin-Gabriel MaricaDéveloppeur full-stack

Sorin Marica est ingénieur Full Stack et DevOps chez WebScrapingAPI ; il développe des fonctionnalités pour les produits et assure la maintenance de l'infrastructure qui garantit le bon fonctionnement de la plateforme.

Commencez à créer

Prêt à faire évoluer votre système de collecte de données ?

Rejoignez plus de 2 000 entreprises qui utilisent WebScrapingAPI pour extraire des données Web à l'échelle de l'entreprise, sans aucun coût d'infrastructure.