De volta ao blog

PHP Web Scraping: Um Guia Prático

Ava Wilson
Ava Wilson

Expert in Web Scraping Technologies

11-Aug-2026

TL;DR:

  • PHP inclui tudo o que um scraper básico precisa. ext-curl busca, DOMDocument mais DOMXPath analisa, e nenhum pacote Composer está envolvido. Uma extração funcionando tem cerca de quinze linhas.
  • Uma instalação padrão do php-cli não é suficiente. Em uma máquina Ubuntu limpa php-cli expôs apenas json e libxml; curl, dom e mbstring tiveram que ser instalados separadamente. Verifique antes de escrever o código, não depois.
  • DOMDocument::loadHTML() inundará sua saída com avisos. Páginas reais são HTML5 e o analisador espera HTML4. Envolva o carregamento em libxml_use_internal_errors(true) ou o ruído parecerá uma falha quando nada falhou.
  • A API de Scraping Universal Scrapeless retorna um envelope JSON. A marcação está em data, então você decodifica primeiro e analisa depois.
  • chrome-php controla um navegador remoto, mas seu tempo limite padrão é dimensionado para um local. Medido contra um endpoint de CDP na nuvem, o padrão de 5 segundos da biblioteca completou 2 de 6 tentativas; aumentando sendSyncDefaultTimeout isso subiu para 5 de 6.
  • Comece grátis: o painel Scrapeless emite uma chave que funciona com todos os exemplos abaixo.

O Que Você Precisa

Cada exemplo aqui foi executado no PHP 8.3.6 (cli) contra quotes.toscrape.com, um site publicado especificamente para prática de scraping.

A primeira surpresa em uma máquina limpa é o quanto uma instalação básica do PHP inclui. Uma php-cli recém-instalada relatou apenas json e libxml do conjunto que importa aqui. Os três que você realmente precisa chegam separadamente:

bash Copy
# Ubuntu/Debian — php-cli alone is not enough
apt-get install -y php-cli php-curl php-xml php-mbstring

php -m | grep -E '^(curl|dom|libxml|mbstring)$'
# curl
# dom
# libxml
# mbstring

php-xml é o pacote que fornece DOMDocument; a extensão é chamada dom, razão pela qual procurar xml não encontra nada e envia as pessoas em círculos.

Buscando uma Página com ext-curl

file_get_contents() funciona para casos triviais, mas não lhe dá código de status, controle de cabeçalho e nenhum tempo limite que valha a pena. ext-curl é a linha de base para qualquer coisa real:

php Copy
<?php
$ch = curl_init('https://quotes.toscrape.com/');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_USERAGENT      => 'Mozilla/5.0 (compatible; php-guide/1.0)',
]);
$html   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

echo "http={$status} bytes=" . strlen($html) . "\n";

Isso imprime http=200 bytes=11064. Duas opções têm mais peso do que parecem: CURLOPT_RETURNTRANSFER é o que faz curl_exec() retornar o corpo em vez de imprimi-lo, e sem CURLOPT_USERAGENT muitos sites respondem a um cliente PHP nu de maneira diferente ou não respondem.

Analisando com DOMDocument e DOMXPath

A extensão DOM do PHP é uma implementação completa do padrão DOM W3C, e DOMXPath fornece o mesmo poder de consulta que qualquer biblioteca de scraping dedicada. A armadilha é o carregador.

php Copy
<?php
$doc = new DOMDocument();
libxml_use_internal_errors(true);   // without this, every HTML5 tag warns
$doc->loadHTML($html);
libxml_clear_errors();

$xpath  = new DOMXPath($doc);
$quotes = $xpath->query("//div[@class='quote']");
echo "quotes=" . $quotes->length . "\n";

$first  = $quotes->item(0);
$text   = trim($xpath->query(".//span[@class='text']", $first)->item(0)->textContent);
$author = trim($xpath->query(".//small[@class='author']", $first)->item(0)->textContent);

echo "first_author={$author}\n";
echo "first_text=" . mb_substr($text, 0, 40) . "\n";

Saída:

text Copy
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr

Três coisas valem a pena destacar.

libxml_use_internal_errors(true) não é opcional na prática. DOMDocument implementa a análise HTML4, então cada elemento HTML5 e atributo não entre aspas em uma página moderna gera um aviso. Pule a chamada e uma extração bem-sucedida entulha sua própria saída sob o ruído do analisador — o manual do PHP documenta essa opção como a forma suportada de controlar esse relatório.

O segundo argumento para DOMXPath::query() limita a consulta a um nó. Sem ele, .//span[@class='text'] pesquisa o documento inteiro e você obtém o texto da primeira citação para cada linha. Esse único argumento faz a diferença entre extração por linha e um conjunto de dados sutilmente errado.

mb_substr() em vez de substr() importa porque a página usa aspas curvas. substr() corta em bytes e dividirá um caractere multibyte em um UTF-8 inválido, que é exatamente o tipo de corrupção que aparece três etapas depois em um banco de dados.

Onde o PHP Puro Não Funciona

