Nuestro interés en este tema es menor, pero merece la pena mencionarlo de todos modos: somos Geonode y vendemos proxies a personas que se dedican a la extracción de datos, por lo que los selectores surgen constantemente en las conversaciones de asistencia técnica. Lo que conviene aclarar antes de la comparación es que ninguna de las dos opciones influye en que te bloqueen. Una cuestión relacionada con los selectores y otra relacionada con la red parecen idénticas a simple vista —ambas dan lugar a «mi scraper ha dejado de devolver datos»— y no tienen nada en común. Si la página se ha cargado correctamente y tu expresión no ha coincidido con nada, se trata de un problema de selectores y ninguna decisión relacionada con la infraestructura lo solucionará.
La versión resumida
| CSS | XPath | |
|---|---|---|
| Seleccionar por clase, identificador o atributo | Sí, de forma clara | Sí, de forma poco elegante |
| Relaciones de descendencia e hijos | Sí | Sí |
| Seleccionar por contenido de texto | No | Sí |
| Navegar al padre o antepasado | En parte, mediante :has() | Sí, directamente |
| Navegar al hermano anterior | En parte, mediante :has() | Sí, directamente |
| Selección posicional | Familia «:nth-child()» | «position()», «last()», predicados |
| Funciones de cadena | No | Sí |
| Consultar XML con espacios de nombres | No | Sí |
| Legibilidad | Mejor | Peor |
| Herramientas y compatibilidad con navegadores | Universal | Universal para la versión 1.0 |
Dos filas son las que más peso tienen. CSS no permite seleccionar por contenido de texto en absoluto, lo que lo descarta para el patrón de extracción más habitual: encontrar un valor por la etiqueta que lo acompaña. Y XPath es más difícil de leer, lo cual importa más de lo que la gente admite cuando un compañero tiene que mantener tu código un año después.
En qué destaca el CSS
En la selección de clases y atributos, y por un amplio margen.
div.product-card
a[href^="https://"]
input[type="checkbox"]:checked
ul > li:first-child
section.content p:not(.footnote)
Los equivalentes en XPath son más largos y, en el caso de las clases, realmente poco intuitivos:
//div[contains(concat(' ', normalize-space(@class), ' '), ' product-card ')]
//a[starts-with(@href, 'https://')]
//ul/li[1]
El primero es el patrón de coincidencia de clases, y existe porque @class
es una cadena separada por un único espacio y XPath no tiene en cuenta los tokens que contiene. Un contains(@class, 'product-card')
ingenuo también coincide con product-card-large
y old-product-card
, por lo que la versión con espacios de relleno es la correcta. Además, tiene cuatro veces la longitud de div.product-card
y es considerablemente más difícil de leer.
Si tus criterios de selección son clases, identificadores, atributos y relaciones estructurales, utiliza CSS. Esto cubre la gran mayoría de las tareas reales de selección, y optar por XPath para ello supone sacrificar legibilidad a cambio de una funcionalidad que no vas a utilizar.
CSS también cuenta con mejores herramientas. El inspector de elementos de todos los navegadores genera selectores CSS de forma nativa, la mayoría de los marcos de pruebas los utilizan por defecto y document.querySelectorAll
está disponible en todas partes sin necesidad de herramientas auxiliares.
En qué destaca XPath
La coincidencia de texto, algo que CSS no puede hacer en absoluto:
//button[normalize-space()='Continue']
//a[contains(., 'Download')]
//dt[normalize-space()='Price']/following-sibling::dd[1]
Ese último patrón —buscar la etiqueta y extraer el valor adyacente— es la pieza clave de la extracción de páginas estructuradas, y no existe ninguna expresión CSS para ello porque CSS no tiene acceso al contenido de texto. Esta única capacidad es la razón por la que XPath sigue utilizándose para extraer datos de bases de código que, por lo demás, utilizan CSS en su totalidad.
Navegación por antepasados:
//span[@class='price']/ancestor::div[contains(@class,'card')][1]
Subir desde un valor hasta el contenedor que lo alberga. :has()
ofrece a CSS una forma de hacerlo, con las limitaciones que se explican a continuación.
Lógica posicional relativa al contenido:
//h2[normalize-space()='Specifications']/following-sibling::table[1]
La primera tabla después de un encabezado específico. :nth-child()
cuenta posiciones entre elementos hermanos; no puede expresar «después del elemento cuyo texto sea X».
Funciones de cadena. normalize-space()
, substring-before()
, translate()
y el resto te permiten realizar operaciones dentro de la expresión. normalize-space()
, en particular, es casi imprescindible, ya que el HTML real se formatea de forma legible y una comparación textual exacta con "\n In stock\n"
fallará.
XML con espacios de nombres. Si estás consultando XML en lugar de HTML —un mapa del sitio, una fuente RSS, una respuesta SOAP—, CSS no es la herramienta adecuada. XPath está diseñado para ello, y la gestión de espacios de nombres forma parte de su diseño.
Cómo cambió las cosas «:has()»
El avance más significativo en esta comparación, y hay muchos artículos anteriores a él.
La documentación de MDN describe :has() como «un elemento si cualquiera de los selectores relativos que se pasan como argumento coincide con al menos un elemento cuando se ancla a este elemento», lo que proporciona «una forma de seleccionar un elemento padre o un elemento hermano anterior con respecto a un elemento de referencia». Su estado de referencia es «ampliamente disponible», y es compatible con todos los navegadores desde diciembre de 2023.
Así pues, CSS ahora puede expresar cosas que antes no podía:
div.card:has(span.sold-out) /* a card containing a sold-out marker */
h1:has(+ p) /* an h1 immediately followed by a p */
li:has(~ li.active) /* an li with a later active sibling */
tr:has(td.error) /* a row containing an error cell */
Esto cubre una parte importante de lo que antes se necesitaba XPath: la selección de elementos padres y las condiciones basadas en un elemento hermano.
Hay tres limitaciones documentadas que conviene conocer. :has() «no puede anidarse dentro de otro :has()». Los pseudoelementos «no son selectores válidos dentro de :has()» y no son anclajes válidos para él. Y su especificidad es «la especificidad del selector más específico de entre sus argumentos», lo que coincide con el comportamiento de :is() y :not().
El límite más importante para la extracción no figura en esa lista: :has() sigue sin poder coincidir con texto. div:has(span) funciona; div:has(span:contains('Price')) no existe, porque :contains() no es un selector CSS estándar. Se propuso y se descartó, y los navegadores no lo implementan. Por lo tanto, el patrón «etiqueta-valor» sigue siendo territorio de XPath, independientemente de :has().
Traducciones en paralelo
La forma más rápida de hacerse una idea de las ventajas e inconvenientes es ver cómo se expresa la misma intención de ambas formas.
| Intención | CSS | XPath |
|---|---|---|
| Elemento con una clase | div.card | //div[contains(concat(' ',normalize-space(@class),' '),' card ')] |
| Elemento con un identificador | #main | //*[@id='main'] |
| El atributo existe | a[href] | //a[@href] |
| El atributo empieza por | a[href^="/docs"] | //a[starts-with(@href,'/docs')] |
| El atributo contiene | a[href*="pdf"] | //a[contains(@href,'pdf')] |
| Hijo directo | ul > li | //ul/li |
| Cualquier descendiente | div p | //div//p |
| Primer hijo | li:first-child | //li[1] |
| Último hijo | li:last-child | //li[last()] |
| Enésimo elemento secundario | li:nth-child(3) | //li[3] |
| Siguiente elemento hermano | h2 + p | //h2/following-sibling::p[1] |
| Cualquier elemento hermano posterior | h2 ~ p | //h2/following-sibling::p |
| Negación | p:not(.footnote) | //p[not(contains(@class,'footnote'))] |
| Elemento padre de una coincidencia | div:has(> span.price) | //span[contains(@class,'price')]/.. |
| Contiene texto | no es posible | //p[contains(., 'Price')] |
| Texto exacto | no es posible | //p[normalize-space()='Price'] |
| Valor junto a una etiqueta | no es posible | //dt[normalize-space()='Price']/following-sibling::dd[1] |
Al leer la tabla de arriba abajo, el patrón queda claro. Para todo lo que aparece por encima de la fila «parent», el CSS es más breve y claro, y optar por XPath supone elegir la verbosidad sin obtener ninguna ventaja. En cuanto a las tres filas de la parte inferior, no hay ninguna columna dedicada al CSS.
Hay dos traducciones que merecen una segunda mirada. La fila de la clase es el contraste más marcado de toda la comparación —nueve caracteres frente a unos setenta— y la versión XPath no es verbosa por el simple hecho de serlo, ya que la forma abreviada contains(@class,'card') realmente coincide en exceso con card-large y discard. Y la fila «primer hijo» esconde una trampa: li:first-child y //li[1] coinciden aquí, pero //li[1] es un predicado que se aplica por cada elemento padre, por lo que selecciona el primer li de cada lista del documento. El CSS no tiene ambigüedad; el XPath necesita (//li)[1] si lo que se pretende es seleccionar solo uno.
Un ejemplo mixto realista
Así es como se ve en la práctica, al extraer una página de producto.
# CSS for the structural work — clear and adequate
cards = tree.cssselect('div.product-grid > article.product-card')
for card in cards:
name = card.cssselect('h3.product-name')[0].text_content().strip()
image = card.cssselect('img.product-image')[0].get('src')
# XPath for the label-value pairs, which CSS cannot express
price = card.xpath(
".//dt[normalize-space()='Price']/following-sibling::dd[1]"
)[0].text_content().strip()
stock = card.xpath(
".//span[contains(., 'in stock') or contains(., 'out of stock')]"
)
Hay tres elementos que vale la pena copiar.
El «.» inicial en cada expresión XPath. «.//dt» busca dentro de la ficha actual; «//dt» buscaría en todo el documento desde la raíz, devolviendo la primera etiqueta coincidente de la página para cada ficha. Este es uno de los errores más comunes en el código de extracción mixta, y produce el mismo valor para cada registro: plausible, uniforme e incorrecto.
CSS cuando el CSS sea suficiente. La cuadrícula y la selección de tarjetas, el encabezado, la imagen. Escribir eso en XPath añadiría longitud y restaría claridad.
XPath solo cuando sea necesario. El precio se identifica por su etiqueta y el estado de existencias por su texto. Ninguno de los dos se puede expresar en CSS, y ambos son la razón por la que el XPath está presente en el archivo.
El resultado es un rastreador en el que cada expresión es lo más breve posible, y el lector puede ver de un vistazo qué partes dependen de la estructura y cuáles del contenido —lo que también sirve de guía para saber qué fallará cuando el sitio cambie—.
Rendimiento
Normalmente irrelevante, en ocasiones decisivo.
El CSS suele ser más rápido en los navegadores, ya que los motores optimizan en gran medida la coincidencia de selectores —se trata de una etapa crítica en el proceso de renderizado—. XPath sigue un modelo de evaluación más general.
La diferencia rara vez es relevante a la escala a la que trabaja la mayoría de la gente. Seleccionar elementos de una sola página unos cientos de veces es imperceptible en cualquier caso; la solicitud de red predomina por órdenes de magnitud.
Dónde sí importa:
Los ejes «preceding» y «following» son realmente costosos. Escanean todo el documento antes o después del nodo de contexto. Dentro de un bucle que recorre muchos nodos, eso se convierte en una operación cuadrática. Si una expresión XPath es notablemente lenta, comprueba si utiliza alguno de esos —«preceding-sibling» y «following-sibling» están limitados por el número de hermanos y funcionan bien.
// al principio de una expresión anidada vuelve a buscar desde la raíz. //div//span es más costoso de lo que parece, y .//span dentro de un bucle sobre elementos div es lo que normalmente se pretende.
:has() puede resultar costoso en documentos grandes, ya que el motor debe evaluar el selector interno para cada candidato. Está bien para la extracción; es algo a tener en cuenta en una hoja de estilo aplicada a una página grande.
Para el análisis del lado del servidor con lxml o similares, la diferencia es lo suficientemente pequeña como para que la legibilidad sea el factor decisivo.
Problemas de disponibilidad y versiones
El problema que provoca que «funcione en el probador en línea pero no en mi código».
Los navegadores implementan XPath 1.0 a través de document.evaluate, y Selenium utiliza el motor del navegador. Las características de XPath 2.0 y 3.1 —matches(), replace(), lower-case(), ends-with(), secuencias— no están disponibles allí. Cualquier fragmento de código que utilice alguna de ellas fallará.
lxml implementa XPath 1.0 para su API común, además de las extensiones EXSLT, incluyendo re:test() para expresiones regulares. Por lo tanto, una expresión basada en expresiones regulares que funcione en Python no funcionará en Selenium.
La compatibilidad con CSS en las bibliotecas de análisis suele realizarse a través de una capa de traducción que convierte CSS a XPath internamente. Esto funciona bien para los selectores comunes y no tan bien para los más recientes — :has() la compatibilidad en los analizadores del lado del servidor varía considerablemente, y un selector que funcione en un navegador puede que no funcione en tu rastreador.
Regla práctica: prueba tus selectores en el entorno en el que se ejecutarán, no en la consola de un navegador ni en una herramienta en línea. Las dos sorpresas más habituales son que las funciones de XPath 2.0 fallen en Selenium y que :has() falle en un analizador de Python.
La elección en la práctica
Un procedimiento de decisión que resuelve casi todos los casos.
Empieza con CSS. Es más legible, cuenta con mejores herramientas y resulta adecuado para la selección por clase, identificador, atributo y estructura —que es la mayor parte de las selecciones—.
Pasa a XPath cuando necesites realizar una coincidencia de texto. Esta es la razón principal y es decisiva: ninguna construcción CSS lee contenido de texto.
Cambia a XPath para la navegación por antepasados más allá de lo que cubre :has(), especialmente cuando necesites un antepasado específico varios niveles por encima, en lugar de una coincidencia condicional con un padre conocido.
Cambia a XPath para XML. Los espacios de nombres y las operaciones relacionadas con el orden de los documentos son precisamente para lo que se diseñó.
Utiliza ambos en una misma base de código. Esto es lo normal y lo sensato, más que una solución de compromiso. Un rastreador que utilice CSS para el 90 % de los casos sencillos y XPath para el 10 % más complicado es más fácil de mantener que uno que se limite a una sola opción.
Y un hábito que vale más que cualquiera de las dos opciones: comprueba si hay datos estructurados incrustados antes incluso de escribir un selector. Muchas páginas incluyen JSON-LD en un bloque <script type="application/ld+json"> porque alimenta las funciones de búsqueda, y analizarlo es mucho más estable que analizar el marcado renderizado. Resiste los rediseños que rompen todos los selectores de la página.
Lo que ninguno de los dos resuelve
Ambos son frágiles de la misma manera, y esta es la parte que cuesta tiempo real.
Los selectores estructurales fallan de forma silenciosa. Si alguien inserta una columna, td[3] pasa a corresponder a una celda diferente. No se genera ningún error; tus datos están erróneos sin que te des cuenta. Utiliza como referencia texto o identificadores siempre que puedas, y comprueba la estructura de lo que extraes: si un precio debe tener el aspecto de un precio, compruébalo.
Ninguno de los dos gestiona el contenido renderizado por el cliente. Si el marcado lo ensambla JavaScript tras la carga, ninguno de los dos encuentra nada en el HTML inicial. Se trata de un problema de obtención de datos que requiere un navegador o la API subyacente, no un problema de los selectores.
Ninguno de los dos sobrevive por sí solo a un rediseño. La solución consiste en mantener los selectores en un único lugar, de modo que la actualización tras un cambio lleve una hora en lugar de un día, y en almacenar el HTML que has analizado para poder comparar lo antiguo con lo nuevo cuando algo falle.
Y ninguno de los dos se ve afectado por la forma en que se ha obtenido la página. Y ahí es donde entramos nosotros: si el documento está intacto y tu expresión no coincide con nada, ningún cambio en la infraestructura modificará la respuesta.
Preguntas frecuentes
¿Es XPath mejor que los selectores CSS?
En general, ninguno de los dos es mejor. XPath ofrece más posibilidades: puede buscar coincidencias de texto, navegar hacia elementos ancestros y utilizar funciones de cadenas. CSS es más legible y cuenta con mayor compatibilidad con las herramientas. Utiliza CSS por defecto y XPath para aquellos casos específicos que CSS no pueda expresar.
¿Pueden los selectores CSS seleccionar por texto?
No. No existe un selector CSS estándar para el contenido de texto: se propuso :contains(), pero nunca se adoptó y los navegadores no lo implementan. Esta es la mayor limitación en cuanto a capacidades y la razón principal por la que XPath sigue utilizándose en tareas de extracción.
¿Sustituye «:has()» a XPath?
En parte. Ofrece la selección de elementos padres en CSS y la coincidencia condicional entre elementos hermanos, que antes solo eran posibles con XPath. No añade la coincidencia de texto, por lo que el patrón «etiqueta-valor» sigue necesitando XPath. Tampoco admite anidamiento, y su compatibilidad con las bibliotecas de análisis sintáctico del lado del servidor varía.
¿Qué es más rápido, XPath o CSS?
CSS suele ser más rápido en los navegadores porque la coincidencia de selectores está optimizada para la representación. La diferencia suele ser irrelevante en comparación con el tiempo de red. Donde XPath se vuelve realmente lento es en los ejes preceding y following, que escanean todo el documento.
¿Puedo utilizar XPath 2.0 en Selenium?
No. Los navegadores implementan XPath 1.0 a través de document.evaluate, y Selenium utiliza el motor del navegador. Funciones como matches(), lower-case() y ends-with() no están disponibles. Esta es la razón habitual por la que un fragmento de código funciona en un probador en línea y falla en un conjunto de pruebas.
¿Cómo selecciono un elemento padre?
En XPath, parent:: o ... En CSS, :has() ofrece una forma condicional: div:has(> span.price) selecciona el div en lugar del span. Para un antepasado específico varios niveles por encima, el eje ancestor:: de XPath es más directo.
¿Debería utilizar CSS o XPath para el web scraping?
Ambos, en la misma base de código. CSS para la selección de clases, identificadores y atributos, que es la mayor parte. XPath cuando necesites buscar coincidencias de texto: encontrar un valor por su etiqueta es el caso más típico y no tiene equivalente en CSS.
¿Cuál es más estable cuando cambia un sitio web?
Ninguno, en sí mismo: la estabilidad depende de en qué te ancles, más que del lenguaje que utilices. Anclarse en texto o en un atributo «data-*» sobrevive a un rediseño; anclarse en la posición no sobrevive a nada. Ambos lenguajes te permiten hacer ambas cosas.
Conclusión
La comparación se reduce a dos asimetrías: el CSS no puede leer texto y el XPath es más difícil de leer. Todo lo demás son detalles.
Esto hace que la regla de trabajo sea sencilla: utiliza CSS por defecto, ya que la mayoría de las selecciones se realizan por clase, identificador o atributo, y CSS los expresa de forma clara. Recurre a XPath en aquellos casos concretos en los que necesites algo que solo él ofrece: sobre todo, la coincidencia de texto; después, la navegación por antepasados y las funciones de cadenas. Mezclarlos es habitual, y un código fuente que utilice cada uno para lo que mejor se le da es más fácil de mantener que uno que se decante por un solo lado.
:has() ha reducido considerablemente la brecha y merece la pena adoptarlo cuando sea adecuado: selección de padres y condiciones de hermanos en CSS puro, ampliamente disponible desde finales de 2023. Solo ten en cuenta que no aborda el texto y que la compatibilidad de los analizadores del lado del servidor es menos uniforme que la de los navegadores.
A continuación, comprueba tu entorno antes de confiar en cualquier expresión. XPath 1.0 en navegadores y Selenium, funciones adicionales de EXSLT en lxml, compatibilidad variable con :has() en bibliotecas de análisis sintáctico… La sorpresa más habitual con los selectores no es un error de sintaxis, sino una característica que existe en otro lugar distinto al que estás ejecutándola.
