Voltar ao blogue
Guias
Sorin-Gabriel MaricaLast updated on Apr 30, 202619 min read

Web Scraping com PHP: Um Guia Prático de Bibliotecas, Código e Melhores Práticas

Web Scraping com PHP: Um Guia Prático de Bibliotecas, Código e Melhores Práticas
Resumo: O PHP é uma linguagem perfeitamente adequada para a extração de dados da Web, graças a extensões integradas como o cURL e o DOMDocument, além de um rico ecossistema do Composer que inclui o Guzzle, o Symfony DomCrawler e o Symfony Panther para navegação sem interface gráfica. Este guia orienta-o ao longo de todo o fluxo de trabalho: obtenção de páginas, análise de HTML, armazenamento de resultados em CSV/JSON/MySQL, tratamento de erros e como evitar bloqueios.

O web scraping com PHP é o processo de obter páginas web programaticamente e extrair dados estruturados do seu HTML utilizando scripts e bibliotecas PHP. Se já escreve PHP no seu trabalho diário, não há razão para mudar de linguagem apenas para extrair dados de sites. O PHP vem com ligações cURL e um analisador DOM integrado de fábrica, e o Composer dá-lhe acesso a clientes HTTP testados em batalha, motores de seleção CSS e até navegadores headless.

Este tutorial destina-se a programadores PHP de nível intermédio que procuram um guia prático e centrado no código. Começará com chamadas cURL de baixo nível, avançará para bibliotecas de nível superior como o Guzzle e o Symfony HttpBrowser, abordará páginas renderizadas em JavaScript com o Symfony Panther e terminará com questões de produção, como armazenamento de dados, tratamento de erros e como evitar listas de bloqueio. Todos os exemplos neste tutorial de web scraping em PHP seguem um único cenário (extrair dados de um site público de listagem de livros), para que possa acompanhar o fluxo de trabalho completo de início a fim, em vez de saltar entre trechos desconexos.

Por que o PHP é uma excelente escolha para web scraping

O PHP pode não ser a primeira linguagem que vem à mente quando as pessoas pensam em scraping, mas tem várias vantagens práticas. Em primeiro lugar, se a sua pilha existente já funciona em PHP, adicionar um scraper significa zero novas dependências de tempo de execução. A sua equipa pode manter o código, o seu pipeline de implementação permanece o mesmo e evita a sobrecarga cognitiva da mudança de contexto para outra linguagem.

Em segundo lugar, as extensões integradas do PHP são surpreendentemente adequadas para esta tarefa. A curl extensão lida com pedidos HTTP, dom e libxml oferecem-lhe um analisador HTML/XML em conformidade com os padrões e mbstring resolve os problemas de codificação de caracteres. Não precisa de instalar nada extra para um scraping básico.

Em terceiro lugar, o ecossistema do Composer preenche todas as lacunas restantes. O Guzzle fornece um cliente HTTP moderno com suporte a middleware. O Symfony DomCrawler adiciona consultas de seletores CSS ao DOMDocument. O Symfony Panther controla uma instância real do Chrome ou do Firefox para páginas com muito JavaScript. As ferramentas são maduras e mantidas ativamente.

E quanto ao PHP versus Python para scraping? O Python tem uma comunidade maior dedicada ao scraping e bibliotecas como Beautiful Soup e Scrapy, mas isso não torna o PHP uma má escolha. Se o PHP é a sua linguagem mais forte, escreverá um scraper funcional mais rapidamente do que o faria numa linguagem que ainda está a aprender. A melhor linguagem de scraping é aquela que consegue depurar às 2 da manhã.

Bibliotecas de scraping em PHP num relance

Antes de escrever código, é útil saber quais as ferramentas existentes e quando recorrer a cada uma delas. A tabela abaixo compara as principais bibliotecas de scraping em PHP com base nos critérios mais importantes: o que fazem, se suportam JavaScript e quanto esforço é necessário para as aprender.

Biblioteca / Ferramenta

Finalidade

Suporte a JS

Curva de aprendizagem

Estado de manutenção

cURL (ext-curl)

Pedidos HTTP de baixo nível

Não

Baixo

Integrado, sempre disponível

Guzzle

Cliente HTTP com middleware, assíncrono

Não

Baixo–Médio

Atualmente em manutenção

DOMDocument + DOMXPath

Análise de HTML/XML, consultas XPath

Não

Médio

Integrado

Symfony DomCrawler

Seletor CSS e consultas XPath

Não

Baixo

