En date du 7 octobre 2026, Shifter dispose du plus grand pool d'IP US actives, devant Oxylabs et Bright Data, pour une fraction du coût.Shifter a désormais le plus grand pool d'IP US actives

Voir les benchmarks

Scraping

Améliorer les applications PHP grâce à l'intégration d'une API de web scraping

Ajoutez des données web à une application PHP avec une API de web scraping : un client sans dépendance avec tentatives, extraction structurée et requêtes parallèles via curl_multi.

Chris Collins

19 octobre 2026 · 8 min de lecture

Une grande partie du web tourne encore sur PHP : boutiques, CMS, outils internes et portails clients. Quand une de ces applications a besoin de données provenant d’autres sites, comme des prix concurrents, des détails produits ou des annonces, la solution tentante est de récupérer les pages avec file_get_contents et de les analyser avec des expressions régulières. Ça fonctionne le premier jour et casse le dixième, quand le site bloque l’IP du serveur ou modifie son balisage.

Une API de web scraping élimine la plupart de cette fragilité. Elle récupère les pages via ses propres proxies et navigateurs, relance les échecs et peut retourner uniquement les champs demandés au format JSON. Ce tutoriel construit un petit client PHP pour une telle API, sans dépendance autre que l’extension curl, et montre comment l’utiliser dans une application.

Ce que le client doit faire

Quatre points couvrent la plupart des cas d’usage réels :

  1. Envoyer une requête avec la clé API et l’URL cible.
  2. Demander des données structurées avec des règles d’extraction, pour que l’application n’ait jamais à analyser du HTML.
  3. Relancer les bonnes erreurs et s’arrêter sur celles qui ne réussiront jamais.
  4. Récupérer plusieurs pages en parallèle sans dépasser la limite de concurrence de votre plan.

Si vous n’avez jamais utilisé d’API de scraping auparavant, notre guide pour débutants détaille davantage le format des requêtes et les codes de statut.

Le client

<?php

final class ShifterScraper
{
    private const RETRY = [408, 422, 429, 500];

    public function __construct(
        private string $apiKey,
        private string $endpoint = 'https://scrape.shifter.io/v1',
        private int $attempts = 3,
        private float $backoffSeconds = 2.0,
    ) {}

    /** Fetch one page; with $extractRules, returns the decoded JSON instead of HTML. */
    public function fetch(string $url, array $extractRules = [], bool $renderJs = false): string|array
    {
        $params = ['api_key' => $this->apiKey, 'url' => $url];
        if ($renderJs) {
            $params['render_js'] = 1;
        }
        if ($extractRules) {
            $params['extract_rules'] = json_encode($extractRules);
        }

        for ($attempt = 1; ; $attempt++) {
            [$status, $body] = $this->get($this->endpoint . '?' . http_build_query($params));
            if ($status === 200) {
                return $extractRules ? json_decode($body, true, flags: JSON_THROW_ON_ERROR) : $body;
            }
            if (!in_array($status, self::RETRY, true) || $attempt >= $this->attempts) {
                throw new RuntimeException("HTTP {$status} for {$url}: " . substr($body, 0, 200));
            }
            usleep((int) ($this->backoffSeconds * $attempt * 1_000_000));
        }
    }

    /** Fetch many pages at once with curl_multi, at most $concurrency in flight. */
    public function fetchMany(array $urls, array $extractRules = [], int $concurrency = 10): array
    {
        $results = [];
        foreach (array_chunk($urls, $concurrency) as $batch) {
            $multi = curl_multi_init();
            $handles = [];
            foreach ($batch as $url) {
                $params = ['api_key' => $this->apiKey, 'url' => $url];
                if ($extractRules) {
                    $params['extract_rules'] = json_encode($extractRules);
                }
                $handles[$url] = $this->handle($this->endpoint . '?' . http_build_query($params));
                curl_multi_add_handle($multi, $handles[$url]);
            }
            do {
                curl_multi_exec($multi, $running);
                curl_multi_select($multi);
            } while ($running > 0);
            foreach ($handles as $url => $h) {
                $status = curl_getinfo($h, CURLINFO_RESPONSE_CODE);
                $body = curl_multi_getcontent($h);
                $results[$url] = $status === 200
                    ? ($extractRules ? json_decode($body, true) : $body)
                    : $this->fetchOrNull($url, $extractRules);   // retry failures one by one
                curl_multi_remove_handle($multi, $h);
            }
            curl_multi_close($multi);
        }
        return $results;
    }

    private function fetchOrNull(string $url, array $extractRules): string|array|null
    {
        try {
            return $this->fetch($url, $extractRules);
        } catch (RuntimeException) {
            return null;
        }
    }

    private function handle(string $requestUrl): CurlHandle
    {
        $h = curl_init($requestUrl);
        curl_setopt_array($h, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 120]);
        return $h;
    }

    private function get(string $requestUrl): array
    {
        $h = $this->handle($requestUrl);
        $body = curl_exec($h);
        $status = curl_getinfo($h, CURLINFO_RESPONSE_CODE);
        return [$status, $body === false ? curl_error($h) : $body];
    }
}

