CSS Selectors and XPath for Web Scraping: A Practical Recipe Guide
Web ScrapingCSS SelectorsXPathHTMLBrowser DevTools

CSS Selectors and XPath for Web Scraping: A Practical Recipe Guide

sscraper.page Editorial Team
2026-11-07
7 min read

A practical CSS selector and XPath cheat sheet for scraping links, attributes, cards, tables, and repeated elements reliably.

Reliable selectors are the foundation of a maintainable scraper. This practical guide shows how to write, test, and troubleshoot CSS selectors and XPath expressions for common page structures, from links and attributes to nested cards, tables, and repeated elements.

Overview

A scraper selector describes which nodes in an HTML document contain the data you need. CSS selectors are usually concise and familiar to frontend developers, while XPath offers more expressive relationships, such as selecting an element based on its text or locating a parent and then moving to a sibling.

The best selector is not necessarily the shortest one. It should identify the intended element, remain understandable to the next person who maintains the scraper, and survive harmless changes to classes, whitespace, or page layout. Prefer stable attributes and meaningful structure over generated class names or a long chain of nested elements.

Before writing a selector, inspect the actual response received by your scraper. A browser may display content that was inserted by JavaScript after the initial HTML loaded. In that case, a correct selector will still return no results when used against a static response. See the guidance on choosing between Playwright and BeautifulSoup if rendering is part of the problem.

For either CSS or XPath, use this basic process:

  1. Identify the smallest useful container for one record.
  2. Find a stable hook inside that container, such as a semantic element, data attribute, or recognizable link.
  3. Test the selector against several records, not just the first match.
  4. Extract and clean the value separately from locating the element.
  5. Add a validation check so a layout change produces a visible failure instead of silently corrupting data.

Checklist by scenario

The examples below use generic HTML. Adapt the class names and attributes to the page you are examining.

ScenarioCSS selectorXPath expression
Element by ID#main-content//*[@id='main-content']
Element by class.product-card//*[contains(concat(' ', normalize-space(@class), ' '), ' product-card ')]
Attribute exists[data-id]//*[@data-id]
Exact attributea[rel='next']//a[@rel='next']
Attribute contains texta[href*='/products/']//a[contains(@href, '/products/')]
Text inside an element.card-title, then read text//h2[contains(normalize-space(.), 'Guide')]
Nested value.card .price//article[contains(@class, 'card')]//span[contains(@class, 'price')]
All links in a containernav.pagination a//nav[contains(@class, 'pagination')]//a
Table body rowstable tbody tr//table//tbody/tr
Cell in each rowtr td:nth-child(2)./td[2], relative to each row
Following siblingh2 + p//h2/following-sibling::p[1]
Parent of a matched elementUse the nearest container, such as .card:has(.price) where supported//span[contains(@class, 'price')]/ancestor::article[1]

Selectors locate nodes; your parser then reads their text, attributes, or HTML. For a product card, a useful pattern is to select the card first and query each field relative to that card:

cards = soup.select('article.product-card')
for card in cards:
    title = card.select_one('h2, h3').get_text(' ', strip=True)
    link = card.select_one('a[href]')['href']
    price_node = card.select_one('[data-price], .price')
    price = price_node.get_text(' ', strip=True) if price_node else None

This container-first approach prevents a title from one record being paired with a price from another. It also makes missing fields explicit.

Repeated cards and nested content

When a page contains repeated cards, select the repeated outer element once and process each match independently. Avoid a global selector such as .title followed by a separate global selector such as .price. Those two lists may have different lengths or ordering.

//article[contains(@class, 'product-card')]
  /descendant::h2[1]

In XPath libraries that support relative expressions, first select each card and then use .//h2[1] from that card. The leading dot matters: without it, the expression may search from the document root instead of the current record.

Tables and row-based extraction

For tables, select rows and extract cells by position or by a header mapping. Positional extraction is quick, but a header-based approach is safer when columns can move. Check that each row has the expected number of cells before assigning values.

rows = soup.select('table.results tbody tr')
for row in rows:
    cells = [cell.get_text(' ', strip=True) for cell in row.select('th, td')]
    if len(cells) >= 3:
        name, status, updated = cells[:3]

Fallback patterns