Manutenção ativa

Goutte (obsoleto)

Rastreamento combinado HTTP + DOM

Não

Baixo

Obsoleto, use HttpBrowser

Symfony HttpBrowser

Sucessor do Goutte, mesma API

Não

Baixo

Manutenção ativa

Symfony Panther

Navegador sem interface gráfica (Chrome/Firefox)

Sim

Médio–Alto

Manutenção ativa

Serviço de API de scraping

Pedido gerido + camada de análise

Depende do fornecedor

Muito baixo

Gerido externamente

Algumas observações. O Goutte foi a biblioteca de scraping «tudo-em-um» de referência durante anos, mas foi descontinuado. No momento da redação deste artigo, o caminho de migração recomendado é o Symfony HttpBrowser, que fornece uma API quase idêntica, apoiada pelos componentes BrowserKit e HttpClient do Symfony. Se estiver a iniciar um novo projeto, ignore completamente o Goutte e passe diretamente para o HttpBrowser.

Para a maioria das tarefas de scraping de páginas estáticas, o Guzzle (para a obtenção de dados) juntamente com o Symfony DomCrawler (para a análise) é uma combinação sólida e leve. Reserve o Symfony Panther para páginas que realmente necessitem da execução de JavaScript, porque iniciar um navegador headless é significativamente mais lento e consome mais recursos.

Configurar o seu ambiente de scraping em PHP

Vamos tratar primeiro dos pré-requisitos. Precisa do PHP 8.1 ou mais recente (para suporte a enum e fiber em bibliotecas modernas), do Composer e de algumas extensões.

Verifique a sua versão de PHP e as extensões carregadas:

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

Se alguma dessas quatro extensões estiver em falta, ative-as no seu php.ini ou instale-as através do gestor de pacotes do seu sistema (por exemplo, sudo apt install php-curl php-xml php-mbstring no Debian/Ubuntu).

Em seguida, inicialize um diretório de projeto e importe as bibliotecas que irá utilizar ao longo deste tutorial:

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

Essa única composer require linha fornece-lhe o Guzzle para HTTP, o DomCrawler para análise e o Symfony HttpBrowser para o fluxo de trabalho combinado de rastreamento. Adicionaremos o Symfony Panther mais tarde, quando precisarmos de suporte para navegadores headless.

Crie um scrape.php ficheiro e adicione o autoloader do Composer no início:

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

Está pronto para buscar a sua primeira página.

Recuperar páginas com o cURL

A extensão cURL do PHP é a ferramenta HTTP de nível mais baixo na sua caixa de ferramentas. É prolixa, mas oferece-lhe controlo total sobre todos os detalhes do pedido, o que é útil quando precisa de imitar uma impressão digital específica do navegador ou depurar problemas de ligação.

Aqui está uma solicitação GET básica que carrega a página inicial de um catálogo público de livros (usaremos http://books.toscrape.com como nosso alvo de demonstração ao longo do texto):

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

Algumas coisas que vale a pena notar. CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE permitem a persistência de cookies entre pedidos, o que é essencial para fluxos de scraping em várias etapas, nos quais o servidor rastreia o estado da sessão. Definir um cabeçalho User-Agent faz com que a sua solicitação pareça tráfego normal de navegador, em vez de um simples script PHP. E CURLOPT_FOLLOWLOCATION lida automaticamente com redirecionamentos 301/302, para que não tenha de os perseguir manualmente.

Para uma solicitação POST (por exemplo, enviar um formulário de pesquisa), substitua por CURLOPT_POST => true e adicione CURLOPT_POSTFIELDS com os dados do seu formulário. O resto do código padrão permanece o mesmo.

O cURL funciona, mas é suficientemente de baixo nível para que acabes por escrever wrappers para cabeçalhos, tentativas e tratamento de erros. É aí que entra o Guzzle.

Recuperar páginas com o Guzzle

O Guzzle envolve a camada cURL (ou stream) do PHP numa API limpa e orientada a objetos. Instale-o através do Composer, se ainda não o fez, e depois recupere a mesma página:

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

Isso é visivelmente menos código padrão. O Guzzle também oferece ganchos de middleware para registo, lógica de repetição e injeção de cabeçalhos, o que significa que pode centralizar preocupações transversais em vez de espalhar curl_setopt chamadas por todo o lado.

Pedidos simultâneos com as promessas do Guzzle

Quando precisa de extrair várias páginas, disparar pedidos um de cada vez é dolorosamente lento. O Guzzle suporta concorrência baseada em promessas através da sua Pool classe, que permite enviar várias solicitações em paralelo enquanto controla o nível de concorrência.

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

Com um nível de concorrência de 5, o Guzzle dispara até cinco pedidos simultaneamente, em vez de esperar que cada um seja concluído. Numa extração de 50 páginas, isto pode reduzir o tempo de execução total de minutos para segundos. De acordo com a documentação do Guzzle sobre pedidos simultâneos, a API Pool utiliza o multi-handle do cURL nos bastidores, pelo que o ganho de desempenho é real, não apenas sintaxe simplificada.

Análise de HTML: DOMDocument e XPath

Depois de ter o HTML bruto numa string, é necessário extrair dados estruturados dele. A classe DOMDocument carrega o HTML numa estrutura em árvore e DOMXPath permite-lhe consultar essa árvore com expressões 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";
}

