Aprender / Hoja de referencia
Hoja de referencia de XPath y selectores CSS
XPath y CSS lado a lado, para Selenium, Playwright y las DevTools del navegador. Usa CSS cuando pueda expresar lo que necesitas, porque es más corto y es la sintaxis que conocen los desarrolladores. Recurre a XPath cuando necesites buscar por texto visible o subir por el árbol hasta un padre o un ancestro.
Revisada con XPath 1.0 (lo que implementan los navegadores), CSS Selectors Level 4 según lo admiten las versiones actuales de Chrome, Firefox y Safari
Lo básico
| Objetivo | XPath | CSS |
|---|---|---|
| Elemento por etiqueta (en cualquier sitio) | //button | button |
| Por id | //*[@id='email'] | #email |
| Por clase (una de varias) | //*[contains(concat(' ', normalize-space(@class), ' '), ' active ')] | .active |
| Hijo directo | //ul/li | ul > li |
| Cualquier descendiente | //form//input | form input |
| Varios selectores (OR) | //h1 | //h2 | h1, h2 |
| Cualquier elemento | //* | * |
Atributos
| Objetivo | XPath | CSS |
|---|---|---|
| Tiene el atributo | //button[@disabled] | button[disabled] |
| Igual a | //input[@name='q'] | input[name='q'] |
| Contiene | //a[contains(@href, 'orders')] | a[href*='orders'] |
| Empieza por | //a[starts-with(@href, 'https')] | a[href^='https'] |
| Termina en | //a[substring(@href, string-length(@href) - 3) = '.pdf'] | a[href$='.pdf'] |
| Dos condiciones (AND) | //input[@type='text' and @required] | input[type='text'][required] |
| Una u otra condición (OR) | //input[@type='email' or @type='tel'] | input[type='email'], input[type='tel'] |
| Negación | //input[not(@disabled)] | input:not([disabled]) |
| Test id | //*[@data-testid='submit'] | [data-testid='submit'] |
XPath 1.0 no tiene ends-with(): la forma con substring() es la alternativa portable.
Texto (solo XPath)
| Objetivo | XPath | CSS |
|---|---|---|
| Texto exacto, espacios normalizados | //button[normalize-space()='Sign in'] | no es posible |
| El texto contiene | //p[contains(., 'expired')] | no es posible |
| Contiene, sin distinguir mayúsculas | //p[contains(translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'), 'expired')] | no es posible |
| Solo su propio nodo de texto | //label[text()='Email'] | no es posible |
. (el valor de cadena) incluye el texto de los elementos hijos; text() solo coincide con los nodos de texto propios del elemento. Prefiere normalize-space() o .
Relaciones y ejes
| Objetivo | XPath | CSS |
|---|---|---|
| Padre | //input[@id='email']/.. | :has(> #email) |
| Fila ancestro de una celda | //td[normalize-space()='#1043']/ancestor::tr | tr:has(td[data-id='1043']) |
| Hermano siguiente | //label[.='Password']/following-sibling::input[1] | label + input |
| Todos los hermanos siguientes | //h2/following-sibling::p | h2 ~ p |
| Hermano anterior | //button/preceding-sibling::input[1] | no es posible |
| Celda junto a una celda de etiqueta | //td[.='Diego Ramos']/following-sibling::td[1] | no es posible (parte del texto) |
| Elemento que contiene un hijo | //li[.//button[@aria-label='Remove']] | li:has(button[aria-label='Remove']) |
:has() funciona en las versiones actuales de Chrome, Edge, Safari y Firefox (121+). Los navegadores antiguos y algunas herramientas lo rechazan.
Posición
| Objetivo | XPath | CSS |
|---|---|---|
| Primera fila hija (empieza en 1) | //tbody/tr[1] | tbody tr:first-child |
| Fila n | //tbody/tr[3] | tbody tr:nth-child(3) |
| Última fila | //tbody/tr[last()] | tbody tr:last-child |
| Penúltima | //tbody/tr[last()-1] | tbody tr:nth-last-child(2) |
| Todas las filas pares | //tbody/tr[position() mod 2 = 0] | tbody tr:nth-child(even) |
| Primera coincidencia en toda la página | (//button)[1] | no es posible: usa first() de tu herramienta |
//tr[1] significa "cada tr que es el primer tr dentro de su padre", no "el primer tr de la página". Para eso, usa (//tr)[1].
Pseudoclases de estado (CSS)
| Objetivo | XPath | CSS |
|---|---|---|
| Checkbox o radio marcado | //input[@checked] (initial state only) | input:checked |
| Deshabilitado | //button[@disabled] | button:disabled |
| Campo obligatorio | //input[@required] | input:required |
| Elemento vacío | //td[not(node())] | td:empty |
XPath lee los atributos tal como están escritos en el HTML, así que @checked no cambia cuando el usuario marca la casilla. :checked de CSS sigue el estado en vivo.
Pruébalos en DevTools
| Tarea | Código |
|---|---|
| CSS, todas las coincidencias (Console) | $$('tbody tr') |
| XPath, todas las coincidencias (Console) | $x("//td[.='Mei Lin']") |
| Buscar en el panel Elements | Ctrl+F / Cmd+F, then type a selector or XPath |
| Contar coincidencias | $$('tbody tr').length |