Use a small, intentional fallback when a field has known variants. For example, a title might be an h2 on one template and an h3 on another:

title_node = card.select_one('h2, h3, [data-testid="title"]')

Do not add every possible class to one selector. If the fallback becomes long or uncertain, record which template was matched and review the page structure instead. For broader extraction and normalization advice, see how to clean scraped HTML into reliable text and structured data.

What to double-check

Test in browser DevTools

Open the target page, inspect an element, and test selectors in the Console:

document.querySelectorAll('article.product-card')
$x("//article[contains(@class, 'product-card')]")

Confirm the count, inspect the first few matches, and verify that the selected node contains the intended field. DevTools tests are useful for discovery, but repeat the test against the exact HTML or rendered page your scraper receives.

Check class matching carefully

In CSS, .card matches an element with a class token named card. In XPath, contains(@class, 'card') can also match values such as discarded-card. Use the token-safe XPath form when class boundaries matter:

//*[contains(concat(' ', normalize-space(@class), ' '), ' card ')]

Separate absolute and relative paths

An absolute XPath beginning with /html is tied closely to the document layout and is usually fragile. Prefer a semantic or attribute-based path. When looping over a selected container, use a relative expression beginning with ..

Validate empty and duplicate results

A selector returning zero matches may indicate a changed layout, a blocked request, missing authentication, or content that requires rendering. A selector returning too many matches may be equally serious. Set reasonable expectations for counts, required fields, and duplicate URLs, then log the failing page for inspection. Selector reliability works best alongside retries, rate limits, and data validation; the reliable scraper checklist covers those surrounding controls.

Respect the page and your data contract

Before collecting data, review the site’s published instructions and the permissions that apply to your use case. Keep request rates controlled, avoid collecting unnecessary personal information, and define the fields your downstream system expects. A technically accurate selector is not a substitute for responsible collection.

Common mistakes

  • Copying a browser-generated selector. DevTools may produce a long path based on every ancestor. Treat it as a starting point, then reduce it to stable attributes and structure.
  • Relying on visual text without accounting for whitespace. XPath normalize-space(.) can help with surrounding whitespace, while parser text cleanup should handle line breaks and nested labels.
  • Using indexes as the primary identity. li:nth-child(3) and //div[3] break when an item is inserted. Use an index only when position is genuinely part of the data.
  • Confusing CSS and XPath syntax. CSS uses .class, #id, and square-bracket attribute filters. XPath uses axes, predicates, and expressions such as @href. Pass each expression to the matching parser API.
  • Ignoring pagination and embedded data. The visible first page may not contain the complete dataset. Check next links, cursor requests, and embedded JSON before expanding a selector. See the guides to pagination patterns and embedded JSON.
  • Failing silently. If a required selector returns nothing, raise a warning or fail the validation step. Silent empty fields are difficult to distinguish from legitimate missing values.

When to revisit

Review selectors whenever the target site changes its templates, navigation, CSS framework, authentication flow, or rendering method. A scheduled check before a seasonal planning cycle is useful when the scraper supports recurring reports or inventory updates. Also revisit the implementation when a field’s missing-value rate changes, match counts become unusual, or a previously static page begins loading content dynamically.

Keep a small selector test fixture containing representative HTML: one normal record, one record with a missing optional field, one record with unusual text, and one page state with no results. Run it whenever you modify a selector or parser. During a quarterly tooling review, confirm that your browser automation or HTML parser still receives the expected document and that DevTools examples match the production workflow.

Use this final checklist before shipping:

  1. Save the exact HTML or rendered snapshot used for testing.
  2. Choose the outer record container before selecting individual fields.
  3. Prefer stable IDs, data attributes, semantic elements, and meaningful links.
  4. Test several records, missing fields, duplicates, and zero-result pages.
  5. Use relative XPath expressions inside loops and token-safe class matching where needed.
  6. Validate counts and required fields, then log enough context to diagnose a failure.
  7. Document why each non-obvious selector was chosen and when it should be reviewed.

Selectors will eventually need maintenance, but a clear container-first design, browser testing, and explicit validation make changes easier to detect and safer to repair.

Related Topics

#Web Scraping#CSS Selectors#XPath#HTML#Browser DevTools
s

scraper.page Editorial Team

Developer Resources Editor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.