Selectors: CSS and XPath

Scrapy · Essentials · 12 min

Selectors are how every value gets out of a page, and knowing both CSS and XPath lets you handle any structure.

A response offers response.css(query) and response.xpath(query). Both return a SelectorList, which you can query further or extract from:

  • .get() returns the first match as a string, or None (or get(default="")).
  • .getall() returns a list of all matches.
  • .re(r"pattern") / .re_first(...) apply a regex to the matches.

CSS in Scrapy has two extensions (from the parsel library) to select text and attributes:

  • span.text::text selects the text nodes of the element.
  • a::attr(href) selects an attribute's value. Alternatively response.css("a").attrib["href"] reads an attribute of the first match.

XPath is more powerful for some tasks: //a[contains(text(), "Next")]/@href, //th[text()="Price"]/following-sibling::td/text(), or string(.) for all text inside an element. CSS queries are actually translated to XPath under the hood, and you can chain both: response.css("div.product").xpath(".//h2/text()").

When looping over elements, query relative to each one: quote.css("span.text::text") stays inside that quote. In XPath, start relative expressions with .// instead of //, which would search the whole document again.

::text only returns direct text nodes; for text spread over nested tags use *::text with getall() and join, or XPath string().

Example

from scrapy.http import HtmlResponse

body = b"""<div class="product" data-sku="A1"><h2>Kettle <small>steel</small></h2>
  <table><tr><th>Price</th><td>29.90 EUR</td></tr></table>
  <a class="next" href="/page/2">Next page</a></div>"""
response = HtmlResponse(url="https://shop.example/", body=body, encoding="utf-8")

p = response.css("div.product")
print(p.attrib["data-sku"])                          # A1
print(p.css("h2::text").get())                       # 'Kettle '
print("".join(p.css("h2 *::text, h2::text").getall()).strip())  # Kettle steel
print(p.xpath("string(.//h2)").get())                # Kettle steel
print(p.xpath('.//th[text()="Price"]/following-sibling::td/text()').re_first(r"[\d.]+"))  # 29.90
print(response.css("a.next::attr(href)").get())      # /page/2
print(response.css("span.missing::text").get(default="n/a"))   # n/a

Remember

  • get() for the first match, getall() for all, re()/re_first() for regex
  • ::text and ::attr(name) are Scrapy's CSS extensions
  • Query relative to each element; in XPath use .// not //
  • XPath handles text conditions and siblings; string() joins nested text

Watch out for

  • Using // inside a loop over elements and getting matches from the whole page every time.

Check yourself

  1. Inside `for q in response.css('div.quote')`, which XPath stays within q?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.scrapy.org/en/latest/topics/selectors.html