Back to Blog
Tutorials 12 min read February 12, 2026

XPath for Web Scraping & Testing: The Selector Language Developers Forget About

CSS selectors get all the attention, but XPath is more powerful for complex queries. Learn XPath expressions that transform your Selenium tests and web scraping workflows.

DevForge Team

DevForge Team

AI Development Educators

Developer writing automated test scripts on a laptop

Why XPath When CSS Selectors Exist?

CSS selectors are the default choice for web scraping and browser testing. They're concise, familiar to frontend developers, and well-supported by every scraping library and testing framework.

But CSS selectors have a hard ceiling. They can't:

  • Select an element based on its text content
  • Traverse upward in the DOM tree (no parent selection)
  • Select an element based on a sibling's properties
  • Apply boolean logic across multiple conditions on different axes
  • Calculate positions dynamically (every other sibling starting from the third)

XPath can do all of these. The query language was designed for navigating XML trees, and since HTML is a form of markup with a tree structure, XPath applies directly to HTML documents.

This guide focuses on the XPath expressions that are most useful in practical web scraping and Selenium testing, with real patterns you can use immediately.

XPath Fundamentals

Absolute vs. Relative Paths

xpath
/html/body/div/p          # Absolute: from root, exact path
//p                       # Relative: find all <p> anywhere in document
//div//p                  # Relative: find all <p> inside any <div>

Always prefer // relative paths in scraping and testing. Absolute paths break whenever the page structure changes. Relative paths target elements by their characteristics rather than their exact position.

Attribute Selection

xpath
//div[@class='title']              # Exact class match
//div[contains(@class, 'title')]   # Partial class match (handles multiple classes)
//a[@href]                         # Any element with an href attribute
//input[@type='submit']            # Input with specific type
//div[@id='main' and @class='content']  # Multiple attribute conditions

The contains() function is essential for class-based selection in real HTML, because elements often have multiple classes: class="title featured active". Exact matching fails here; contains(@class, 'title') succeeds.

Text Content Selection

This is where XPath surpasses CSS selectors:

xpath
//button[text()='Submit']              # Exact text match
//button[contains(text(), 'Submit')]   # Partial text match
//h2[normalize-space()='Product Name'] # Text match ignoring whitespace
//a[starts-with(text(), 'Read more')]  # Text starts with pattern

In Selenium, finding buttons by their visible text is far more robust than finding them by class name (which changes with styling) or position (which changes with layout). //button[contains(text(), 'Add to Cart')] will survive a CSS refactor.

Axes: Navigating the Tree

XPath axes are the feature that most separates it from CSS:

xpath
//label[text()='Email']/following-sibling::input  # Input after a label
//input[@id='email']/parent::div                  # Parent div of an input
//li[@class='active']/preceding-sibling::li       # All li before the active one
//table/tbody/tr[3]/td[2]                         # Third row, second cell
//div[@class='error']/ancestor::form              # Form containing an error div

The following-sibling, preceding-sibling, parent, and ancestor axes enable DOM traversal that CSS cannot express. In testing, this is critical for associating form labels with their inputs, or finding containers based on their contents.

Selenium Testing Patterns

Finding Elements by Visible Text

python
from selenium import webdriver
from selenium.webdriver.common.by import By

driver = webdriver.Chrome()
driver.get("https://example.com")

submit_button = driver.find_element(By.XPATH, "//button[contains(text(), 'Submit')]")
submit_button.click()

nav_link = driver.find_element(By.XPATH, "//nav//a[text()='Pricing']")
nav_link.click()

Form Interaction Using Label Association

python
email_input = driver.find_element(
    By.XPATH, "//label[contains(text(), 'Email')]/following-sibling::input"
)
email_input.send_keys("test@example.com")

password_input = driver.find_element(
    By.XPATH, "//label[normalize-space()='Password']/following-sibling::input[@type='password']"
)
password_input.send_keys("secret123")

Verifying Error Messages

python
error_message = driver.find_element(
    By.XPATH, "//div[contains(@class, 'error') or contains(@class, 'alert')]"
)
assert "required" in error_message.text.lower()

field_error = driver.find_element(
    By.XPATH, "//input[@name='email']/following-sibling::span[@class='error']"
)

Table Data Extraction

python
price_cell = driver.find_element(
    By.XPATH, "//tr[td[contains(text(), 'Product A')]]/td[@class='price']"
)

header_index = driver.find_element(
    By.XPATH, "//th[text()='Price']"
)
column_index = int(header_index.get_attribute("cellIndex")) + 1

price_cells = driver.find_elements(
    By.XPATH, f"//tbody/tr/td[{column_index}]"
)

Waiting for Elements

python
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)

success_message = wait.until(
    EC.presence_of_element_located(
        (By.XPATH, "//div[contains(@class, 'success') and contains(text(), 'saved')]")
    )
)

