PHP Web Scraping: Una Guía Práctica
Expert in Web Scraping Technologies
TL;DR:
- PHP envía todo lo que un scraper básico necesita.
ext-curlobtiene,DOMDocumentmásDOMXPathanaliza, y no se involucra ningún paquete de Composer. Una extracción funcional son aproximadamente quince líneas. - Una instalación predeterminada de
php-clino es suficiente. En una caja limpia de Ubuntuphp-cliexpuso solojsonylibxml;curl,domymbstringdebieron instalarse por separado. Verifica antes de escribir el código, no después. DOMDocument::loadHTML()inundará tu salida con advertencias. Las páginas reales son HTML5 y el analizador espera HTML4. Envuelve la carga enlibxml_use_internal_errors(true)o el ruido se leerá como un fallo cuando nada falló.- La API Universal de Scraping Scrapeless devuelve un sobre JSON. El marcado se encuentra en
data, así que primero decodificas y luego analizas. chrome-phpcontrola un navegador remoto, pero su tiempo de espera predeterminado está diseñado para uno local. Medido contra un punto final de CDP en la nube, el predeterminado de 5 segundos de la biblioteca completó 2 de 6 intentos; elevarsendSyncDefaultTimeoutlo llevó a 5 de 6.- Comienza gratis: el tablero de Scrapeless emite una clave que funciona con todos los ejemplos a continuación.
Lo Que Necesitas
Cada ejemplo aquí se ejecutó en PHP 8.3.6 (cli) contra quotes.toscrape.com, un sitio publicado específicamente para la práctica de scraping.
La primera sorpresa en una máquina limpia es cuán poco incluye una instalación base de PHP. Una instalación fresca de php-cli reportó solo json y libxml del conjunto que importa aquí. Los tres que realmente necesitas llegan por separado:
bash
# 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 es el paquete que proporciona DOMDocument; la extensión se llama dom, que es la razón por la cual hacer grep por xml no encuentra nada y envía a la gente en círculos.
Obtener una Página Con ext-curl
file_get_contents() funciona para casos triviales, pero no te da código de estado, control de encabezados y ningún tiempo de espera que valga la pena. ext-curl es la base para cualquier cosa real:
php
<?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";
Eso imprime http=200 bytes=11064. Dos opciones tienen más peso del que parecen: CURLOPT_RETURNTRANSFER es lo que hace que curl_exec() devuelva el cuerpo en lugar de imprimirlo, y sin CURLOPT_USERAGENT muchos sitios responden a un cliente PHP desnudo de manera diferente o no responden en absoluto.
Análisis Con DOMDocument y DOMXPath
La extensión DOM de PHP es una implementación completa del estándar W3C DOM, y DOMXPath te da el mismo poder de consulta que cualquier biblioteca de scraping dedicada. La trampa es el cargador.
php
<?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";
Salida:
text
quotes=10
first_author=Albert Einstein
first_text=“The world as we have created it is a pr
Tres cosas valen la pena mencionar.
libxml_use_internal_errors(true) no es opcional en la práctica. DOMDocument implementa el análisis HTML4, por lo que cada elemento HTML5 y atributo sin comillas en una página moderna genera una advertencia. Si pasas por alto la llamada, un scraping exitoso oculta su propia salida bajo el ruido del analizador — el manual de PHP documenta este interruptor como la forma soportada de tomar control sobre ese informe.
El segundo argumento a DOMXPath::query() delimita la consulta a un nodo. Sin él, .//span[@class='text'] busca en todo el documento y obtienes el texto de la primera cita para cada fila. Ese solo argumento es la diferencia entre la extracción por fila y un conjunto de datos sutilmente incorrecto.
mb_substr() en lugar de substr() importa porque la página utiliza comillas curvas. substr() corta en bytes y dividirá un carácter de múltiples bytes en un UTF-8 inválido, que es exactamente el tipo de corrupción que aparece tres pasos más adelante en una base de datos.
Donde PHP Plano Se Queda Corto
El script anterior funciona porque el objetivo renderiza su contenido del lado del servidor y no le importa quién está preguntando. Dos cosas terminan eso: contenido que solo existe después de que JavaScript se ejecute, y sitios que deciden que un cliente HTTP desnudo no es un navegador. Ninguna de estas es un problema de PHP — ningún cliente HTTP resuelve ninguna de estas, en ningún lenguaje.
Enrutar solicitudes a través de un grupo de proxies maneja una parte del segundo caso, y si trabajas dentro de un marco, la guía de integración de proxy de Laravel cubre esa configuración con más profundidad de lo que este artículo hace.
En ese punto hay dos escalaciones, y son herramientas diferentes en lugar de mejores versiones la una de la otra. Mantén la ruta HTTP simple donde funciona; sigue siendo la opción más rápida y económica por un amplio margen.
Escalación Uno: La API Universal de Scraping
La primera escalación mantiene tu código con la forma de un cliente HTTP y mueve la parte difícil al lado del servidor. La solicitud es ordinaria ext-curl; la respuesta es donde se muestra la diferencia.
php
<?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
http=200 envelope_keys=code,data
bytes=11064
El sobre es la cosa que hay que interiorizar. $raw es JSON, y pasarlo a DOMDocument produce un documento sin nodos coincidentes y sin error: los selectores simplemente devuelven cero filas. Decodifica, toma data, luego analiza. Mantén esa desestructuración en un ayudante en lugar de repetir json_decode(...)['data'] en los puntos de llamada.
Aparte del recuento de bytes, el código de análisis no ha cambiado. Alimentar $apiHtml en el mismo bloque DOMXPath devuelve las mismas diez citas, que es el punto: solo cambia la obtención.
Escalamiento Dos: Un Navegador Real Desde PHP
Cuando el contenido requiere genuinamente JavaScript, necesitas un navegador. chrome-php/chrome habla el Protocolo de Herramientas de Desarrollo de Chrome y puede lanzar un Chrome local o conectarse a uno remoto a través de un WebSocket.
bash
composer require chrome-php/chrome
# Using version ^1.16 for chrome-php/chrome → v1.16.1
php
<?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
title: Quotes to Scrape
quotes on page: 10
El Tiempo de Espera por Defecto Está Dimensionado para un Navegador Local
Esa línea sendSyncDefaultTimeout es la parte que vale la pena del artículo. chrome-php lo establece en 5000 ms, lo cual es generoso para un Chrome que se ejecuta en la misma máquina y marginal para uno a través de una conexión TLS. Dos conjuntos emparejados de seis ejecuciones, mismo script, mismo objetivo, cambiando solo esa opción:
| configuración | resultado | modo de fallo |
|---|---|---|
| valor predeterminado de la biblioteca (5000 ms) | 2 exitosos, 4 fallidos | cada fallo OperationTimedOut: Operation timed out after 5s |
sendSyncDefaultTimeout => 60000 |
5 exitosos, 1 fallido | el único fallo es un error diferente, en el momento de la conexión |
Se siguen dos conclusiones, y la segunda es la que la gente pasa por alto.
Aumentar el tiempo de espera es necesario. Dejado en el valor predeterminado, la mayoría de los intentos fallaron por el mismo mensaje, y es un mensaje que engaña: menciona un tiempo de espera, por lo que parece que la página o la red están lentas, cuando lo que realmente expiró fue la propia espera de la biblioteca en un viaje de protocolo.
Aumentarlo también no es suficiente. El único fallo que sobrevivió fue Cannot connect to the browser, make sure it was not closed, levantado alrededor de cinco segundos después, mientras la conexión aún se estaba estableciendo en lugar de durante un comando. Un mayor tiempo de espera para comandos no afecta eso. Considera obtener el navegador como un paso falible en el diseño del trabajo, distinto del trabajo que realizas una vez que lo obtienes, y mantén $browser->close() en un finally para que un fallo a mitad de trabajo nunca deje a una sesión varada.
Un Diagnóstico Que Estuvo Equivocado
La primera teoría para esos tiempos de espera era que la cadena de consulta del URI nunca llegó al apretón de manos del WebSocket, llevándose ?token= con ella. Hay evidencia real para ello: Protocol::validateSocketUri() devuelve solo [$scheme, $host, $port] y descarta el path y la query por completo.
Todavía está equivocada. El apretón de manos se construye en otro lugar, por Protocol::getRequestHandshake(), que llama a un validateUri() separado que devuelve cinco elementos y vuelve a anexar explícitamente la query antes de la línea de solicitud. El token sí llega. El fallo fue latencia, y las dos funciones que analizan el mismo URI a diferentes profundidades es una coincidencia que parece exactamente un bug.
Es un recordatorio útil que en una biblioteca con más de un analizador de URI, encontrar uno que elimina tus datos no significa que sea el que está en la ruta que te importa.
Elegir Entre los Tres
| enfoque | usar cuando | costo |
|---|---|---|
ext-curl + DOMXPath |
HTML renderizado por servidor, objetivo permisivo | el más bajo; sin dependencias |
| API de Raspado Universal | bloqueado o desafiado, no se necesita JS | una llamada HTTP, sobre que desestructurar |
chrome-php + Navegador de Raspado |
el contenido requiere ejecución de JavaScript | el más alto; una sesión de navegador por trabajo |
Trabaja hacia abajo en esa lista, no hacia arriba. La mayoría de las páginas que parecen necesitar un navegador resultan incrustar sus datos en una etiqueta <script>, y una consulta DOMXPath más json_decode() supera una sesión de navegador en todos los ejes.
Conclusión
PHP es un lenguaje de raspado perfectamente razonable, y las partes que la gente espera que falten están en la biblioteca estándar. ext-curl y DOMXPath cubren páginas renderizadas por servidor por completo, con libxml_use_internal_errors(true) y un segundo argumento de ámbito para query() como los dos detalles que separan el código que funciona del código que está silenciosamente equivocado.
Cuando un objetivo deja de cooperar, escala deliberadamente. La ruta de la API mantiene tu código como un cliente HTTP y solo pide que desestructures un sobre. El camino del navegador cuesta una sesión y, si ese navegador es remoto, un elemento de configuración específico: el valor predeterminado de cinco segundos de chrome-php es una suposición de Chrome local, y dejarlo en su lugar costó cuatro de seis conexiones aquí.
¿Listo para Raspar Desde PHP?
Crea una clave en el tablero de Scrapeless y ejecuta el ejemplo ext-curl en una página que ya coleccionas. Si devuelve lo que esperas, has terminado — sin dependencias, sin navegador. La API de Scraping Universal está disponible para las páginas donde no lo está, y las tarifas actuales están en la página de precios.
FAQ
P: ¿Necesito Composer para hacer scraping con PHP?
No. Obtener datos con ext-curl y analizar con DOMDocument y DOMXPath no necesita nada más allá de las extensiones en una instalación estándar. Composer solo entra en juego para un controlador de navegador como chrome-php/chrome.
P: ¿Por qué DOMDocument imprime advertencias en cada página?
Porque implementa el análisis de HTML4 y las páginas modernas son HTML5. Las advertencias son informativas en lugar de errores. Llama a libxml_use_internal_errors(true) antes de loadHTML() y libxml_clear_errors() después, y inspecciona libxml_get_errors() cuando realmente quieras verlas.
P: ¿Por qué mi análisis no devuelve nada cuando la solicitud de API tuvo éxito?
Casi con certeza estás analizando el sobre. La API de Scraping Universal devuelve JSON con el marcado dentro de data, así que DOMDocument recibe una cadena JSON y no encuentra nodos coincidentes sin generar un error. Decodifica la respuesta y analiza data.
P: ¿Qué XPath debo usar para un atributo de clase?
//div[@class='quote'] coincide solo con un valor exacto de atributo. Para un elemento que lleva varias clases, usa //div[contains(concat(' ', normalize-space(@class), ' '), ' quote ')], que evita coincidir quote-footer de la forma en que lo haría un contains() sin modificaciones.
P: ¿Debo usar PHP para un gran proyecto de scraping?
Para trabajos de obtención y análisis a gran escala, sí — curl_multi_* te da concurrencia real y la extensión DOM es rápida. El lugar donde PHP es más débil es la automatización de navegador de larga duración, donde las herramientas alrededor de Playwright y Puppeteer son más maduras. Una división común es PHP para la ruta HTTP y un servicio de navegador para las páginas que realmente necesitan uno.
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.