Quelques choix de conception à relever :

  • Relances uniquement là où elles aident. Les délais d’attente dépassés (408), les rendus échoués (422), les limitations de débit (429) et les erreurs serveur (500) sont relancés avec une pause croissante. Tout le reste, comme une clé invalide, provoque une exception immédiate.
  • Les règles d’extraction deviennent un tableau. Avec des règles, fetch() retourne du JSON décodé, donc le code appelant travaille avec $product['price'], pas avec du balisage.
  • Requêtes parallèles avec curl_multi. fetchMany() envoie un lot de requêtes en une fois et attend qu’elles se terminent toutes. Les lots sont dimensionnés pour rester sous la limite de concurrence de votre plan. Les pages qui échouent dans le lot obtiennent une seconde chance via fetch() avec ses relances, et reviennent en tant que null si elles échouent encore.

Utilisation

<?php
require __DIR__ . '/ShifterScraper.php';

$scraper = new ShifterScraper(getenv('SHIFTER_SCRAPER_KEY'));

// One page, as structured data instead of HTML
$product = $scraper->fetch('https://shop.example.com/p/123', [
    'name'  => ['selector' => 'h1', 'output' => 'text'],
    'price' => ['selector' => '.price', 'output' => 'text'],
]);

// A batch, ten requests in flight at a time; failed pages come back as null
$pages = $scraper->fetchMany($productUrls, ['name' => ['selector' => 'h1', 'output' => 'text']], 10);

Conservez la clé API dans une variable d’environnement ou dans la configuration de secrets de votre framework, jamais dans le code.

Comment nous l’avons testé

Nous avons exécuté le client contre un serveur PHP local simulant l’API. Il servait des pages normales, une page qui échouait deux fois avec 429 avant de réussir, une qui retournait toujours 401 et une qui dépassait toujours le délai avec 408. Les tests vérifiaient que :

  • le HTML et le JSON extrait revenaient correctement ;
  • la page instable réussissait à sa troisième tentative ;
  • le 401 était tenté une seule fois et le 408 était relancé jusqu’à la limite avant d’échouer ;
  • un lot de 13 pages retournait 12 résultats et un null pour la page en échec.

Les 11 vérifications sont passées sur PHP 8.5.

Intégration dans une application

Ne scrapez pas pendant l’affichage d’une page. Un utilisateur ne devrait jamais attendre qu’un site distant se charge. Exécutez la collecte dans un worker de file d’attente ou une tâche planifiée, et servez les résultats stockés.

Stockez les résultats avec un horodatage. Enregistrez chaque donnée avec son moment de collecte, pour que l’application puisse montrer sa fraîcheur et ne rafraîchir que ce qui est périmé.

Mettez en cache de manière agressive. La plupart des données ne changent pas à la minute. Un rafraîchissement horaire ou quotidien suffit pour les prix et les annonces sur la plupart des sites, et chaque requête évitée fait économiser de l’argent.

Journalisez les échecs. Enregistrez quelles URL reviennent en null et pourquoi. Une hausse soudaine signifie généralement qu’un site a changé sa mise en page et que vos règles d’extraction doivent être mises à jour.

Respectez les limites de votre plan. Réglez la taille des lots sur la limite de concurrence de votre plan ou en dessous. Sur Shifter, cette limite se situe entre 20 et 500 requêtes simultanées, selon le plan.

Quand utiliser des proxies directement à la place

Une API de scraping convient à la plupart des collectes de données : elle gère le rendu, les relances et le choix des IP pour vous. Certaines tâches se prêtent encore mieux à une connexion proxy directe, par exemple quand vous avez besoin d’un contrôle total du client HTTP, ou de sessions longues sur une seule IP. Notre guide sur l’utilisation de proxies résidentiels en PHP avec cURL et Guzzle couvre cette approche.

FAQ

PHP peut-il être utilisé pour le web scraping ?

Oui. L’extension curl de PHP gère bien le HTTP, y compris les requêtes parallèles avec curl_multi. L’associer à une API de web scraping signifie que l’application n’a pas à gérer elle-même les proxies, les navigateurs ou les blocages.

Ai-je besoin de Guzzle pour appeler une API de web scraping depuis PHP ?

Non. Le client de ce guide utilise uniquement l’extension curl intégrée. Guzzle fonctionne aussi si votre application l’utilise déjà.

Comment scraper des pages rendues en JavaScript depuis PHP ?

Demandez à l’API de rendre la page en passant render_js, que le client expose comme paramètre. L’API exécute un navigateur et retourne la page rendue, donc PHP n’a jamais besoin d’un navigateur headless.

Combien de requêtes puis-je envoyer à la fois ?

Autant que votre plan d’API de scraping le permet. Dimensionnez les lots de fetchMany() à cette limite ou en dessous.

En résumé

Une application PHP peut utiliser des données web sans construire d’infrastructure de scraping : un petit client basé sur curl, des règles d’extraction au lieu de l’analyse HTML, des relances uniquement pour les erreurs qui peuvent réussir, et des lots parallèles dimensionnés selon votre plan. Exécutez-le depuis une file d’attente, stockez les résultats avec des horodatages, et l’application obtient des données fraîches sans jamais faire attendre un utilisateur pour le site web de quelqu’un d’autre.

Prêt à commencer ?

Essayez les proxies résidentiels de Shifter, 205M+ IPs, 195+ pays, à partir de 0,75 $/GB.

Commencer