A libxml_use_internal_errors(true) chamada é importante. O HTML do mundo real quase nunca é XML válido e, sem esse sinalizador, o PHP emitirá avisos para cada tag não fechada ou atributo incompatível. Suprimir esses avisos permite analisar páginas desorganizadas sem sobrecarregar os seus registos.

O XPath é poderoso para consultas complexas. Queres obter todos os livros com preço inferior a 20 £? Podes combinar eixos e predicados:

$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";
    }
}

O DOMDocument, juntamente com o XPath, oferece-lhe controlo total e zero dependências externas. A desvantagem é a verbosidade: mesmo uma consulta simples requer várias linhas de configuração. É aí que o Symfony DomCrawler mostra o seu valor.

Análise de HTML: Symfony DomCrawler e seletores CSS

O Symfony DomCrawler assenta no DOMDocument, mas expõe uma API muito mais intuitiva. Em vez de escrever XPath manualmente, pode usar seletores CSS (que a maioria dos programadores web já conhece) e encadear métodos num estilo semelhante ao 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";
});

Compare isso com a versão DOMXPath acima. A intenção é idêntica, mas o código do DomCrawler tem metade do tamanho e é mais fácil de ler. O filter() método aceita qualquer seletor CSS válido, text() retorna o conteúdo de texto e attr() extrai o valor de um atributo.

Quando deve usar seletores CSS em vez de XPath para scraping? Os seletores CSS cobrem 90% dos casos práticos e são mais intuitivos para quem escreve código front-end. O XPath ganha quando precisa percorrer para cima (selecionar um pai com base no texto de um filho), executar funções de cadeia de caracteres dentro da consulta ou navegar por eixos de irmãos. Uma boa regra prática: comece com seletores CSS e recorra ao XPath apenas quando o CSS não conseguir expressar o que precisa.

Por que é que o Regex é arriscado para a análise de HTML

É tentador recorrer a preg_match() quando precisa apenas de um valor de uma página. Resista à tentação. O HTML não é uma linguagem regular, e a extração baseada em regex falha no momento em que a marcação muda de forma trivial: um novo atributo, uma mudança no estilo das aspas ou espaços em branco adicionais.

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

Um analisador DOM lida com todas essas variações com facilidade. Guarde a regex para texto genuinamente plano (ficheiros de log, linhas CSV) e use DOMDocument ou DomCrawler para qualquer coisa que venha de um documento HTML.

Construir um Scraper Completo com o Goutte e o Seu Sucessor

O Goutte foi a biblioteca que tornou o web scraping em PHP acessível. Combinou o cliente HTTP do Guzzle com o DomCrawler do Symfony numa única classe, permitindo-lhe buscar e analisar numa única chamada. No entanto, o Goutte foi oficialmente descontinuado. Os seus mantenedores recomendam a migração para o Symfony HttpBrowser, que vem incluído no componente Symfony BrowserKit e oferece uma API quase idêntica.

Aqui está um scraper completo construído com o Symfony HttpBrowser que recupera listas de livros em várias páginas:

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";

Repare como funciona a lógica de paginação. Após analisar cada página, o scraper verifica se existe um link «seguinte». Se existir, o scraper segue-o e repete o processo. Caso contrário, $url é definido como null e o ciclo termina. Este padrão é reutilizável para qualquer lista paginada.

A migração do Goutte é mínima. Se o seu código existente usa $goutte = new \Goutte\Client(), substitua-o por $browser = new HttpBrowser(HttpClient::create()). O request(), filter(), e selectLink() permanecem os mesmos. A camada HTTP subjacente muda do Guzzle para o Symfony HttpClient, o que lhe oferece suporte assíncrono nativo e uma melhor integração com o resto do ecossistema Symfony.