O script acima funciona porque o alvo renderiza seu conteúdo no lado do servidor e não se importa com quem está perguntando. Duas coisas encerram isso: conteúdo que só existe após a execução do JavaScript e sites que decidem que um cliente HTTP nu não é um navegador. Nenhum dos dois é um problema do PHP — nenhum cliente HTTP resolve nenhum dos dois, em qualquer linguagem.

Roteando solicitações através de um pool de proxies lida com uma parte do segundo caso, e se você estiver trabalhando dentro de um framework, o guia de integração de proxy do Laravel cobre essa configuração em mais detalhes do que este artigo.

Nesse ponto, há duas escaladas, e elas são ferramentas diferentes em vez de versões melhores umas das outras. Mantenha o caminho HTTP puro onde ele funciona; ele continua sendo a opção mais rápida e barata por uma larga margem.

Escalada Um: A API de Scraping Universal

A primeira escalada mantém seu código moldado como um cliente HTTP e move a parte difícil para o lado do servidor. A solicitação é ordinária ext-curl; a resposta é onde a diferença aparece.

php Copy
<?php
$key     = getenv('SCRAPELESS_API_KEY');
$payload = json_encode([
    'actor' => 'unlocker.webunlocker',
    'input' => ['url' => 'https://quotes.toscrape.com/', 'js_render' => false],
]);

$ch = curl_init('https://api.scrapeless.com/api/v2/unlocker/request');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_TIMEOUT        => 90,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json', "x-api-token: {$key}"],
]);
$raw    = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

$body = json_decode($raw, true);
echo "http={$status} envelope_keys=" . implode(',', array_keys($body)) . "\n";

$apiHtml = $body['data'];              // the markup lives here, not in $raw
echo "bytes=" . strlen($apiHtml) . "\n";
text Copy
http=200 envelope_keys=code,data
bytes=11064

O envelope é a coisa a ser internalizada. $raw é JSON, e passá-lo para DOMDocument produz um documento sem nós correspondentes e sem erro — os seletores simplesmente retornam zero linhas. Decodifique, leve data, e depois analise. Mantenha aquele desdobramento em um helper em vez de repetir json_decode(...)['data'] em vários pontos de chamada.

Desconsiderando a contagem de bytes, o código de análise está inalterado. Alimentar $apiHtml no mesmo bloco DOMXPath retorna as mesmas dez citações, que é o ponto: apenas a busca muda.

Quando o conteúdo realmente requer JavaScript, você precisa de um navegador. chrome-php/chrome fala o Protocolo DevTools do Chrome e pode iniciar um Chrome local ou se conectar a um remoto via WebSocket.

bash Copy
composer require chrome-php/chrome
# Using version ^1.16 for chrome-php/chrome  → v1.16.1
php Copy
<?php
require __DIR__ . '/vendor/autoload.php';

use HeadlessChromium\BrowserFactory;

$key = getenv('SCRAPELESS_API_KEY');
$uri = "wss://browser.scrapeless.com/api/v2/browser?token={$key}";

$browser = BrowserFactory::connectToBrowser($uri, [
    'sendSyncDefaultTimeout' => 60000,   // see below — the default is 5000
]);

$page = $browser->createPage();
$page->navigate('https://quotes.toscrape.com/')->waitForNavigation();

echo "title: "          . $page->evaluate('document.title')->getReturnValue() . "\n";
echo "quotes on page: " . $page->evaluate('document.querySelectorAll(".quote").length')->getReturnValue() . "\n";

$browser->close();

Output:

text Copy
title: Quotes to Scrape
quotes on page: 10

A linha sendSyncDefaultTimeout é a parte que vale o artigo. chrome-php define o padrão em 5000 ms, que é generoso para um Chrome rodando na mesma máquina e marginal para um conectado através de uma conexão TLS. Dois conjuntos correspondentes de seis execuções, mesmo script, mesmo alvo, mudando apenas essa opção:

configuração resultado modo de falha
padrão da biblioteca (5000 ms) 2 sucedidos, 4 falharam cada falha OperationTimedOut: Operation timed out after 5s
sendSyncDefaultTimeout => 60000 5 sucedidos, 1 falhou a única falha é um erro diferente, no momento da conexão

Duas conclusões se seguem, e a segunda é a que as pessoas perdem.

Aumentar o tempo limite é necessário. Mantido no padrão, a maioria das tentativas falhou na mesma mensagem, e é uma mensagem que engana — ela cita um tempo limite, então parece que a página ou a rede está lenta, quando na verdade expirou a espera da própria biblioteca em um ciclo de round trip do protocolo.

Aumentá-lo também não é suficiente. A única falha que sobreviveu foi Cannot connect to the browser, make sure it was not closed, levantada cerca de cinco segundos após o início, enquanto a conexão ainda estava sendo estabelecida, em vez de durante um comando. Um tempo limite de comando maior não tem influência sobre isso. Trate a obtenção do navegador como um passo falível em seu design de trabalho, distinto do trabalho que você faz assim que o possui, e mantenha $browser->close() em um finally para que uma falha no meio do trabalho nunca deixe uma sessão isolada.

