DOMDocument es la extensión de PHP que implementa la especificación DOM de W3C. A diferencia de SimpleXML, DOMDocument puede trabajar con HTML malformado (mediante loadHTML()), ofrece control completo sobre los nodos del árbol y resulta imprescindible para scraping, generación de HTML dinámico o modificaciones quirúrgicas de documentos.
Cargar HTML y XML
<?php
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
// Desde string HTML (soporta HTML malformado)
libxml_use_internal_errors(true);
$dom->loadHTML('<!DOCTYPE html><html><body><p>Hola<p>Mundo</body></html>');
libxml_clear_errors();
// Desde fichero HTML
$dom->loadHTMLFile('pagina.html');
// XML bien formado
$dom->loadXML('<?xml version="1.0"?><root><item>Texto</item></root>');
// Con encoding correcto para HTML en UTF-8
libxml_use_internal_errors(true);
$dom->loadHTML('<!DOCTYPE html><meta charset="UTF-8">' . $html_utf8);
Ejemplo 1: extraer precios de una página web
<?php
function extraerPrecios(string $html): array {
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML($html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// Seleccionar elementos con clase "precio"
$nodos = $xpath->query('//*[contains(@class, "precio")]');
$precios = [];
foreach ($nodos as $nodo) {
$texto = trim($nodo->textContent);
// Extraer el número del texto (ej: "89,99 " ? 89.99)
if (preg_match('/[d.,]+/', $texto, $m)) {
$precios[] = (float) str_replace(',', '.', $m[0]);
}
}
return $precios;
}
$html = file_get_contents('https://ejemplo.com/productos');
$precios = extraerPrecios($html);
echo "Precio mínimo: " . min($precios) . " ";
DOMXPath: consultas sobre el documento
<?php
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML(file_get_contents('https://ejemplo.com'));
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// Todos los enlaces
$enlaces = $xpath->query('//a[@href]');
foreach ($enlaces as $a) {
echo $a->getAttribute('href') . ' ' . $a->textContent . "n";
}
// El primer h1
$h1 = $xpath->query('//h1')->item(0);
echo $h1 ? $h1->textContent : 'Sin título';
// Imágenes con alt vacío (accesibilidad)
$sinAlt = $xpath->query('//img[not(@alt) or @alt=""]');
echo "Imágenes sin alt: " . $sinAlt->length;
// Párrafos dentro de un div concreto
$parrafos = $xpath->query('//div[@id="contenido"]//p');
foreach ($parrafos as $p) {
echo $p->textContent . "n";
}
Ejemplo 2: limpiar HTML de usuario
Eliminar etiquetas peligrosas (script, iframe, atributos on*) manteniendo el resto del marcado:
<?php
function limpiarHTML(string $html, array $permitidas = ['p', 'a', 'strong', 'em', 'ul', 'ol', 'li', 'br']): string {
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('<div id="_root">' . $html . '</div>', LIBXML_HTML_NOIMPLIED);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
// Eliminar script, style, iframe y etiquetas no permitidas
$peligrosas = $xpath->query(
'//*[not(self::' . implode(' or self::', $permitidas) . ')]'
. '[not(self::html or self::body or self::div[@id="_root"])]'
);
foreach (iterator_to_array($peligrosas) as $nodo) {
// Reemplazar el nodo por sus hijos de texto
$padre = $nodo->parentNode;
while ($nodo->firstChild) {
$padre->insertBefore($nodo->firstChild, $nodo);
}
$padre->removeChild($nodo);
}
// Eliminar atributos on* (onclick, onload, etc.)
foreach ($xpath->query('//@*[starts-with(name(), "on")]') as $attr) {
$attr->ownerElement->removeAttributeNode($attr);
}
$root = $dom->getElementById('_root');
$resultado = '';
foreach ($root->childNodes as $hijo) {
$resultado .= $dom->saveHTML($hijo);
}
return trim($resultado);
}
Ejemplo 3: construir HTML dinámicamente
<?php
function generarTabla(array $datos, array $cabeceras): string {
$dom = new DOMDocument('1.0', 'UTF-8');
$tabla = $dom->createElement('table');
$tabla->setAttribute('class', 'tabla-datos');
// Cabecera
$thead = $dom->createElement('thead');
$tr = $dom->createElement('tr');
foreach ($cabeceras as $cab) {
$th = $dom->createElement('th', htmlspecialchars($cab));
$tr->appendChild($th);
}
$thead->appendChild($tr);
$tabla->appendChild($thead);
// Cuerpo
$tbody = $dom->createElement('tbody');
foreach ($datos as $fila) {
$tr = $dom->createElement('tr');
foreach ($fila as $celda) {
$td = $dom->createElement('td', htmlspecialchars((string)$celda));
$tr->appendChild($td);
}
$tbody->appendChild($tr);
}
$tabla->appendChild($tbody);
$dom->appendChild($tabla);
return $dom->saveHTML($tabla);
}
$productos = [
['Teclado', 89.99, 42],
['Ratón', 29.99, 150],
];
echo generarTabla($productos, ['Producto', 'Precio', 'Stock']);
Ejemplo 4: leer un feed RSS con namespaces
<?php
function parsearRSS(string $url): array {
$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->load($url);
libxml_clear_errors();
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('media', 'http://search.yahoo.com/mrss/');
$xpath->registerNamespace('dc', 'http://purl.org/dc/elements/1.1/');
$items = [];
foreach ($xpath->query('//item') as $item) {
$imagen = $xpath->query('media:thumbnail/@url', $item)->item(0);
$autor = $xpath->query('dc:creator', $item)->item(0);
$items[] = [
'titulo' => $xpath->query('title', $item)->item(0)?->textContent,
'enlace' => $xpath->query('link', $item)->item(0)?->textContent,
'fecha' => $xpath->query('pubDate', $item)->item(0)?->textContent,
'imagen' => $imagen?->nodeValue,
'autor' => $autor?->textContent,
];
}
return $items;
}
Modificar y guardar el documento
<?php
$dom = new DOMDocument();
$dom->loadHTMLFile('pagina.html');
$xpath = new DOMXPath($dom);
// Añadir clase a todos los h2
foreach ($xpath->query('//h2') as $h2) {
$claseActual = $h2->getAttribute('class');
$h2->setAttribute('class', trim($claseActual . ' seccion'));
}
// Insertar un nodo nuevo
$aviso = $dom->createElement('div');
$aviso->setAttribute('class', 'aviso');
$aviso->textContent = 'Contenido actualizado';
$body = $dom->getElementsByTagName('body')->item(0);
$body->insertBefore($aviso, $body->firstChild);
// Eliminar un nodo
$anuncios = $xpath->query('//div[@id="publicidad"]');
foreach (iterator_to_array($anuncios) as $nodo) {
$nodo->parentNode->removeChild($nodo);
}
// Guardar
$dom->saveHTMLFile('pagina_modificada.html');
echo $dom->saveHTML(); // string
Errores frecuentes
- No usar libxml_use_internal_errors(true): el HTML real casi siempre tiene errores que DOMDocument reporta con warnings. Silenciarlos con esta función es la práctica estándar.
- Modificar una NodeList mientras se itera: eliminar nodos en un foreach de DOMNodeList produce comportamientos inesperados. Usa
iterator_to_array()primero. - textContent vs nodeValue:
textContentdevuelve el texto completo incluyendo descendientes;nodeValuesolo el del nodo actual. Para elementos de texto, usatextContent. - Encoding en loadHTML: si el HTML no declara charset UTF-8, DOMDocument puede malinterpretar caracteres. Añade
<meta charset="UTF-8">al principio del string o usamb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8').
