Em 7 de outubro de 2026, a Shifter tem o maior pool de IPs dos EUA ativos, mais do que a Oxylabs e a Bright Data, por uma fração do custo.A Shifter agora tem o maior pool de IPs ativos dos EUA

Ver benchmarks

Extração de dados

Aprimorando Aplicações PHP com Integração de API de Web Scraping

Adicione dados da web a uma aplicação PHP com uma API de web scraping: um cliente sem dependências com retentativas, extração estruturada e requisições paralelas via curl_multi.

Chris Collins

19 de outubro de 2026 · 7 min de leitura

Grande parte da web ainda roda em PHP: lojas, CMSs, ferramentas internas e portais de clientes. Quando uma dessas aplicações precisa de dados de outros sites, como preços de concorrentes, detalhes de produtos ou listagens, o caminho tentador é buscar páginas com file_get_contents e analisá-las com expressões regulares. Funciona no primeiro dia e quebra no décimo, quando o site bloqueia o IP do servidor ou muda seu markup.

Uma web scraping API elimina a maior parte dessa fragilidade. Ela busca páginas por meio de seus próprios proxies e navegadores, tenta novamente em caso de falhas e pode retornar apenas os campos solicitados em JSON. Este tutorial constrói um pequeno cliente PHP para uma delas, sem dependências além da extensão curl, e mostra como usá-lo em uma aplicação.

O que o cliente precisa fazer

Quatro coisas cobrem a maior parte do uso real:

  1. Enviar uma requisição com a chave de API e a URL de destino.
  2. Pedir dados estruturados com regras de extração, para que a aplicação nunca precise analisar HTML.
  3. Tentar novamente os erros certos e parar nos que nunca vão dar certo.
  4. Buscar várias páginas em paralelo sem ultrapassar o limite de concorrência do plano.

Se você nunca usou uma web scraping API antes, nosso guia para iniciantes cobre o formato das requisições e os códigos de status com mais detalhes.

O cliente

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

Algumas escolhas de design que vale destacar:

  • Repetições apenas onde ajudam. Timeouts (408), renderizações que falham (422), limites de taxa (429) e erros de servidor (500) são repetidos com uma pausa crescente. Qualquer outro caso, como uma chave inválida, lança uma exceção imediatamente.
  • Regras de extração viram um array. Com regras definidas, fetch() retorna JSON decodificado, de modo que o código que chama trabalha com $product['price'], e não com markup.
  • Requisições paralelas com curl_multi. fetchMany() envia um lote de requisições de uma vez e espera que todas terminem. Os lotes são dimensionados para ficar abaixo do limite de concorrência do seu plano. Páginas que falham no lote ganham uma segunda chance por meio de fetch(), com suas repetições, e retornam como null se ainda assim falharem.

Usando o cliente

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

Mantenha a chave de API em uma variável de ambiente ou na configuração de segredos do seu framework, nunca no código.

Como testamos

Executamos o cliente contra um servidor PHP local fazendo o papel da API. Ele servia páginas normais, uma página que falhava duas vezes com 429 antes de ter sucesso, uma que sempre retornava 401 e uma que sempre dava timeout com 408. Os testes verificaram que:

  • o HTML e o JSON extraído voltaram corretamente;
  • a página instável teve sucesso na terceira tentativa;
  • o 401 foi tentado uma única vez e o 408 foi repetido até o limite antes de falhar;
  • um lote de 13 páginas retornou 12 resultados e um null para a página que falhou.

Todas as 11 verificações passaram no PHP 8.5.

Encaixando isso em uma aplicação

Não faça scraping durante a visualização de uma página. Um usuário nunca deve esperar o carregamento de um site remoto. Execute a coleta em um worker de fila ou em um job agendado, e sirva os resultados armazenados.

Armazene os resultados com um timestamp. Salve cada registro com o horário em que foi coletado, para que a aplicação possa mostrar o quão recente ele é e atualizar apenas o que estiver desatualizado.

Faça cache de forma agressiva. A maioria dos dados não muda a cada minuto. Atualizar a cada hora ou diariamente já é suficiente para preços e listagens na maioria dos sites, e cada requisição evitada economiza dinheiro.

Registre as falhas. Anote quais URLs retornam null e por quê. Um aumento repentino geralmente significa que um site mudou seu layout e suas regras de extração precisam de atualização.

Respeite os limites do plano. Defina o tamanho do lote igual ou abaixo do limite de concorrência do seu plano. No Shifter, esse limite varia entre 20 e 500 requisições simultâneas, dependendo do plano.

Quando usar proxies diretamente em vez disso

Uma web scraping API atende à maior parte da coleta de dados: ela cuida da renderização, das repetições e da escolha de IP para você. Alguns trabalhos ainda se encaixam melhor em uma conexão direta por proxy, como quando você precisa de controle total do cliente HTTP, ou de sessões longas em um único IP. Nosso guia sobre uso de proxies residenciais em PHP com cURL e Guzzle cobre essa abordagem.

Perguntas frequentes

O PHP pode ser usado para web scraping?

Sim. A extensão curl do PHP lida bem com HTTP, incluindo requisições paralelas com curl_multi. Combiná-la com uma web scraping API significa que a aplicação não precisa gerenciar proxies, navegadores ou bloqueios por conta própria.

Preciso do Guzzle para chamar uma web scraping API a partir do PHP?

Não. O cliente deste guia usa apenas a extensão curl embutida. O Guzzle também funciona se sua aplicação já o utiliza.

Como faço scraping de páginas renderizadas por JavaScript a partir do PHP?

Peça à API para renderizar a página passando render_js, que o cliente expõe como parâmetro. A API executa um navegador e retorna a página renderizada, de modo que o PHP nunca precisa de um navegador headless.

Quantas requisições posso enviar de uma vez?

Tantas quantas o seu plano de web scraping API permitir. Dimensione os lotes de fetchMany() para esse limite ou abaixo dele.

Conclusão

Uma aplicação PHP pode usar dados da web sem construir uma infraestrutura de scraping: um pequeno cliente baseado em curl, regras de extração em vez de análise de HTML, repetições apenas para os erros que podem ser superados, e lotes paralelos dimensionados conforme o seu plano. Execute isso a partir de uma fila, armazene os resultados com timestamps, e a aplicação terá dados atualizados sem nunca fazer um usuário esperar pelo site de outra pessoa.

Pronto para começar?

Experimente os proxies residenciais da Shifter, mais de 205M IPs, mais de 195 países, a partir de $ 0,75/GB.

Começar