Mit Stand 7. Oktober 2026 verfügt Shifter über den größten Pool an aktiven US-IPs, mehr als Oxylabs und Bright Data, und das zu einem Bruchteil der Kosten.Shifter hat jetzt den größten Pool aktiver US-IPs

Benchmarks ansehen

Scraping

PHP-Anwendungen mit der Integration einer Web-Scraping-API verbessern

Webdaten mit einer Web-Scraping-API in eine PHP-Anwendung einbinden: ein abhängigkeitsfreier Client mit Wiederholungsversuchen, strukturierter Extraktion und parallelen Anfragen über curl_multi.

Chris Collins

19. Oktober 2026 · 7 Min. Lesezeit

Ein Großteil des Webs läuft immer noch auf PHP: Shops, CMS, interne Tools und Kundenportale. Wenn eine dieser Anwendungen Daten von anderen Seiten benötigt, etwa Konkurrenzpreise, Produktdetails oder Listings, liegt der naheliegende Weg darin, Seiten mit file_get_contents abzurufen und mit regulären Ausdrücken zu parsen. Das funktioniert am ersten Tag und bricht am zehnten Tag, wenn die Seite die IP des Servers blockiert oder ihr Markup ändert.

Eine Web-Scraping-API nimmt dieser Vorgehensweise den Großteil ihrer Zerbrechlichkeit. Sie ruft Seiten über eigene Proxys und Browser ab, wiederholt fehlgeschlagene Anfragen und kann genau die Felder, die man anfragt, als JSON zurückgeben. Dieses Tutorial baut einen kleinen PHP-Client für eine solche API, ohne Abhängigkeiten über die curl-Erweiterung hinaus, und zeigt, wie man ihn in einer Anwendung einsetzt.

Was der Client können muss

Vier Dinge decken die meisten realen Anwendungsfälle ab:

  1. Eine Anfrage senden mit dem API-Schlüssel und der Ziel-URL.
  2. Strukturierte Daten anfragen mit Extraktionsregeln, sodass die Anwendung niemals HTML parsen muss.
  3. Die richtigen Fehler wiederholen und bei denen aufhören, die ohnehin nie erfolgreich sein werden.
  4. Viele Seiten parallel abrufen, ohne das Nebenläufigkeitslimit des Plans zu überschreiten.

Falls Sie noch keine Scraping-API verwendet haben, behandelt unsere Einführung für Einsteiger das Anfrageformat und die Statuscodes ausführlicher.

Der 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];
    }
}

Ein paar Entwurfsentscheidungen, die eine Erwähnung wert sind:

  • Wiederholungen nur dort, wo sie helfen. Timeouts (408), fehlgeschlagene Renderings (422), Rate-Limits (429) und Serverfehler (500) werden mit wachsender Pause wiederholt. Alles andere, etwa ein ungültiger Schlüssel, wirft sofort eine Ausnahme.
  • Extraktionsregeln werden zu einem Array. Mit Regeln gibt fetch() decodiertes JSON zurück, sodass der aufrufende Code mit $product['price'] arbeitet, nicht mit Markup.
  • Parallele Anfragen mit curl_multi. fetchMany() sendet einen Stapel von Anfragen auf einmal und wartet auf alle. Die Stapelgröße ist so bemessen, dass das Nebenläufigkeitslimit des Plans eingehalten wird. Seiten, die innerhalb des Stapels fehlschlagen, erhalten über fetch() mit seinen Wiederholungen eine zweite Chance und kommen als null zurück, falls sie weiterhin fehlschlagen.

Verwendung

<?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);

Bewahren Sie den API-Schlüssel in einer Umgebungsvariablen oder der Secrets-Konfiguration Ihres Frameworks auf, niemals im Code.

Wie wir ihn getestet haben

Wir haben den Client gegen einen lokalen PHP-Server getestet, der die API simulierte. Er lieferte normale Seiten aus, eine Seite, die zweimal mit 429 fehlschlug, bevor sie erfolgreich war, eine, die immer 401 zurückgab, und eine, die immer mit 408 in ein Timeout lief. Die Tests prüften, dass:

  • HTML und extrahiertes JSON korrekt zurückkamen;
  • die instabile Seite beim dritten Versuch erfolgreich war;
  • die 401 einmal versucht und die 408 bis zur Obergrenze wiederholt wurde, bevor sie fehlschlug;
  • ein Stapel von 13 Seiten 12 Ergebnisse und ein null für die fehlgeschlagene Seite zurückgab.