modal_close = wait.until(
    EC.element_to_be_clickable(
        (By.XPATH, "//div[@role='dialog']//button[contains(@aria-label, 'close')]")
    )
)

Web Scraping Patterns with lxml

Python's lxml library provides XPath support for HTML and XML scraping:

Basic Setup

python
import requests
from lxml import html

response = requests.get("https://example.com/products")
tree = html.fromstring(response.content)

product_names = tree.xpath("//h2[@class='product-title']/text()")

prices = tree.xpath("//span[contains(@class, 'price')]/text()")

links = tree.xpath("//a[@class='product-link']/@href")

Extracting Structured Data

python
products = tree.xpath("//div[@class='product-card']")

for product in products:
    name = product.xpath(".//h2/text()")[0]
    price = product.xpath(".//span[@class='price']/text()")[0]
    link = product.xpath(".//a/@href")[0]

    in_stock = bool(product.xpath(".//span[contains(@class, 'in-stock')]"))

    print(f"{name}: {price} {'(In Stock)' if in_stock else '(Out of Stock)'}")

Note the ./ prefix when using XPath on a specific element — this makes the expression relative to that element rather than the document root.

Handling Pagination

python
next_page = tree.xpath("//a[contains(@class, 'next') or contains(text(), 'Next')]/@href")

page_numbers = tree.xpath("//nav[@aria-label='Pagination']//a[@class='page-number']/text()")

last_page_link = tree.xpath(
    "//a[contains(@class, 'page-number')][last()]/@href"
)

Scraping Tables

python
headers = tree.xpath("//table[@id='results']//th/text()")

rows = []
for row in tree.xpath("//table[@id='results']//tbody/tr"):
    cells = row.xpath(".//td/text()")
    rows.append(dict(zip(headers, cells)))

XPath Functions Reference

| Function | Use | Example |

|----------|-----|---------|

| text() | Element text content | //h1/text() |

| contains() | Substring match | contains(@class, 'active') |

| starts-with() | Prefix match | starts-with(@id, 'item-') |

| normalize-space() | Trim/collapse whitespace | normalize-space(text()) = 'Submit' |

| not() | Negation | //div[not(@class)] |

| count() | Count nodes | count(//li) > 5 |

| last() | Last in set | //li[last()] |

| position() | Position in parent | //li[position() > 2] |

| string() | Convert to string | string(//div) |

Positional Selectors

xpath
//li[1]                  # First list item (XPath is 1-indexed!)
//li[last()]             # Last list item
//li[last()-1]           # Second to last
//li[position() > 3]     # All items after the third
//tr[position() mod 2 = 0]  # Every even row
//p[count(preceding-sibling::p) = 0]  # First paragraph (no preceding siblings)

Note that XPath uses 1-based indexing, unlike CSS :nth-child() which uses 1-based indexing too, but XPath's [1] is equivalent to CSS's :first-of-type rather than :nth-child(1).

Debugging XPath in Browser DevTools

You can test XPath expressions directly in browser DevTools without writing any code:

  1. Open DevTools (F12)
  2. Go to the Console tab
  3. Use $x("//your/xpath") to evaluate expressions
javascript
$x("//button[contains(text(), 'Submit')]")
$x("//input[@type='email']")
$x("//nav//a")

The console returns an array of matching DOM elements, which you can inspect to verify your selector before using it in code.

In the Elements tab, you can also right-click any element and select "Copy > Copy XPath" to get the browser's generated XPath for that element (though browser-generated XPaths are often absolute paths that break easily — use them as a starting point, not a final answer).

When to Use XPath vs. CSS Selectors

Use XPath when:

  • You need to select by text content
  • You need parent or ancestor traversal
  • You need complex boolean conditions
  • You're working with XML (not just HTML)
  • You need positional logic relative to siblings

Use CSS selectors when:

  • You need simple class, ID, or attribute selection
  • You want more readable, concise syntax for straightforward cases
  • You're targeting pseudo-elements (::before, ::after)
  • Performance is critical (CSS selectors are slightly faster in browsers)

In practice, most test suites use a mix: CSS selectors for simple element identification, XPath for complex traversal and text-based selection.

The Full Picture

XPath is one piece of the XML ecosystem. To understand why XPath expressions look the way they do — the tree model, axis names, node types — it helps to understand the XML structure they're navigating.

The XML Fundamentals tutorial covers the complete foundation: from document structure and namespaces through XPath and XSLT. The XPath lesson in particular provides the complete axis reference and function library that makes complex selectors readable.

For teams building test automation, combining XPath knowledge with a solid understanding of how web pages are structured as document trees makes test selectors more intentional and more maintainable.

#XPath#Web Scraping#Selenium#Testing#XML