Um Diagnóstico que Estava Errado

A primeira teoria para aqueles tempos limite era que a string de consulta da URI nunca chegava ao handshake do WebSocket, levando ?token= com ela. Há evidências reais para isso: Protocol::validateSocketUri() retorna apenas [$scheme, $host, $port] e descarta caminho e consulta.

Ainda está errado. O handshake é construído em outro lugar, por Protocol::getRequestHandshake(), que chama um validateUri() separado retornando cinco elementos e reanexa explicitamente a consulta antes da linha de requisição. O token realmente chega. A falha foi latência, e as duas funções analisando a mesma URI a diferentes profundidades é uma coincidência que parece exatamente como um bug.

É um lembrete útil de que, em uma biblioteca com mais de um analisador de URI, encontrar um que descarta seus dados não significa que seja o que está no caminho que você se importa.

Escolhendo Entre os Três

abordagem usar quando custo
ext-curl + DOMXPath HTML renderizado pelo servidor, alvo permissivo o mais baixo; sem dependências
API de Scraping Universal bloqueado ou desafiado, sem JS necessário uma chamada HTTP, envelope a ser desdobrado
chrome-php + Navegador de Scraping conteúdo requer execução de JavaScript o mais alto; uma sessão de navegador por trabalho

Trabalhe para baixo nessa lista, não para cima. A maioria das páginas que parecem precisar de um navegador acaba incorporando seus dados em uma tag <script>, e uma consulta DOMXPath mais json_decode() supera uma sessão de navegador em todos os aspectos.

Conclusão

PHP é uma linguagem de scraping perfeitamente razoável, e as partes que as pessoas esperam que estejam faltando estão na biblioteca padrão. ext-curl e DOMXPath cobrem completamente páginas renderizadas pelo servidor, com libxml_use_internal_errors(true) e um segundo argumento escopado para query() como os dois detalhes que separam o código funcional do código silenciosamente errado.

Quando um alvo para de cooperar, escale deliberadamente. O caminho da API mantém seu código como um cliente HTTP e pede apenas que você desdobre um envelope. O caminho do navegador custa uma sessão e, se esse navegador for remoto, uma peça específica de configuração: o padrão de cinco segundos de chrome-php é uma suposição de Chrome local, e deixá-lo em prática custou quatro das seis conexões aqui.

Pronto para Scraper a Partir do PHP?

Crie uma chave no painel do Scrapeless e execute o exemplo ext-curl contra uma página que você já coleta. Se retornar o que você espera, você está feito — sem dependência, sem navegador. A Universal Scraping API está disponível para as páginas onde não retorna, e as tarifas atuais estão na página de preços.

FAQ

P: Preciso do Composer para fazer scraping com PHP?

Não. Buscar com ext-curl e analisar com DOMDocument e DOMXPath não precisa de nada além das extensões em uma instalação padrão. O Composer só entra em cena para um driver de navegador como chrome-php/chrome.

P: Por que o DOMDocument imprime avisos em todas as páginas?

Porque implementa análise HTML4 e páginas modernas são HTML5. Os avisos são informativos, em vez de falhas. Chame libxml_use_internal_errors(true) antes de loadHTML() e libxml_clear_errors() depois, e inspecione libxml_get_errors() quando você realmente quiser vê-los.

P: Por que minha análise não retorna nada quando a solicitação da API foi bem-sucedida?

Você está quase certamente analisando o envelope. A Universal Scraping API retorna JSON com a marcação dentro de data, então DOMDocument recebe uma string JSON e não encontra nós correspondentes sem gerar um erro. Decodifique a resposta e analise data.

P: Qual XPath devo usar para um atributo de classe?

//div[@class='quote'] corresponde apenas a um valor de atributo exato. Para um elemento que contém várias classes, use //div[contains(concat(' ', normalize-space(@class), ' '), ' quote ')], que evita corresponder quote-footer da maneira que um contains() nu faria.

P: Devo usar PHP para um grande projeto de scraping?

Para trabalhos de busca e análise em larga escala, sim — curl_multi_* lhe dá verdadeira concorrência e a extensão DOM é rápida. O ponto onde o PHP é mais fraco é na automação de navegador de longa duração, onde as ferramentas em torno do Playwright e Puppeteer são mais maduras. Uma divisão comum é usar PHP para o caminho HTTP e um serviço de navegador para as páginas que realmente precisam de um.

Na Scorretless, acessamos apenas dados disponíveis ao público, enquanto cumprem estritamente as leis, regulamentos e políticas de privacidade do site aplicáveis. O conteúdo deste blog é apenas para fins de demonstração e não envolve atividades ilegais ou infratoras. Não temos garantias e negamos toda a responsabilidade pelo uso de informações deste blog ou links de terceiros. Antes de se envolver em qualquer atividade de raspagem, consulte seu consultor jurídico e revise os termos de serviço do site de destino ou obtenha as permissões necessárias.

Artigos mais populares

Catálogo