Tecnologías y protecciones 6 min de lectura

Parser del DOM: análisis detallado

Qué es un parser del DOM y cómo funciona por dentro: construcción del árbol del documento, navegación por nodos y diferencias con los enfoques en flujo y con regex.

EW
Equipo Web-Scraping.es
Recopilación de datos para las necesidades del negocio
Publicado: 4 febrero 2025

Qué es el DOM

El DOM (Document Object Model) es la representación de un documento HTML o XML en forma de árbol de objetos. Cada etiqueta, atributo y fragmento de texto se convierte en un nodo (node), y el anidamiento de las etiquetas define la jerarquía «padre → hijo».

El parser del DOM es el componente que lee el marcado (una cadena o un flujo de bytes) y construye a partir de él ese árbol en memoria. Una vez construido, se puede trabajar con el árbol de forma programática: buscar elementos, leer atributos, modificar, eliminar y añadir nodos, o serializarlo de vuelta a texto.

Cómo funciona

El parseo transcurre en dos etapas principales:

  1. Tokenización — el flujo de caracteres se divide en tokens (etiqueta de apertura, etiqueta de cierre, texto, comentario, etc.).
  2. Construcción del árbol — los tokens se ensamblan en una estructura jerárquica según las reglas de anidamiento. Para HTML son las reglas de la especificación WHATWG, que, entre otras cosas, «reparan» el marcado incorrecto (etiquetas sin cerrar, anidamiento erróneo).

Parseo DOM frente a SAX

Existen dos enfoques radicalmente distintos para analizar un documento.

Enfoque Memoria Acceso Cuándo usarlo
DOM Todo el documento en memoria Acceso aleatorio a cualquier nodo Web scraping, procesado de configuraciones, la mayoría de tareas
SAX / en flujo Consumo casi nulo Solo secuencial, por eventos XML muy grandes (de cientos de MB a gigabytes)

DOM carga el documento completo y ofrece un cómodo acceso aleatorio, pero exige una memoria proporcional al tamaño del documento.

SAX no construye ningún árbol: genera eventos («empezó una etiqueta», «terminó una etiqueta», «apareció texto») a medida que lee. Apenas necesita memoria, pero el código es más difícil de escribir y no hay acceso aleatorio.

La mayoría de las bibliotecas populares de más abajo son parsers DOM.

Formas de buscar en el árbol

Una vez construido el árbol, los elementos se suelen buscar de dos maneras.

Selectores CSS — los mismos que en la maquetación: div.post > a, #main .title, ul li:first-child. Resultan familiares a los desarrolladores front-end y son legibles.

XPath — un lenguaje de consultas sobre el árbol, más potente que CSS: //div[@class="post"]/a/@href. Admite condiciones, el salto al padre (..) y la búsqueda por texto (//a[contains(text(),"Comprar")]). Se usa más a menudo con XML, aunque muchas bibliotecas HTML también lo soportan.

Regla de elección: si le basta con CSS, use CSS: es más legible. XPath hace falta cuando se necesita navegar al padre o a un ancestro, seleccionar por texto o expresar condiciones complejas.


Ejemplos en varios lenguajes

En todos los ejemplos se resuelve la misma tarea: encontrar los enlaces dentro de div.post y mostrar su texto y su href.

Python

Las bibliotecas más populares son BeautifulSoup (simple, tolerante) y lxml (rápida, basada en la biblioteca C libxml2, con soporte de XPath).

BeautifulSoup

python
from bs4 import BeautifulSoup

html = """
<html><body>
  <div class="post">
    <h2>Título</h2>
    <a href="https://example.com">Enlace</a>
  </div>
</body></html>
"""

soup = BeautifulSoup(html, "lxml")   # el parser puede cambiarse por "html.parser"

# Búsqueda por etiqueta
print(soup.find("h2").text)

# Selectores CSS
for a in soup.select("div.post a"):
    print(a.text, a.get("href"))

lxml con XPath

python
from lxml import html as lxml_html

tree = lxml_html.fromstring(html)
hrefs = tree.xpath('//div[@class="post"]/a/@href')
print(hrefs)  # ['https://example.com']

En la biblioteca estándar de Python también están los módulos integrados html.parser y xml.dom.minidom, que no requieren instalación.

JavaScript / Node.js

En el navegador, el parser del DOM viene integrado — DOMParser y querySelector:

javascript
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");

doc.querySelectorAll("div.post a").forEach(a => {
  console.log(a.textContent, a.href);
});

En Node.js se usan Cheerio (ligera, con API al estilo jQuery) o jsdom (emulación completa del DOM).

javascript
import * as cheerio from "cheerio";

