PINGDOM_CHECK

XPath Tips From The Web Scraping Trenches

XPath is helpful for web scraping, allowing to write specifications more flexibly than CSS selectors. This tutorial is packed with XPath tips and examples.

Valdir Stumm Junior

3 min read ·

XPath Tips From The Web Scraping Trenches

XPath tips from the web scraping trenches

In the context of web scraping, XPath is a nice tool to have in your belt, as it allows you to write specifications of document locations more flexibly than CSS selectors.

In case you're looking for a tutorial, here is a tutorial with XPath tips and examples.

In this post, we'll show you some tips we found valuable when using XPath in the trenches, using Scrapy Selector API for our examples.

Avoid using contains(.//text(), 'search text') in your XPath conditions.

Use contains(., 'search text') instead.

Here is why: the expression .//text() yields a collection of text elements -- a node-set. And when a node-set is converted to a string, which happens when it is passed as argument to a string function like contains() or starts-with(), results in the text for the first element only.

1>>> from scrapy import Selector
2>>> sel = Selector(text='<a href="#">Click here to go to the <strong>Next Page</strong></a>')
3>>> xp = lambda x: sel.xpath(x).extract() # let's type this only once
4>>> xp('//a//text()') # take a peek at the node-set
5   [u'Click here to go to the ', u'Next Page']
6>>> xp('string(//a//text())')  # convert it to a string
7   [u'Click here to go to the ']
Copy

A node converted to a string, however, puts together the text of itself plus of all its descendants:

1>>> xp('//a[1]') # selects the first a node
2[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
3>>> xp('string(//a[1])') # converts it to string
4[u'Click here to go to the Next Page']
Copy

So, in general:

GOOD:

1>>> xp("//a[contains(., 'Next Page')]")
2[u'<a href="#">Click here to go to the <strong>Next Page</strong></a>']
Copy

BAD:

1>>> xp("//a[contains(.//text(), 'Next Page')]")
2[]
Copy

GOOD:

1>>> xp("substring-after(//a, 'Next ')")
2[u'Page']
Copy

BAD:

1>>> xp("substring-after(//a//text(), 'Next ')")
2[u'']
Copy

You can read more detailed explanations about string values of nodes and node-sets in the XPath spec.

Beware of the difference between //node[1] and (//node)[1]

//node[1] selects all the nodes occurring first under their respective parents.

(//node)[1] selects all the nodes in the document, and then gets only the first of them.

1>>> from scrapy import Selector
2>>> sel=Selector(text="""
3....:     <ul class="list">
4....:         <li>1</li>
5....:         <li>2</li>
6....:         <li>3</li>
7....:     </ul>
8....:     <ul class="list">
9....:         <li>4</li>
10....:         <li>5</li>
11....:         <li>6</li>
12....:     </ul>""")
13>>> xp = lambda x: sel.xpath(x).extract()
14>>> xp("//li[1]") # get all first LI elements under whatever it is its parent
15[u'<li>1</li>', u'<li>4</li>']
16>>> xp("(//li)[1]") # get the first LI element in the whole document
17[u'<li>1</li>']
18>>> xp("//ul/li[1]")  # get all first LI elements under an UL parent
19[u'<li>1</li>', u'<li>4</li>']
20>>> xp("(//ul/li)[1]") # get the first LI element under an UL parent in the document
21[u'<li>1</li>']
Copy

Also,

//a[starts-with(@href, '#')][1] gets a collection of the local anchors that occur first under their respective parents.

(//a[starts-with(@href, '#')])[1] gets the first local anchor in the document.

When selecting by class, be as specific as necessary

If you want to select elements by a CSS class, the XPath way to do that is the rather verbose:

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

Let's cook up some examples:

1>>> sel = Selector(text='<p class="content-author">Someone</p><p class="content text-wrap">Some content</p>')
2>>> xp = lambda x: sel.xpath(x).extract()
Copy

BAD: doesn't work because there are multiple classes in the attribute

1>>> xp("//*[@class='content']")
2[]
Copy

BAD: gets more than we want

1>>> xp("//*[contains(@class,'content')]")
2[u'<p class="content-author">Someone</p>']
Copy

GOOD:

1>>> xp("//*[contains(concat(' ', normalize-space(@class), ' '), ' content ')]")
2[u'<p class="content text-wrap">Some content</p>']
Copy

And many times, you can just use a CSS selector instead, and even combine the two of them if needed:

ALSO GOOD:

1>>> sel.css(".content").extract()
2[u'<p class="content text-wrap">Some content</p>']
3>>> sel.css('.content').xpath('@class').extract()
4[u'content text-wrap']
Copy

Read more about what you can do with Scrapy's Selectors here.

XPath tips: Learn to use all the different axes

It is handy to know how to use the axes, you can follow through the examples given in the tutorial to quickly review this.

In particular, you should note that following and following-sibling are not the same thing, this is a common source of confusion.

The same goes for preceding and preceding-sibling, and also ancestor and parent.

Useful trick to get text content

Want even more XPath tips? Here is another trick that you may use to get the interesting text contents:

1//*[not(self::script or self::style)]/text()[normalize-space(.)]
Copy

This excludes the content from script and style tags and also skip whitespace-only text nodes.

Source: http://stackoverflow.com/a/19350897/2572383

Do you have other XPath tips?

Please, leave us a comment with your tips or questions. 🙂

And for everybody who contributed tips and reviewed this article, a big thank you!

Try Zyte API

Build your first scraper in minutes

Free trial, no credit card. From a single request to production in an afternoon.

Get started

Valdir Stumm Junior

Valdir's writing centers on the open-source Scrapy ecosystem and Zyte's developer tools, spanning deployment guides ("Deploy Your Scrapy Spiders From GitHub", "How To Debug Your Scrapy Spiders"), core libraries he helped build (Parsel, Scrapely, Frontera, Dateparser), and the lon…

More from this author

Continue reading

12 things I use Zyte MCP for, ranked, with prompts and rules you can copy
AI

12 things I use Zyte MCP for, ranked, with prompts and rules you can copy

What can you use Zyte MCP for? 12 uses, 11 tested, ranked, each with a copy-paste prompt, a CLAUDE.md rule, and a skill idea.

Ayan Pahwa
A shopper faces an overwhelming cascade of ecommerce T-shirt product cards in many colours and configurations, illustrating product-variant explosion.
Web data collection

The trouble with variants: Solving the web's infinite ecommerce explosion

Product variants can turn a straightforward ecommerce scraping project into millions of combinations. Learn how to classify, scope and control them.

Robert Andrews12 min read
How I use Zyte MCP to plan web scrapers
AI

How I use Zyte MCP to plan web scrapers

Zyte MCP lets my AI agent investigate a site before I write the scraper, so I can decide where the data should come from, compare an API with the rendered page for missing fields, and end up with a crawl plan I can act on.

John Rooney
JSON Parsing with Python [Practical Guide]
Scraping practice

JSON Parsing with Python [Practical Guide]

Parse JSON in Python with json.loads and json.load, read what each error means, and get JSON out of APIs and web pages with JMESPath and ChompJS.

Felipe Boff Nunes20 min read
scrapy-jev
Data quality

Stopping a crawl when the data stops looking real with Jev

A Scrapy pipeline that asks a fast, calibrated AI model whether each scraped field still looks real, and stops the crawl when too many don't. What it caught, what it misses, and whether building it was worth it.

John Rooney
podcast-ep08
Scraping strategy

Podcast Ep08 - Scrapy, Python and mushroom soup

Scrapy's core handles crawling well and deliberately leaves almost everything else out: no bundled browser, no opinion about how you shape your data, no built-in answer for every anti-bot wrinkle. What it gives you instead is a clean way to add those things at the edges, exactly when you need them and never before.

John Rooney

The Community · Newsletter

The best of Zyte and the data web, in your inbox.

One curated edition — new articles, product updates, and the stories shaping the data web. No noise.