Alle 11 Prüfungen bestanden auf PHP 8.5.

Einbindung in eine Anwendung

Nicht während eines Seitenaufrufs scrapen. Ein Nutzer sollte niemals darauf warten müssen, dass eine entfernte Seite lädt. Führen Sie die Datenerfassung in einem Queue-Worker oder einem geplanten Job aus und liefern Sie die gespeicherten Ergebnisse aus.

Ergebnisse mit Zeitstempel speichern. Speichern Sie jeden Datensatz mit dem Zeitpunkt seiner Erfassung, sodass die Anwendung anzeigen kann, wie aktuell er ist, und nur das aktualisiert, was veraltet ist.

Aggressiv cachen. Die meisten Daten ändern sich nicht minütlich. Eine stündliche oder tägliche Aktualisierung reicht für Preise und Listings auf den meisten Seiten aus, und jede Anfrage, die Sie sich sparen, spart Geld.

Fehlschläge protokollieren. Zeichnen Sie auf, welche URLs null zurückgeben und warum. Ein plötzlicher Anstieg bedeutet meist, dass eine Seite ihr Layout geändert hat und Ihre Extraktionsregeln aktualisiert werden müssen.

Die Limits des Plans beachten. Setzen Sie die Stapelgröße auf das Nebenläufigkeitslimit Ihres Plans oder darunter. Bei Shifter liegt dieses Limit je nach Plan zwischen 20 und 500 gleichzeitigen Anfragen.

Wann Proxys stattdessen direkt verwendet werden sollten

Eine Scraping-API eignet sich für die meisten Datenerfassungsaufgaben: Sie übernimmt Rendering, Wiederholungen und IP-Auswahl für Sie. Manche Aufgaben passen dennoch besser zu einer direkten Proxy-Verbindung, etwa wenn Sie volle Kontrolle über den HTTP-Client benötigen oder lange Sitzungen auf einer IP. Unser Leitfaden zur Verwendung von Residential Proxys in PHP mit cURL und Guzzle behandelt diesen Weg.

FAQ

Kann PHP für Web Scraping verwendet werden?

Ja. Die curl-Erweiterung von PHP handhabt HTTP gut, einschließlich paralleler Anfragen mit curl_multi. Kombiniert man sie mit einer Web-Scraping-API, muss die Anwendung Proxys, Browser oder Blockaden nicht selbst verwalten.

Brauche ich Guzzle, um eine Web-Scraping-API aus PHP aufzurufen?

Nein. Der Client in diesem Leitfaden verwendet nur die eingebaute curl-Erweiterung. Guzzle funktioniert ebenfalls, wenn Ihre Anwendung es bereits einsetzt.

Wie scrape ich JavaScript-gerenderte Seiten aus PHP?

Bitten Sie die API, die Seite zu rendern, indem Sie render_js übergeben, was der Client als Parameter zur Verfügung stellt. Die API führt einen Browser aus und gibt die gerenderte Seite zurück, sodass PHP niemals einen Headless-Browser benötigt.

Wie viele Anfragen kann ich gleichzeitig senden?

So viele, wie Ihr Scraping-API-Plan erlaubt. Bemessen Sie die Stapelgröße von fetchMany() auf dieses Limit oder darunter.

Fazit

Eine PHP-Anwendung kann Webdaten nutzen, ohne eine eigene Scraping-Infrastruktur aufzubauen: ein kleiner curl-basierter Client, Extraktionsregeln statt HTML-Parsing, Wiederholungen nur für die Fehler, die beim zweiten Versuch funktionieren können, und parallele Stapel in der Größe des eigenen Plans. Lassen Sie ihn aus einer Queue laufen, speichern Sie Ergebnisse mit Zeitstempeln, und die Anwendung erhält aktuelle Daten, ohne jemals einen Nutzer auf die Website eines anderen warten zu lassen.

Bereit, loszulegen?

Testen Sie Shifters Residential-Proxys, 205M+ IPs, 195+ Länder, ab 0,75 $/GB.

Jetzt starten