const $ = cheerio.load(html);
$("div.post a").each((i, el) => {
  console.log($(el).text(), $(el).attr("href"));
});

jsdom

javascript
import { JSDOM } from "jsdom";

const dom = new JSDOM(html);
dom.window.document.querySelectorAll("div.post a")
  .forEach(a => console.log(a.textContent, a.href));

Java

El estándar de facto es jsoup, muy cómodo para HTML, con selectores CSS.

java
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;

Document doc = Jsoup.parse(html);
for (Element link : doc.select("div.post a")) {
    System.out.println(link.text() + " " + link.attr("href"));
}

Para el XML estricto, el JDK incluye JAXP (javax.xml.parsers.DocumentBuilder), un parser DOM de serie, además de SAX/StAX para el análisis en flujo.

PHP

La clase integrada DOMDocument, con soporte de XPath

php
$dom = new DOMDocument();
@$dom->loadHTML($html);          // @ silencia los avisos por el HTML «sucio»
$xpath = new DOMXPath($dom);

foreach ($xpath->query('//div[@class="post"]/a') as $node) {
    echo $node->textContent . ' ' . $node->getAttribute('href') . PHP_EOL;
}

La alternativa con selectores CSS es Symfony DomCrawler

C# / .NET

HtmlAgilityPack — el clásico, trabaja con XPath

c#
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(html);

foreach (var link in doc.DocumentNode.SelectNodes("//div[@class='post']/a"))
{
    Console.WriteLine(link.InnerText + " " + link.GetAttributeValue("href", ""));
}

AngleSharp — más moderno, con un parseo fiel a la especificación y selectores CSS

c#
var context = AngleSharp.BrowsingContext.New(AngleSharp.Configuration.Default);
var doc = await context.OpenAsync(req => req.Content(html));

foreach (var a in doc.QuerySelectorAll("div.post a"))
    Console.WriteLine(a.TextContent + " " + a.GetAttribute("href"));

Go

goquery ofrece una API al estilo jQuery

go
import (
    "fmt"
    "strings"
    "github.com/PuerkitoBio/goquery"
)

doc, _ := goquery.NewDocumentFromReader(strings.NewReader(html))
doc.Find("div.post a").Each(func(i int, s *goquery.Selection) {
    href, _ := s.Attr("href")
    fmt.Println(s.Text(), href)
})

Por debajo, goquery utiliza el paquete oficial de bajo nivel golang.org/x/net/html, que también puede usarse directamente.

Ruby

Nokogiri — soporta tanto CSS como XPath

ruby
require "nokogiri"

doc = Nokogiri::HTML(html)
doc.css("div.post a").each do |link|
  puts "#{link.text} #{link['href']}"
end

Tabla comparativa de bibliotecas

Lenguaje Biblioteca Selectores Particularidades
Python BeautifulSoup CSS Simple, tolera errores de marcado
Python lxml CSS + XPath Rápida (backend C libxml2)
JS (navegador) DOMParser CSS Integrada en el navegador
Node.js Cheerio CSS Ligera, al estilo jQuery
Node.js jsdom CSS Emulación completa del DOM
Java jsoup CSS Estándar de facto para HTML
PHP DOMDocument XPath Integrada en PHP
C# HtmlAgilityPack XPath El clásico del ecosistema .NET
C# AngleSharp CSS Moderno, parseo correcto
Go goquery CSS Al estilo jQuery
Ruby Nokogiri CSS + XPath Rápida (backend en C)

Observaciones prácticas

El HTML es «sucio» por naturaleza. El marcado real suele contener etiquetas sin cerrar y errores de anidamiento. Los parsers orientados a HTML (BeautifulSoup, jsoup, AngleSharp, Nokogiri en modo HTML) los toleran y los corrigen. Los parsers XML estrictos, ante una entrada así, fallan: para XML se necesita un marcado válido.

Parser ≠ navegador. Las bibliotecas mencionadas analizan únicamente el HTML original que llegó del servidor. Si el contenido lo carga JavaScript ya en el navegador, un parser del DOM corriente no lo verá. Para esos sitios hacen falta navegadores headless:

Primero ejecutan los scripts y después entregan el DOM ya listo.

Rendimiento. Los parsers en C (lxml, el backend libxml2 de Nokogiri) son un orden de magnitud más rápidos que las implementaciones puras en Python o Ruby. Para XML enormes, elija SAX/StAX en flujo en lugar de DOM, para no chocar contra la memoria.

Elección de selectores. Si le basta con CSS, use CSS: es más legible. XPath hace falta para navegar al padre o a un ancestro, seleccionar por texto o expresar condiciones complejas.