A stdlib-only scraper: how to pick a row container, why one wrapper div breaks your selector, and how to do pagination and detail pages correctly.
Every scraping tutorial starts with pip install selenium. On a locked-down machine that
is where the project ends. But most list pages are plain server-rendered HTML, and parsing them needs
three things: an HTML parser, a selector, and the discipline to check you got rows before writing a
file.
The mistake is starting from a field (span.price) and hoping you can line things up
later. Start from the container that repeats — one card, one table row, one list item — and extract
every field inside it. That is what keeps row 3's price with row 3's name.
--row "div.list > div.card"
--field "sku=@data-sku" an attribute of the container itself
--field "name=h3.name@text"
--field "price=span.price@text"
--field "link=a.detail@href"
Real pages contain this:
<div class="card"> <!-- page header block, same class -->
<h3>Catalogue</h3>
</div>
<div class="list">
<div class="card"> ... 9 real cards ... </div>
</div>
div.card alone returns 10 containers and one of them is a header with no price.
div.list > div.card returns exactly the 9. The fix is not "filter empty rows later" —
filtering hides the bug, and next month the header gains a price element and starts producing phantom
products.
Test the selector in the browser first: document.querySelectorAll('div.list > div.card').length
in the console tells you the right answer before your script disagrees with you.
<h3 class="name">
Gasket Ring 1
</h3>
The text is "\n Gasket Ring 1\n". Any == "Gasket Ring 1" comparison
fails. Collapse whitespace on extraction (re.sub(r"\s+", " ", t).strip()) and do the
comparison after, never before.
next_href = first_match("div.pager > a.next@href")
url = urllib.parse.urljoin(url, next_href) # "/list/2.html" -> absolute
if url in visited: break # a "next" that points back to page 1
That second line is not paranoia. Sites with a "last page" link, or a template that renders the pager wrongly on the final page, will loop forever. A visited set costs one line.
--field "link=a.detail@href" --follow link
--detail-field "desc=p.desc@text"
--detail-field "made_in=table.spec td@text"
Always keep the detail URL in the output. When a field turns out to be wrong three weeks later, the URL is how you re-check the source without re-crawling.
no rows extracted — check --row and the field selectors
[!] these fields came back empty every time: ['price'] — selector probably wrong
Two different bugs. The first is the container. The second is one field: rows came back, so the container is right, but that field is empty on every row — a selector typo, or the field is inside an element that only some rows have. Without that check you get a CSV with a blank column and no idea why.
Python-urllib/3.x.robots.txt. It is not enforced by anything except your own judgement, which is
the point.If the data is rendered by JavaScript, the HTML you fetch has an empty container in it. That is not a selector problem and no amount of retrying fixes it — you need a headless browser or the site's own API. Recognizing this in ten seconds (view-source, search for your value) saves an hour of debugging a selector that can never match.
WebScrape Kit is this whole pipeline in one dependency-free file: CSS-lite selectors (tag, class, id, attributes, descendant and child combinators), pagination with a visited set, per-row detail pages, incremental runs via a seen-file, backoff and delay, CSV / JSON / xlsx output, and the two error messages above. It also ships an offline fixture site — including the wrapper-div trap — so you can verify it works before pointing it at anything real. 29 USDT.
Turn an HTML page into a spreadsheet with a CSS selector — one file, stdlib only, nothing to install.
Get it — 29 USDT