beautifulsoup4 y lxml
BeautifulSoup analiza HTML/XML desordenado en un árbol navegable: etiquetas, atributos, búsqueda de texto. Combínalo con lxml como backend del analizador para obtener velocidad y XPath cuando los documentos crecen o se vuelven malformados.
Receta
from bs4 import BeautifulSoup
html = '<div class="item"><a href="/p/1">Widget</a><span class="price">$9</span></div>'
soup = BeautifulSoup(html, "lxml")
for item in soup.select(".item"):
name = item.find("a").get_text(strip=True)
price = item.find(class_="price").get_text(strip=True)
print(name, price)Cuándo usar esto:
- Extracción de datos de exportaciones HTML o portales heredados sin API
- Limpieza de HTML de CMS antes de su almacenamiento o visualización
- Análisis de respuestas XML RSS/Atom o SOAP
- Fixtures de prueba que capturan fragmentos de HTML
Ejemplo de Trabajo
from __future__ import annotations
from dataclasses import dataclass
import httpx
from bs4 import BeautifulSoup
@dataclass
class Product:
sku: str
title: str
price: str
def parse_catalog(html: str) -> list[Product]:
soup = BeautifulSoup(html, "lxml")
products: list[Product] = []
for card in soup.select("article.product"):
sku = card.get("data-sku")
title_el = card.select_one("h2.title")
price_el = card.select_one("span.price")
if not sku or not title_el or not price_el:
continue
products.append(
Product(
sku=sku,
title=title_el.get_text(strip=True),
price=price_el.get_text(strip=True),
)
)
return products
def fetch_catalog(url: str) -> list[Product]:
with httpx.Client(timeout=10.0, headers={"User-Agent": "catalog-bot/1.0"}) as client:
response = client.get(url)
response.raise_for_status()
return parse_catalog(response.text)
if __name__ == "__main__":
sample = """
<article class="product" data-sku="W1">
<h2 class="title">Widget</h2><span class="price">$9.00</span>
</article>
"""
print(parse_catalog(sample))Lo que esto demuestra:
- Selectores CSS a través de
select/select_one - Análisis defensivo cuando faltan nodos
get_text(strip=True)para texto normalizado- Separación de la obtención HTTP de la lógica de análisis para pruebas
Inmersión Profunda
Cómo Funciona
- Backends de análisis -
html.parser(stdlib),lxml(C rápido),html5lib(el más permisivo). - Navegación -
.parent,.next_sibling,.find,.find_all,.select(CSS). - XPath de lxml -
tree.xpath("//div[@class='item']")cuando CSS es incómodo. - Codificación - BeautifulSoup detecta la codificación de las etiquetas meta; pasa
from_encodingcuando sea incorrecto.
Elección del Analizador
| Analizador | Velocidad | Permisivo | Dependencia |
|---|---|---|---|
| lxml | Rápido | Moderado | libxml2 |
| html.parser | Lento | Moderado | ninguno |
| html5lib | El más lento | Muy | html5lib |
Notas de Python
# Analizar XML con lxml directamente para esquemas estrictos
from lxml import etree
root = etree.fromstring(xml_bytes)
for node in root.xpath("//item[@id]"):
print(node.get("id"), node.text)Trampas Comunes
- Scraping sin permiso - Riesgo legal y de Términos de Servicio. Solución: usar API oficiales; limitar las solicitudes.
- Selectores frágiles - Las clases CSS cambian en el rediseño. Solución: preferir atributos
data-*; pruebas de contrato sobre fixtures de HTML. - No manejar paginación - Conjuntos de datos incompletos. Solución: seguir
rel=nexto el cursor de la API. - Ejecutar scripts extraídos - XSS si se vuelve a mostrar HTML sin procesar. Solución: extraer solo texto o sanitizar con bleach.
- Memoria en páginas enormes - Cargar 50 MB de HTML. Solución: análisis en streaming con
lxml iterparse. - Zona horaria en fechas extraídas - Cadenas ambiguas. Solución: analizar a UTC en el límite de extracción.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| API JSON Oficial | El proveedor ofrece una API estable | No hay API y HTML de baja frecuencia de cambio |
selectolax | Máxima velocidad de análisis | Necesita ejemplos del ecosistema BeautifulSoup |
| Playwright/Selenium | SPAs renderizados con JavaScript | HTML estático del servidor |
feedparser | Feeds RSS/Atom | Catálogos HTML arbitrarios |
Preguntas Frecuentes
¿BeautifulSoup o lxml por separado?
Usa BeautifulSoup por su ergonomía; backend lxml para velocidad. lxml directo cuando necesites XPath en XML estricto.
¿Cómo manejo URLs relativas?
urllib.parse.urljoin(base_url, href) en los valores extraídos de a["href"].
¿Cómo pruebo los analizadores?
Mantén fixtures de HTML en tests/fixtures/ y afirma dataclasses analizados; sin red en pruebas unitarias.
¿BeautifulSoup modifica el HTML?
Puede embellecer o reescribir etiquetas; trata la salida como derivada, no como fuente autorizada.
¿Cómo respeto robots.txt?
Usa urllib.robotparser o un framework de scraping que verifique robots antes de la obtención.
¿Qué pasa con las páginas autenticadas?
Pasa cookies de sesión a través del cliente httpx; nunca confirmes credenciales; usa un gestor de secretos.
¿Cómo evito ser bloqueado?
Limita la tasa, rota los agentes de usuario de forma responsable, almacena en caché las respuestas ETag; no hagas crawling agresivo.
¿Analizar tablas a CSV?
pandas.read_html utiliza lxml/html5lib internamente; bueno para la extracción de tablas única.
¿Espacios de nombres en XML?
Registra espacios de nombres en lxml xpath: namespaces={"ns": "http://..."}.
¿Puedo usar BS4 en código asíncrono?
El análisis consume CPU; ejecuta parse_catalog(html) en asyncio.to_thread después de la obtención asíncrona.
Relacionado
- httpx y requests - obtención de HTML
- re - Expresiones Regulares - limpieza de texto después del análisis
- Habilidad de Pipeline de Datos - ETL desde fuentes web
- Validación de Entrada - sanitización de contenido
Versiones de Stack: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.