Mais uma vantagem do HttpBrowser: ele rastreia automaticamente cookies e sessões entre as solicitações. Quando chama $browser->request() várias vezes, o cliente comporta-se como uma sessão de navegador real, transportando os cookies sem necessidade de configuração adicional.

Rastreamento de páginas renderizadas por JavaScript com o Symfony Panther

Os scrapers de páginas estáticas falham quando o conteúdo de que necessita é injetado por JavaScript após o carregamento inicial da página. Aplicações de página única, feeds de rolagem infinita e grelhas de produtos carregadas de forma diferida requerem todas um motor de navegador real para serem renderizadas. O Symfony Panther preenche essa lacuna ao controlar o Chrome ou o Firefox através do protocolo WebDriver.

Instale o Panther e um binário do 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

Agora, extraia uma página que depende da renderização de conteúdo dinâmico com 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();

O waitFor() método pausa a execução até que o seletor CSS especificado apareça no DOM renderizado, com um tempo limite (10 segundos neste caso) para evitar travamentos infinitos. Isto é essencial para a extração de conteúdo dinâmico com PHP, pois o HTML de que necessita pode nem existir na resposta inicial.

O Panther é poderoso, mas dispendioso. Cada pedido inicia um processo de navegador real, consumindo memória e CPU. Utilize-o apenas quando a renderização de JavaScript for genuinamente necessária. Para páginas que carregam dados através de uma simples chamada XHR/API, é frequentemente mais rápido encontrar esse ponto final da API no separador Rede do seu navegador e aceder-lhe diretamente com o Guzzle.

Utilizar uma API de scraping para extração sem intervenção

A certa altura, o custo de engenharia de manter o seu próprio scraper (rotação de proxies, resolução de CAPTCHA, impressão digital do navegador, lógica de repetição) excede o custo de externalizar essa infraestrutura para um serviço dedicado. Esse é o ponto ideal para uma API de scraping.

O padrão de integração é simples. Envia-se um URL para o endpoint da API, e este devolve o HTML da página (ou JSON estruturado) com todo o tratamento anti-bot feito do lado do servidor:

$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

Quando é que uma API de scraping faz mais sentido do que uma abordagem DIY? Considere-a quando estiver a fazer scraping em grande escala (milhares de páginas por dia), visando sites com defesas anti-bot agressivas, ou quando a sua equipa não tiver tempo para manter pools de proxies e infraestrutura de navegadores. A troca é o custo por pedido versus horas de engenharia.

Um serviço gerido também se destaca na carga de manutenção. Quando um site alvo altera a sua pilha anti-bot, um fornecedor de API de scraping atualiza a sua infraestrutura. O seu código permanece o mesmo. Se estiver a avaliar opções, procure um fornecedor que cobre apenas pelas respostas bem-sucedidas, para que não pague por pedidos falhados.

Armazenamento de dados extraídos: CSV, JSON e MySQL

A recolha de dados é apenas metade do trabalho. É necessário armazená-los num formato que os processos a jusante (análise, pipelines de ML, painéis) possam utilizar.

O CSV é a opção mais simples e funciona bem para dados planos e tabulares:

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

O JSON preserva estruturas aninhadas e é mais fácil de importar para APIs e armazenamentos NoSQL:

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

O MySQL via PDO é a escolha certa quando precisa de um armazenamento relacional pesquisável:

$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'],
    ]);
}

A utilização de instruções preparadas com PDO não é opcional. Protege-o contra injeções SQL, que constituem um risco real ao inserir texto gerado pelo utilizador ou extraído externamente numa base de dados.

Para dados orientados a documentos ou esquemas que mudam frequentemente, o MongoDB é outra opção viável. O mongodb/mongodb pacote do Composer fornece um método simples insertMany() que aceita diretamente matrizes de matrizes associativas. A escolha entre armazenamento relacional e de documentos depende de quão estruturados estão os seus dados extraídos e do que os irá consumir.

Tratamento de erros, novas tentativas e registo

Um scraper que funciona no seu portátil não é o mesmo que um scraper que funciona de forma fiável em produção. Tempos de espera de rede, respostas 5xx, reinicializações de ligação e erros de limitação de taxa são inevitáveis quando se fazem milhares de pedidos HTTP. Incorporar resiliência no seu scraper desde o início evita-lhe a perda silenciosa de dados.

Envolva cada chamada HTTP num try-catch com back-off exponencial:

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

