Nuestro interés, dicho sin rodeos: somos Geonode y vendemos proxies a personas que se dedican al scraping, por lo que XPath está relacionado con nuestro negocio. Vale la pena señalar que un selector que no funciona nunca es un problema del proxy. Si tu extracción ha dejado de funcionar, es porque la página web ha cambiado su código HTML, y ni todo el ancho de banda ni la rotación de direcciones pueden solucionar eso. Ambos casos se confunden porque ambos se manifiestan como «mi rastreador ha dejado de devolver datos», pero un fallo del proxy produce respuestas bloqueadas y páginas de error, mientras que un fallo del selector produce respuestas correctas que no arrojan ningún resultado. Comprueba el HTML sin procesar antes de comprobar cualquier otra cosa. Si la página está ahí y tu XPath devuelve un conjunto de nodos vacío, este artículo te resulta relevante y tu proxy funciona correctamente.
Qué selecciona realmente «preceding-sibling»
La especificación de XPath 1.0 lo define en una sola línea: el eje «preceding-sibling» «contiene todos los hermanos anteriores del nodo de contexto; si el nodo de contexto es un nodo de atributo o un nodo de espacio de nombres, el eje «preceding-sibling» está vacío».
Dos palabras transmiten el significado. Hermanos significa nodos que comparten el mismo padre —no primos, ni antepasados, ni nada que se encuentre a una profundidad diferente—. Precedentes significa anteriores en el orden del documento.
<div>
<p>First</p>
<p>Second</p>
<span id="here">Context</span>
<p>Third</p>
</div>
Tomando span como nodo de contexto:
preceding-sibling::p → First, Second
following-sibling::p → Third
preceding-sibling::* → both p elements
Vale la pena recordar la salvedad relativa a los nodos de atributo, ya que explica un tipo de resultados vacíos. Si has navegado hasta un atributo —//@class—, entonces preceding-sibling desde allí estará vacío por definición, independientemente de lo que rodee al elemento al que pertenece el atributo. Los atributos no son hermanos de nada.
La trampa del eje inverso: «[1]
» no significa «el primero» Esta es la causa más habitual de resultados erróneos, y la especificación explica exactamente por qué.
XPath clasifica «ancestor
», «ancestor-or-self
», «preceding
» y «preceding-sibling
» como ejes inversos. En el caso de los ejes inversos, la posición de proximidad se determina ordenando los nodos en orden inverso al del documento.
Así pues, en preceding-sibling
, la posición 1 es el hermano más cercano que precede —el que se encuentra inmediatamente antes del nodo de contexto—, no el primero del documento.
<div>
<p>Alpha</p>
<p>Beta</p>
<p>Gamma</p>
<span id="here">Context</span>
</div>
preceding-sibling::p[1] → Gamma (nearest)
preceding-sibling::p[2] → Beta
preceding-sibling::p[3] → Alpha (furthest)
(preceding-sibling::p)[1] → Alpha (first in document order)
Los paréntesis lo cambian todo. Sin ellos, [1]
es un predicado aplicado a lo largo del eje inverso y significa «más cercano». Con ellos, el resultado del eje se recopila primero en un conjunto de nodos en orden de documento y [1]
indexa dentro de ese conjunto, lo que significa «el primero».
Compáralo con following-sibling
, que es un eje hacia delante en el que ambos coinciden:
following-sibling::p[1] → the next one
(following-sibling::p)[1] → also the next one
Esa asimetría es la razón por la que a quienes han aprendido con following-sibling
les confunde preceding-sibling
. El hábito se transfiere, pero la semántica no.
En la práctica, la forma sin paréntesis es casi siempre la que te conviene. «La etiqueta inmediatamente anterior a este valor» es el requisito habitual, y eso es preceding-sibling::label[1]
. Recurre a los paréntesis solo cuando realmente quieras decir «la primera del documento», lo cual es una necesidad menos frecuente de lo que parece.
«preceding-sibling» frente a «preceding»
Dos conceptos distintos con nombres muy parecidos que pueden llevar a confusión, y la diferencia es importante.
La especificación define «preceding» como el conjunto de «todos los nodos del mismo documento que el nodo de contexto y que se encuentran antes de este en el orden del documento, excluyendo cualquier antepasado y excluyendo los nodos de atributo y los nodos de espacio de nombres».
Por lo tanto, «preceding» son todos los elementos que aparecen antes en el documento, a cualquier profundidad, menos los antepasados. «preceding-sibling» son solo aquellos que comparten el mismo padre.
<body>
<header><h1>Title</h1></header>
<div>
<p>One</p>
<span id="here">Context</span>
</div>
</body>
De la «span»:
preceding-sibling::* → the p only
preceding::* → the p, the h1, and the header
La exclusión de los antepasados es lo que más sorprende a la gente. El elemento «div» que contiene el «span» no está en preceding, aunque su etiqueta de apertura aparezca antes en el código fuente. Los antepasados quedan excluidos por definición, ya que el eje se refiere a lo que viene antes de ti, no a lo que te contiene.
Cuándo utilizar cada uno. preceding-sibling para relaciones estructuradas: una etiqueta y su valor, un encabezado y el párrafo que le sigue, celdas en una fila. preceding para relaciones verdaderamente flexibles, como «el encabezado más cercano en cualquier lugar por encima de este elemento, independientemente del anidamiento». preceding es más amplio, considerablemente más lento y mucho más propenso a encontrar coincidencias que no eran tu intención.
El patrón «etiqueta-valor»
Esta es la razón por la que existe preceding-sibling
en el ámbito del scraping, y merece la pena dominarlo bien, ya que la mayor parte del marcado real es alguna variante del mismo.
El problema: quieres obtener un valor que solo se pueda identificar por la etiqueta que aparece junto a él. El valor en sí mismo no tiene ninguna clase útil, ni un identificador (id), ni nada que lo distinga.
Listas de definición:
<dl>
<dt>Price</dt>
<dd>£42.00</dd>
<dt>Stock</dt>
<dd>In stock</dd>
</dl>
Seleccionar el precio significa encontrar el dd
cuyo dt
más cercano que lo precede diga «Precio»:
//dd[preceding-sibling::dt[1] = 'Price']
Léelo al revés: para cada dd
, toma su dt
más cercano que lo precede; conserva el dd
si ese texto es «Precio». Ten en cuenta que el [1]
es esencial. Sin él, preceding-sibling::dt = 'Price'
es cierto si cualquier dt
anterior coincide, por lo que el segundo dd
también cumpliría los requisitos. Se trata de un error real y frecuente.
Celdas de tabla:
<tr>
<td>SKU</td>
<td>ABC-123</td>
</tr>
//td[preceding-sibling::td[1] = 'SKU']
O desde la columna de encabezado a lo largo de una fila de una tabla de especificaciones de dos columnas:
//th[normalize-space() = 'Weight']/following-sibling::td[1]
Esta segunda forma suele ser preferible cuando la etiqueta es un «th
», ya que se lee de forma lineal y se ajusta a la estructura de la tabla.
Mejoras de robustez que permiten que estas expresiones resistan el marcado real:
//dd[preceding-sibling::dt[1][normalize-space() = 'Price']]
normalize-space()
elimina los espacios en blanco internos y recorta los extremos, lo que permite gestionar el HTML formateado que, de otro modo, rompería una comparación exacta de cadenas. Es la función de mayor valor en el scraping con XPath.
Para coincidencias parciales, contains()
es más flexible y, en consecuencia, menos precisa:
//dd[preceding-sibling::dt[1][contains(., 'Price')]]
Ten cuidado: contains(., 'Price')
también coincide con «Precio sin IVA» y «Precio histórico». Si la página tiene varias etiquetas de este tipo, obtendrás varios resultados y tu código tomará el primero sin avisar.
Encabezados y contenido posterior, que es el mismo patrón en sentido contrario:
//h2[normalize-space() = 'Specifications']/following-sibling::table[1]
La primera tabla que aparece después de ese encabezado. Se trata de un requisito realmente habitual en páginas de documentación y de productos, y no existe ningún equivalente en CSS que exprese «la primera tabla después de este encabezado específico».
Combinación con predicados y condiciones «
preceding-sibling
» se combina con el resto de XPath, y conviene conocer algunas combinaciones.
Recuento de elementos hermanos — útil para encontrar el primer o el último elemento, o para comprobar la posición:
//li[count(preceding-sibling::li) = 0] first li
//li[count(preceding-sibling::li) < 3] first three
Comprobación de existencia: un conjunto de nodos en un contexto booleano es verdadero cuando no está vacío:
//p[preceding-sibling::h2] paragraphs with an h2 somewhere before
//p[not(preceding-sibling::p)] first paragraph among its siblings
Encadenamiento de ejes:
//span[@class='value']/preceding-sibling::*[1]/text()
El elemento inmediatamente anterior, independientemente de la etiqueta, y su texto.
Condiciones múltiples:
//td[preceding-sibling::td[1] = 'Status'][normalize-space() != '']
Dos predicados aplicados en secuencia: la celda situada después de una etiqueta «Status» y que no esté vacía.
Una nota sobre el atajo ..
, que a menudo resulta más limpio que un eje:
//dt[.='Price']/following-sibling::dd[1]
suele ser más legible que la forma preceding-sibling
para obtener el mismo resultado, y se lee en la dirección en la que está escrito el documento. Es preferible utilizarlo cuando el ancla es la etiqueta.
En qué casos los selectores CSS pueden y no pueden sustituirlo
La idea generalizada de que «el CSS no puede mirar hacia atrás» necesita actualizarse.
El CSS ahora permite establecer condiciones de hermanos con :has(). Ampliamente compatible con los navegadores modernos, permite que un selector esté condicionado a una relación de hermanos:
dt:has(+ dd) a dt immediately followed by a dd
li:has(~ li.active) an li with a later sibling that is active
Lo que el CSS aún no puede hacer:
Seleccionar por contenido de texto. No existe un equivalente en CSS de [text() = 'Price']. Solo por esto, el patrón «etiqueta-valor» sigue siendo territorio de XPath, ya que la coincidencia de etiquetas es una coincidencia de texto.
Navegar hasta un antepasado arbitrario. :has() proporciona una forma de selección de padres, pero no existe un eje general de antepasados.
Indexar a lo largo de un eje inverso. No existe ninguna construcción CSS que signifique «el elemento anterior más cercano de este tipo».
Y lo que CSS hace mejor: todo lo sencillo. Selección por clase e id, relaciones de descendencia, coincidencia de atributos. Los selectores CSS son más legibles, están mejor soportados por las herramientas y son más rápidos en la mayoría de los motores.
Lo más sensato es utilizar CSS por defecto y recurrir a XPath solo en aquellos casos concretos en los que se necesite la coincidencia de texto o la navegación hacia atrás. Mezclarlos en una misma base de código está bien, y un rastreador que utilice CSS para el 90 % de sus selectores y XPath para el 10 % más complicado es más fácil de mantener que uno que se ciña exclusivamente a uno de los dos.
Rendimiento y fragilidad
Dos limitaciones prácticas.
Rendimiento. «preceding-sibling» está limitado por el número de elementos hermanos, que suele ser pequeño, lo cual está bien. «preceding» recorre todo lo que hay antes del nodo de contexto en el documento, lo cual resulta costoso en una página grande y, dentro de un bucle, tiene una complejidad cuadrática. Si un selector que utiliza preceding es lento, ese es casi con toda seguridad el motivo. La solución habitual consiste en reescribirlo utilizando preceding-sibling desde un nodo de contexto más cercano.
Fragilidad. Los selectores basados en elementos hermanos dependen de la estructura del documento, que es precisamente lo que cambia con un rediseño. preceding-sibling::td[1] deja de funcionar sin avisar el día que alguien inserta una columna. No aparece ningún error; el selector coincide con una celda diferente y tus datos están erróneos sin que te des cuenta.
Tres medidas que realmente ayudan:
Ancla el selector al texto en lugar de a la posición siempre que puedas. //dt[.='Price']/following-sibling::dd[1] sigue funcionando tras un reordenamiento de la lista. (//dd)[3] no.
Verifica lo que extraes. Si un precio debe ajustarse a un patrón de divisa, compruébalo. Un selector que empieza a devolver el estado de las existencias en lugar del precio pasa desapercibido a menos que algo valide el formato.
Da preferencia a los datos estructurados incrustados cuando existan. Si la página incluye JSON-LD en un bloque <script type="application/ld+json">, analiza eso en su lugar. Está diseñado para ser leído por máquinas, es mucho más estable ante los rediseños y elimina por completo la fragilidad de los selectores. Comprobarlo antes de escribir cualquier XPath merece la pena esos treinta segundos.
Los fallos estructurales silenciosos pertenecen a la misma clase de problemas que describimos en por qué es importante probar los proxies: la solicitud se realiza con éxito, el análisis se realiza con éxito y los datos son erróneos.
Cuándo no utilizarlo
Cuando el elemento tiene un identificador utilizable. Si hay un id, una clase o un atributo «data», utilízalo. Un selector que depende de la estructura es, sin duda, más frágil que uno que depende de un nombre elegido deliberadamente por el desarrollador.
Cuando hay datos estructurados disponibles. JSON-LD, microdatos, una carga útil JSON tras una solicitud XHR. Cualquiera de estas opciones es mejor que analizar el HTML renderizado.
Cuando la relación es realmente laxa. Si te ves escribiendo «preceding::*[5]», la estructura no te está diciendo realmente nada y el selector dejará de funcionar en la próxima implementación. Reconsidera el enfoque.
Cuando basta con CSS. Para selecciones sencillas, el CSS es más legible y está mejor soportado. Reserva XPath para la coincidencia de texto y la navegación hacia atrás.
Cuando dependes de las características de XPath 2.0 en un navegador. Los navegadores implementan XPath 1.0 a través de document.evaluate, y Selenium sigue al navegador. Por lo tanto, nada de «matches()», ni expresiones regulares, ni «upper-case()», ni tipos de secuencia. Las bibliotecas del lado del servidor, como lxml, también utilizan XPath 1.0 para la API común. Si un fragmento de código que has encontrado en Internet no funciona, comprueba si utiliza una función que solo existe en una versión posterior.
Preguntas relacionadas
¿Qué hace la función «preceding-sibling» en XPath?
Selecciona todos los nodos que comparten el mismo nodo padre que el nodo de contexto y aparecen antes que este en el orden del documento. No incluye antepasados, descendientes ni nodos de otros niveles del árbol, solo nodos hermanos. Queda vacío si el nodo de contexto es un nodo de atributo o de espacio de nombres.
¿Por qué «preceding-sibling[1]» devuelve un elemento incorrecto?
Porque «preceding-sibling» es un eje inverso, y la numeración de posiciones en los ejes inversos sigue el orden inverso del documento. Por lo tanto, «[1]» se refiere al hermano anterior más cercano, no al primero del documento. Para obtener el primero en el orden del documento, hay que poner el eje entre paréntesis: «(preceding-sibling::p)[1]».
¿Cuál es la diferencia entre «preceding» y «preceding-sibling»? «
preceding-sibling» solo abarca los nodos que tienen el mismo padre. «preceding» abarca todos los nodos que aparecen antes en el documento, a cualquier profundidad, excluyendo a los antepasados y a los nodos de atributo. «preceding» es mucho más amplio, considerablemente más lento y mucho más propenso a encontrar coincidencias no deseadas.
¿Cómo selecciono un valor en función de su etiqueta en XPath?
Busca la coincidencia de la etiqueta por texto y, a continuación, toma el elemento adyacente: //dt[normalize-space()='Price']/following-sibling::dd[1], o desde el lado del valor, //dd[preceding-sibling::dt[1]='Price']. El [1] es importante: sin él, el predicado es verdadero si coincide cualquier etiqueta precedente.
¿Pueden los selectores CSS hacer lo mismo que «preceding-sibling»?
En parte. :has() ofrece selección condicional de hermanos en los navegadores modernos. Lo que el CSS aún no puede hacer es seleccionar por contenido de texto o por índice a lo largo de un eje inverso, y la coincidencia de texto es precisamente lo que requiere el patrón de etiqueta-valor. Utiliza CSS para selecciones sencillas y XPath cuando necesites texto o navegación hacia atrás.
¿Funciona «preceding-sibling» en Selenium y en los navegadores?
Sí. Los navegadores implementan XPath 1.0 a través de document.evaluate, y Selenium utiliza el motor del navegador. El eje es XPath 1.0 y está disponible de forma universal. Lo que no está disponible es cualquier elemento de XPath 2.0 o posterior: ni expresiones regulares, ni «matches()», ni «upper-case()».
¿Es lento el «preceding-sibling»?
Normalmente no. Su rendimiento depende del número de elementos hermanos, que suele ser reducido. El eje «preceding» es el que resulta lento, ya que recorre todo lo anterior en el documento, y dentro de un bucle eso se convierte en una operación cuadrática. Si un selector basado en elementos hermanos es lento, comprueba si realmente has utilizado «preceding».
¿Cómo puedo hacer que los selectores XPath sean menos frágiles?
Ancla en texto en lugar de en posición siempre que sea posible, utiliza normalize-space() para sortear las diferencias de espacios en blanco, da preferencia a los identificadores y a los atributos de datos frente a la estructura, comprueba si hay JSON-LD incrustado antes de analizar el HTML y valida la forma de lo que extraes, de modo que un selector que coincida con el elemento equivocado falle de forma evidente en lugar de hacerlo de forma silenciosa.
Conclusión: «
preceding-sibling» es una herramienta limitada con una única función: recorrer hacia atrás entre nodos del mismo nivel. Lo que hay que tener en cuenta es que se trata de un eje inverso, por lo que «[1]» significa «el más cercano» en lugar de «el primero», y al añadir paréntesis se invierte por completo ese significado. Esa única distinción explica la mayoría de los resultados erróneos que se obtienen al utilizarla.
Su verdadero valor reside en el patrón «etiqueta-valor»: extraer un campo que solo se puede identificar por el texto que aparece junto a él. CSS ha reducido en parte esta brecha con :has(), pero sigue sin poder seleccionar por contenido de texto, y el texto es precisamente lo que constituye una etiqueta. Ese es el caso en el que XPath sigue siendo la herramienta adecuada, más que la habitual.
Hay que tener en cuenta que los selectores estructurales fallan de forma silenciosa. Una nueva columna, una lista reordenada, un div contenedor… y tu selector apunta a otra cosa, aunque todo siga pareciendo correcto. Ancla el selector al texto siempre que puedas, comprueba si hay datos estructurados incrustados antes de escribir cualquier selector y valida el resultado; porque el fallo que puedes ver nunca es el más costoso.
