Skip to main content

Scrape a catalogue page

Opens a catalogue page, reads the title, price and availability of every tile, and writes them to Excel. The pattern here — one Get Elements per column, joined on position — is the one that survives contact with real pages.

The workflow

StartWebBrowser("https://books.toscrape.com/catalogue/category/books/mystery_3/index.html", "newBrowserSession")
WaitForElement("ol.row li:nth-child(1) article.product_pod", state="visible", timeout=30000)

links = GetHtmlElements("article.product_pod h3 a")
prices = GetHtmlElements("article.product_pod p.price_color")
availability = GetHtmlElements("article.product_pod p.instock")

Assert("{{ len(links) == len(prices) }}")
WriteLine("{{ len(links) }} titles on the page")

CreateTable("catalogue", schema={"columns": [{"name": "title", "isKey": True}, {"name": "price"}, {"name": "availability"}, {"name": "url"}]})

for index, link in links: # _name="Collect one tile" _description="Columns come from three parallel element arrays joined on the tile's position; a length mismatch would already have failed the Assert above"
TableUpsertRow("catalogue", {"title": "{{ link.title }}", "price": "{{ prices[index].textContent }}", "availability": "{{ trim(availability[index].textContent) }}", "url": "{{ link.href }}"})

export = TableExport("catalogue", "C:/Temp/rinkt-examples/catalogue.xlsx", format="xlsx", sheetName="Mystery")
WriteLine("Wrote {{ export.rows }} rows to {{ export.filePath }}")

The run

Two and a half seconds, 20 rows:

Activity completed [Export Table to File] -> {"filePath": "C:/Temp/rinkt-examples/catalogue.xlsx", "format": "xlsx", "rows": 20}
titlepriceavailabilityurl
Sharp Objects£47.82In stockhttps://books.toscrape.com/catalogue/sharp-objects_997/index.html
In a Dark, Dark Wood£19.63In stockhttps://books.toscrape.com/catalogue/in-a-dark-dark-wood_963/index.html
The Past Never Ends£56.50In stockhttps://books.toscrape.com/catalogue/the-past-never-ends_942/index.html

What an element actually is

Get Elements returns objects, not strings. Each one carries:

src boundingRect tagName id href title value
outerHTML className textContent innerHTML name

Two things follow, and both cost a run to find out:

innerText is not there. It resolves to nothing. The text of an element is textContent.

textContent is what the page shows, truncation included. The tile in this catalogue displays In a Dark, Dark ..., and that is exactly what textContent returns. The full string is in the title attribute, which is why the workflow reads link.title for the name and prices[index].textContent for the price. Check both before choosing.

What it got wrong first

The wait was written against the tiles themselves:

WaitForElement("article.product_pod", state="visible") # wrong
strict mode violation: locator('article.product_pod') resolved to 20 elements

A wait targets exactly one element. Twenty matches is an error, not a wait that settles on the first. Point it at something singular — here, the first tile inside the list — ol.row li:nth-child(1) article.product_pod. Get Elements on the same page is fine with twenty, because a plural result is what it is for.

The failure is worth reading for a second reason: the error names every element it matched, and the platform's failure envelope records selectorMatchCount: 20, re-queried at the moment it broke. You are told how ambiguous the selector was, not merely that it was.

Worth taking from it

for index, item in collection: gives the loop an index as well as an iterator, which is what joins parallel arrays: prices[index] next to link.

Assert what the join assumes. Assert("{{ len(links) == len(prices) }}") fails the run at the point the assumption breaks, rather than producing a spreadsheet whose price column is off by one from the row the page it came from. The designer flags the input as expecting a boolean and getting a string — an expression is a string until it is evaluated — and corrects the type for the run.