Para registo estruturado, o Monolog é o padrão de facto no ecossistema PHP. Adicionar um gestor de ficheiros rotativos requer duas linhas:

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()]);

Registe todos os URLs de pedido, códigos de estado e quaisquer exceções. Quando uma tarefa de scraping falha na página 847 de 1000, os registos são a única coisa que lhe dirá o que correu mal. Este tipo de foco na preparação para produção é o que distingue um protótipo de um pipeline fiável.

Evitar bloqueios: proxies, cabeçalhos e limitação de taxa

Os sites não gostam que os bots sobrecarreguem os seus servidores. Se o seu scraper enviar centenas de pedidos idênticos por minuto a partir de um único IP, espere ser bloqueado. O scraping educado é tanto uma obrigação ética como uma necessidade prática para projetos de longa duração.

Alterne as cadeias de caracteres do User-Agent para que cada pedido não seja identificado como proveniente do mesmo cliente:

$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)]];

Adicione atrasos aleatórios entre as solicitações para evitar padrões de tempo previsíveis:

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

Respeite robots.txt programaticamente. Antes de fazer scraping de um domínio, obtenha o seu robots.txt e verifique se o seu caminho de destino é proibido. Pode analisar isto manualmente ou usar uma biblioteca como 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";
}

A rotação de proxies é a defesa mais eficaz contra o bloqueio baseado em IP. Se estiver a fazer scraping em qualquer volume significativo, encaminhar os pedidos através de um conjunto de proxies residenciais torna o seu tráfego praticamente indistinguível do tráfego de utilizadores orgânicos. Pode configurar o Guzzle para usar um proxy com uma única opção:

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

Combinar todas estas técnicas (cabeçalhos variados, atrasos educados, respeito pelo robots.txt e rotação de proxies) dá-lhe a melhor hipótese de fazer scraping de forma fiável sem ser sinalizado.

Considerações legais e éticas

O web scraping ocupa uma zona cinzenta legal que varia consoante a jurisdição. Alguns princípios aplicam-se de forma geral.

O robots.txt é uma norma voluntária, não um contrato legal, mas ignorá-lo enfraquece qualquer argumento de boa-fé que possa apresentar caso seja contestado. Trate-o como uma base que deve sempre respeitar.

Os Termos de Serviço do site de destino podem proibir explicitamente o acesso automatizado. A violação dos Termos de Serviço pode expô-lo a reclamações por quebra de contrato, particularmente nos Estados Unidos após casos como o hiQ Labs v. LinkedIn, que esclareceu que a extração de dados acessíveis ao público não é necessariamente uma violação da Lei de Fraude e Abuso Informático, mas não abordou a aplicação dos Termos de Serviço.

O RGPD é relevante se extrair dados pessoais pertencentes a residentes da UE (nomes, endereços de e-mail, detalhes de perfil). Ao abrigo do RGPD, a extração de dados da Web pode constituir tratamento de dados, o que significa que necessita de uma base legal (normalmente interesse legítimo) e deve tratar esses dados de acordo com os requisitos do RGPD: limitação da finalidade, minimização do armazenamento e atendimento aos pedidos de acesso dos titulares dos dados. Em caso de dúvida, consulte um profissional jurídico, especialmente se a sua extração tiver como alvo conteúdos gerados pelos utilizadores.

O mínimo ético é simples: não faça scraping a um ritmo que prejudique o desempenho do site alvo, não recolha dados para os quais não tenha uma utilização legítima e seja transparente quanto às suas intenções sempre que possível.

Pontos-chave

  • Escolha a ferramenta certa para o tipo de página. Use o Guzzle mais o DomCrawler para HTML estático, o Symfony Panther para conteúdo renderizado em JavaScript e uma API de scraping quando a infraestrutura anti-bot ultrapassar a sua configuração DIY.
  • O Goutte está obsoleto. Comece novos projetos com o Symfony HttpBrowser, que oferece o mesmo fluxo de trabalho de rastreamento apoiado por componentes Symfony mantidos ativamente.
  • Construa resiliência desde o primeiro dia. Tentativas de retoma com recuo exponencial, registo estruturado e validação de entradas não são opcionais em scrapers de produção.
  • Armazene dados no formato de que os seus consumidores a jusante necessitam. CSV para análise rápida, JSON para APIs e armazenamento de documentos, MySQL/PDO para consultas relacionais.
  • Faça o rastreamento de forma educada e legal. Alterne cabeçalhos e proxies, respeite robots.txt, adicione intervalos entre pedidos e compreenda as implicações do RGPD na recolha de dados pessoais.

