En el material anterior — «Parser del DOM» — vimos cómo el navegador y las bibliotecas convierten el HTML en un árbol de nodos y cómo recorrer ese árbol por código. Pero recorrer el DOM «a mano», saltando de padre a hijo, resulta tedioso y frágil. En la práctica del scraping de sitios web, el nodo buscado casi siempre se localiza con una única expresión corta: un selector CSS.
Los selectores CSS nacieron para la maquetación: con ellos se describe a qué elementos aplicar un estilo. Pero esa misma sintaxis sirve de maravilla para encontrar elementos. Por eso casi todas las herramientas de scraping — de BeautifulSoup a Scrapy o Playwright — admiten la selección por CSS. Repasemos las construcciones una por una.
Para qué sirven los selectores CSS en el scraping
Un selector es una forma declarativa de decir «deme todos los elementos que tengan este aspecto». En lugar de una decena de líneas recorriendo el árbol, se escribe una sola línea, y el motor desciende por el DOM y devuelve las coincidencias. El resultado es:
- compacto — un selector en vez de un bucle;
- legible — la expresión se parece a la descripción del lugar que ocupa el elemento en la página;
- portable — el mismo selector funciona en Python, en JS y en la consola del navegador (
document.querySelectorAll).
La habilidad clave al scrapear consiste en mirar la estructura de la página en DevTools y componer un selector que dé exactamente en el blanco y, además, sobreviva a los pequeños cambios de maquetación.
Selectores básicos
Son los átomos con los que se construye todo lo demás.
| Selector | Qué selecciona | Ejemplo |
|---|---|---|
* |
cualquier elemento | * |
div |
elementos por nombre de etiqueta | a, span, tr |
.price |
elementos con la clase price |
.product-card |
#main |
el elemento con id="main" |
#catalog |
La clase y la etiqueta pueden pegarse sin espacio — es un «Y» lógico: a.button significa «enlace que tiene la clase button». Y div.card.featured es un div con dos clases a la vez.
span.price.current /* <span class="price current"> */
li#first-item /* <li id="first-item"> */Agrupación
La coma une varios selectores en una sola lista: se devuelven los elementos que encajen con cualquiera de ellos:
h1, h2, h3 /* todos los encabezados de estos tres niveles */
.price-new, .price-old /* tanto el precio nuevo como el antiguo */Resulta útil cuando el producto lleva la clase price-new en unas fichas y price-old en otras, y hay que capturar ambas variantes con una sola consulta.
Combinadores: relaciones entre elementos
Los combinadores describen cómo se sitúan los elementos unos respecto a otros dentro del árbol. Es la parte más importante para un scraping robusto.
Descendiente (espacio)
A B — un elemento B situado en algún punto dentro de A, a cualquier profundidad.
.product-card .price /* .price en cualquier punto dentro de la ficha */Hijo directo (>)
A > B — B es hijo inmediato de A. No desciende más allá.
ul.menu > li /* solo los ítems del primer nivel del menú, sin los anidados */La diferencia es de fondo: .menu li capturará también los ítems de los submenús; .menu > li, no.
Adyacente (+)
A + B — el elemento B que va justo después de A y comparte padre con él.
h2 + p /* el primer párrafo inmediatamente después del encabezado */
.label + .value /* el valor pegado justo detrás de su etiqueta */Hermanos generales (~)
A ~ B — todos los B que van después de A bajo el mismo padre (no necesariamente pegados a él).
.in-stock ~ .price /* todos los precios que siguen a la marca «en stock» */Selectores de atributos
Con frecuencia el elemento no tiene una clase cómoda, pero sí un atributo característico. Los selectores de atributos salvan la situación cuando las clases son dinámicas o están llenas de ruido.
| Construcción | Significado |
|---|---|
[data-id] |
existe el atributo data-id (con cualquier valor) |
[type="submit"] |
coincidencia exacta del valor |
[class~="price"] |
el valor contiene la palabra price entre palabras separadas por espacios |
[href^="https"] |
el valor empieza por https |
[href$=".pdf"] |
el valor termina en .pdf |
[href*="product"] |
el valor contiene la subcadena product |
[lang|="en"] |
el valor es en o empieza por en- |
[type="text" i] |
la bandera i — comparación sin distinguir mayúsculas de minúsculas |
a[href^="/catalog/"] /* enlaces dentro del catálogo */
img[src$=".webp"] /* imágenes en formato webp */
div[data-testid*="product"] /* contenedores con la marca de producto en un atributo data */
input[name="csrf"] /* campo oculto del token */Los selectores de atributos son especialmente valiosos en la monitorización de precios de tiendas online: las grandes plataformas suelen exponer los datos en atributos data-* (data-price, data-sku, data-currency), y agarrarse a ellos es más fiable que a unas clases que cambian con cada release del frontend.
Pseudoclases: selección por posición y estado
Las pseudoclases filtran los elementos por su posición entre los vecinos o por condiciones adicionales.
Posicionales
li:first-child /* primer hijo */
li:last-child /* último hijo */
tr:nth-child(2) /* segunda fila */
tr:nth-child(odd) /* filas impares */
tr:nth-child(even) /* filas pares */
tr:nth-child(3n) /* una de cada tres */
li:nth-last-child(1) /* el primero empezando por el final */La fórmula an+b de :nth-child() es flexible: nth-child(2n+1) selecciona los impares; nth-child(n+3), todos a partir del tercero.
Por tipo de etiqueta
:nth-child cuenta a todos los hermanos seguidos, mientras que :nth-of-type solo cuenta los elementos de la misma etiqueta:
p:first-of-type /* el primer párrafo entre sus hermanos */
img:nth-of-type(2) /* la segunda imagen */
span:last-of-type /* el último span */Esto salva la situación cuando el <p> buscado es el segundo párrafo, pero antes de él se han colado etiquetas de otro tipo.
Negación y estado
input:not([type="hidden"]) /* todos los campos salvo los ocultos */
li:not(.sold-out) /* productos que no figuran como agotados */
a:not([rel="nofollow"]) /* enlaces sin nofollow */:not() acepta dentro otro selector y lo invierte. También existen :empty (elementos vacíos), :checked, :disabled y otros, pero en el scraping estático lo que más se usa son precisamente las pseudoclases posicionales y :not().
Tenga en cuenta que los pseudoelementos (
::before,::after,::first-line) no se usan en el scraping: describen contenido generado por los estilos, que no existe en el HTML original.
Montamos un selector complejo
Las expresiones reales combinan todo lo anterior. Un ejemplo: extraer el precio vigente de la ficha de producto de un catálogo:
div.product-card a[href^="/product/"] span.price:not(.price--old)Se lee así: dentro de la ficha de producto, en el enlace hacia la página del producto, tomar el span con la clase price que no sea el precio antiguo (tachado).
Herramientas: dónde funciona esto
Los selectores CSS están soportados por casi todo el stack de scraping.
Python — BeautifulSoup (a través de la biblioteca soupsieve):
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
# .select() devuelve una lista; .select_one(), el primer elemento
cards = soup.select("div.product-card")
for card in cards:
title = card.select_one("h3.title").get_text(strip=True)
price = card.select_one("span.price:not(.price--old)").get_text(strip=True)
print(title, price)Python — parsel / Scrapy (un envoltorio sobre lxml, rápido):
from parsel import Selector
sel = Selector(text=html)
prices = sel.css("span.price::text").getall() # ::text es una extensión propia de parsel
links = sel.css("a.product-link::attr(href)").getall()Aquí ::text y ::attr(name) son pseudoelementos no estándar de Scrapy/parsel: extraen directamente el texto o el atributo, sin necesidad de una llamada aparte.
JavaScript / navegador — API nativa, la misma sintaxis:
const cards = document.querySelectorAll("div.product-card");
const price = document.querySelector("span.price:not(.price--old)")?.textContent.trim();Playwright / Selenium — para páginas dinámicas, donde los datos se cargan mediante scripts:
# Playwright
price = page.locator("div.product-card span.price").first.inner_text()Es decir: una vez dominados los selectores, los usará tanto en scrapers HTTP ligeros como en navegadores headless.
Aplicación en la monitorización de precios
En una tarea típica de monitorización de precios de tiendas online el guion es este:
- Lista de productos del catálogo. Un selector del tipo
div.catalog__itemlocaliza todas las fichas en la página de resultados. - Enlace al producto. De cada ficha,
a[href^="/product/"]::attr(href)extrae la URL. - Precio y disponibilidad. En la página del producto,
[data-price]::attr(data-price)ospan.price--currentda el precio, y.availability:not(.out-of-stock)confirma que el producto está a la venta. - Precio antiguo y descuento.
span.price--oldcaptura el precio tachado para poder calcular el tamaño del descuento.
Como cada competidor maqueta a su manera, para cada plataforma se escribe su propio juego de selectores, y el motor de scraping de sitios web los aplica según un calendario y vuelca el resultado en una tabla común para compararlo.
Errores frecuentes y consejos
- No se aferre a clases «basura». Las clases del tipo
.css-1ab2c3las generan los empaquetadores y cambian con cada despliegue. Prefiera atributosdata-*,idcon significado o combinadores estructurales. - Use
>allí donde la estructura importa. Así se descartan coincidencias accidentales en bloques anidados. - Compruebe el selector en DevTools. El comando
$$("su selector")en la consola del navegador muestra al instante cuántos elementos se han encontrado. - Prevea variantes. Enumere, separados por comas, varios posibles nombres de clase del precio: eso aumenta la resistencia frente a los tests A/B de la tienda.
- Recuerde los límites de CSS. Los selectores no saben «subir hasta el padre» a partir del contenido ni filtrar por el texto del nodo. Donde CSS no llega, entran en juego XPath o las expresiones regulares, de las que hablaremos en el próximo material.
Conclusión
Los selectores CSS son la herramienta principal de direccionamiento dentro del DOM. Los selectores básicos (etiqueta, clase, id) definen el «qué»; los combinadores (espacio, >, +, ~), el «dónde»; y los selectores de atributos junto con las pseudoclases añaden condiciones precisas. Juntos cubren la inmensa mayoría de las tareas de scraping de sitios web, y un selector bien construido hace que el scraper resista los pequeños retoques de la maquetación, algo crítico cuando se trata de una monitorización de precios de tiendas online periódica.
En el próximo artículo veremos cómo extraer datos de texto «en bruto» con expresiones regulares, allí donde la estructura del DOM ya no basta.