Scraping por lenguaje 6 min de lectura

Web scraping en Python con lxml: velocidad y XPath

Por qué lxml es uno de los parsers de HTML y XML más rápidos de Python: consultas XPath, comparación con BeautifulSoup y técnicas para documentos de gran tamaño.

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

lxml es la biblioteca de parseo de HTML y XML más rápida del ecosistema Python. Está construida sobre las bibliotecas C libxml2 y libxslt, por lo que supera con creces a los parsers escritos en Python puro y, además, ofrece XPath completo, más potente que los selectores CSS. Si ha topado con el límite de rendimiento de BeautifulSoup en volúmenes grandes o necesita selecciones complejas, lxml es la elección correcta.

Este artículo profundiza en un tema de la guía general «Web scraping con Python», donde lxml aparece mencionado junto a otros parsers. Aquí lo analizamos en detalle.

Contenido

  1. Instalación y los dos módulos de lxml
  2. Parseo de HTML
  3. XPath: la base de la potencia de lxml
  4. Selectores CSS con cssselect
  5. Extracción de datos: texto, atributos y enlaces
  6. Acentos y codificaciones en lxml
  7. Documentos grandes: iterparse
  8. lxml vs BeautifulSoup
  9. Pros y contras

1. Instalación y los dos módulos de lxml

bash
pip install lxml

lxml tiene dos módulos clave:

  • lxml.html: para parsear HTML (perdona el marcado «sucio», igual que un navegador).
  • lxml.etree: para XML estricto (véase el artículo dedicado «Parseo de XML en Python»).

Para el scraping web, en el 95% de los casos necesitará lxml.html.


2. Parseo de HTML

python
import requests
from lxml import html

resp = requests.get("https://example.com", timeout=10)
tree = html.fromstring(resp.content)   # pasamos bytes: lxml detecta la codificación por sí solo

fromstring devuelve el elemento raíz del árbol. A partir de ahí se le aplican XPath o selectores CSS. También se puede parsear desde un archivo:

python
tree = html.parse("page.html").getroot()

Importante: pase resp.content (bytes) y no resp.text (cadena); así lxml leerá correctamente la declaración de codificación del propio HTML. Esto elimina la mayoría de los problemas con acentos y caracteres especiales.


3. XPath: la base de la potencia de lxml

XPath es un lenguaje para direccionar nodos dentro del árbol del documento, y es aquí donde lxml despliega todo su potencial.

python
# texto de todos los encabezados h2 con la clase title
titles = tree.xpath('//h2[@class="title"]/text()')

# atributo href de todos los enlaces
links = tree.xpath('//a/@href')

# texto dentro de un bloque concreto
price = tree.xpath('//div[@class="price"]/text()')[0]

Construcciones útiles de XPath

python
# por coincidencia parcial de la clase (cuando class="title big featured")
tree.xpath('//h2[contains(@class, "title")]/text()')

# por el texto del elemento
tree.xpath('//a[text()="Ver más"]/@href')

# n-ésimo elemento (¡la numeración empieza en 1!)
tree.xpath('(//div[@class="item"])[3]')

# ruta relativa desde un nodo encontrado
for card in tree.xpath('//div[@class="card"]'):
    name = card.xpath('.//h3/text()')      # el punto significa «desde el nodo actual»
    link = card.xpath('.//a/@href')