Perguntas frequentes

O PHP ou o Python são melhores para projetos de scraping na web?

Nenhuma das duas é objetivamente superior. O Python tem um ecossistema de scraping mais vasto (Beautiful Soup, Scrapy, ligações Selenium), o que significa mais tutoriais e respostas da comunidade. O PHP tem extensões HTTP e DOM robustas integradas, e bibliotecas do Composer como Guzzle e DomCrawler são de nível de produção. Escolha a linguagem que a sua equipa conhece melhor. Um scraper PHP bem escrito irá sempre superar um scraper Python mal mantido.

O PHP consegue fazer scraping de aplicações de página única com muito JavaScript?

Sim, mas precisa de um navegador headless. O Symfony Panther controla o Chrome ou o Firefox através do protocolo WebDriver e consegue renderizar páginas totalmente dinâmicas. Para casos mais simples, em que a página obtém dados de um ponto de extremidade XHR, pode ignorar completamente o navegador e chamar diretamente esse ponto de extremidade da API com um cliente HTTP, o que é mais rápido e utiliza menos recursos.

A legalidade depende da jurisdição, dos termos de serviço do site de destino e do tipo de dados recolhidos. O scraping de dados não pessoais e acessíveis ao público é geralmente permitido em muitas jurisdições. O RGPD aplica-se quando se processam dados pessoais de residentes na UE, exigindo uma base legal, como o interesse legítimo. Reveja sempre os Termos de Serviço do site de destino e consulte um advogado antes de fazer scraping de dados pessoais em grande escala.

Como evito que o meu IP seja bloqueado enquanto faço scraping com PHP?

Combine várias técnicas: alterne as cadeias de caracteres do User-Agent, adicione atrasos aleatórios entre os pedidos (1 a 3 segundos é um intervalo razoável), respeite robots.txt diretivas e encaminhe o tráfego através de um conjunto de proxies rotativos. Evite enviar rajadas de pedidos a partir de um único IP. Se estiver a fazer scraping em grande volume, um proxy gerido ou um serviço de API de scraping trata da rotação e da anti-detecção por si.

Como lido com páginas protegidas por login ao fazer scraping com PHP?

Envie as credenciais através de uma solicitação POST (ou através do envio de um formulário com o Symfony HttpBrowser) e mantenha o cookie de sessão resultante nas solicitações subsequentes. Com o HttpBrowser, os cookies de sessão persistem automaticamente. Com o cURL puro, defina CURLOPT_COOKIEJAR e CURLOPT_COOKIEFILE para o mesmo caminho. Verifique sempre se o seu login não ativou um CAPTCHA ou um desafio de autenticação de dois fatores e tenha em conta que o scraping atrás de um login pode ter implicações legais mais rigorosas ao abrigo dos termos de serviço do site.

Conclusão

O scraping da Web com PHP é um fluxo de trabalho prático e bem suportado, desde que saiba quais as bibliotecas a utilizar. Comece com o cURL ou o Guzzle para a recuperação de dados, adicione o DomCrawler ou o DOMXPath para a análise e recorra ao Symfony Panther apenas quando a renderização em JavaScript for inevitável. Mantenha os seus dados no formato que os utilizadores esperam, envolva tudo em lógica de repetição de tentativas e registo, e faça sempre o scraping de forma educada.

Os exemplos deste tutorial abrangeram todo o ciclo de vida: desde um pedido HTTP bruto até ao tratamento da paginação, recuperação simultânea, armazenamento de dados e estratégias anti-bloqueio. Cada técnica corresponde a uma preocupação real de produção, não apenas a uma demonstração de brinquedo.

Se se vir a gastar mais tempo a lutar contra defesas anti-bot do que a escrever lógica de análise, pode valer a pena transferir a infraestrutura de pedidos para um serviço como a Scraper API da WebScrapingAPI, que lida com rotação de proxies, CAPTCHAs e novas tentativas, para que se possa concentrar no código de extração de dados que realmente importa.

Sobre o autor
Sorin-Gabriel Marica, Desenvolvedor Full-Stack @ WebScrapingAPI
Sorin-Gabriel MaricaDesenvolvedor Full-Stack

Sorin Marica é engenheiro Full Stack e DevOps na WebScrapingAPI, onde desenvolve funcionalidades do produto e mantém a infraestrutura que garante o bom funcionamento da plataforma.

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.