El punto inicial (.//) en un XPath relativo es crítico: sin él, la búsqueda partirá de la raíz del documento y no de la tarjeta actual. Es el error más frecuente entre quienes empiezan.

Ejes de XPath

XPath sabe moverse por el árbol en cualquier dirección, algo que CSS no permite:

python
# el padre del elemento
tree.xpath('//span[@class="price"]/parent::div')

# el siguiente elemento del mismo nivel
tree.xpath('//h2/following-sibling::p[1]/text()')

# el ancestro con una clase determinada
tree.xpath('//a[@id="buy"]/ancestor::div[@class="product"]')

4. Selectores CSS con cssselect

Si XPath le resulta farragoso, lxml admite selectores CSS (hace falta el paquete cssselect):

bash
pip install cssselect
python
# el método .cssselect() devuelve una lista de elementos
cards = tree.cssselect("div.product-card")
title = tree.cssselect("h1.title")[0].text_content()
links = [a.get("href") for a in tree.cssselect("a.product-link")]

CSS resulta más familiar para quien viene del frontend. Por debajo, cssselect traduce el CSS a XPath, así que la velocidad se mantiene. Para las selecciones complejas (por texto, por ancestros) seguirá haciendo falta XPath.


5. Extracción de datos: texto, atributos y enlaces

python
el = tree.cssselect(".product")[0]

el.text_content()          # todo el texto interior, etiquetas anidadas incluidas
el.get("href")             # valor de un atributo
el.attrib                  # diccionario con todos los atributos
el.tag                     # nombre de la etiqueta
el.text                    # solo el texto directo del nodo (sin los anidados)

La diferencia entre .text y .text_content() importa:

python
# <p>Hola, <b>mundo</b>!</p>
p.text              # "Hola, "          (solo hasta la etiqueta anidada)
p.text_content()    # "Hola, mundo!"    (todo el texto, de forma recursiva)

Para convertir los enlaces relativos en absolutos:

python
tree.make_links_absolute("https://example.com")
links = tree.xpath('//a/@href')   # ahora todos los enlaces son absolutos

make_links_absolute ahorra el pegado manual de URL, algo muy práctico al recorrer un sitio.


6. Acentos y codificaciones en lxml

lxml maneja sin problemas los acentos, las eñes y cualquier carácter no ASCII siempre que reciba bytes y no una cadena ya decodificada:

python
# CORRECTO: bytes; lxml leerá la codificación del <meta charset>
tree = html.fromstring(resp.content)

# ARRIESGADO: cadena ya decodificada por requests (quizá de forma errónea)
tree = html.fromstring(resp.text)

Si necesita fijar la codificación a mano (por ejemplo, cuando el servidor miente en las cabeceras):

python
from lxml import html

parser = html.HTMLParser(encoding="windows-1252")
tree = html.fromstring(resp.content, parser=parser)

La teoría completa del problema de las codificaciones está en la guía general, sección «Codificaciones».


7. Documentos grandes: iterparse

Cuando el documento es enorme (un feed XML de cientos de megabytes), cargarlo entero en memoria es un despilfarro. iterparse lo lee en flujo, procesando los elementos a medida que aparecen y liberando memoria:

python
from lxml import etree

for event, element in etree.iterparse("huge.xml", tag="item"):
    title = element.findtext("title")
    process(title)
    element.clear()                       # liberamos memoria
    while element.getprevious() is not None:
        del element.getparent()[0]        # eliminamos los nodos ya procesados

Esta técnica (lectura + clear()) permite parsear archivos que no caben enteros en la RAM. Hay más detalles sobre el parseo de XML en flujo en «Parseo de XML en Python».


8. lxml vs BeautifulSoup

Criterio lxml BeautifulSoup
Velocidad muy alta (C) menor (sin el motor lxml)
API más estricta, exige saber XPath amigable, se lee como texto
XPath completo no (solo búsqueda CSS)
HTML «sucio» bien muy bien, perdona casi todo
Memoria más eficiente, con iterparse consumo mayor
Barrera de entrada más alta más baja

En la práctica se combinan a menudo: BeautifulSoup puede usar lxml como motor (BeautifulSoup(html, "lxml")), con lo que obtiene la comodidad de la API de bs4 y la velocidad de lxml. Si necesita XPath o el parseo en flujo de archivos de gigabytes, use lxml directamente. La comparación de los distintos parsers está en la guía general, y la extracción de tablas, en un artículo aparte.


9. Pros y contras de lxml

Pros:

  • Uno de los parsers más rápidos del ecosistema Python (núcleo en C).
  • XPath completo con ejes (parent, sibling, ancestor), algo inalcanzable para CSS.
  • Soporte de CSS mediante cssselect: lo mejor de ambos mundos.
  • iterparse para el procesamiento en flujo de documentos gigantescos.
  • Consumo de memoria contenido.

Contras:

  • Curva de aprendizaje más pronunciada, sobre todo por XPath.
  • API más estricta y menos «indulgente» que la de BeautifulSoup.
  • Mensajes de error a veces crípticos.
  • La instalación exige en ocasiones bibliotecas del sistema (en algunos sistemas operativos).

Conclusión: lxml es la opción cuando priman el rendimiento y las selecciones complejas. Para tareas puntuales y máxima legibilidad, BeautifulSoup es más sencillo; para volúmenes gigantescos con selecciones CSS también merece un vistazo selectolax. Precisamente por su velocidad, lxml suele emparejarse con el scraping asíncrono: cuando se descargan cientos de páginas a la vez, un parser lento se convierte en el cuello de botella, y el motor en C de lxml lo elimina.