Skip to content

Documentation

Online store: collect the complete catalog (Python)

Collect a complete store catalog when products span several pages. Use the pattern to refresh a product and price list regularly.

Choose Python, paste the complete source from this article and set the run parameters below. The source for this collection is ScrapingCourse. This walkthrough applies to the specified demonstration page.

Example settings

{
  "path": "/ecommerce/",
  "mode": "http",
  "expected": 188
}

path selects the page, mode selects an algorithm branch, and expected sets the control count for this example. It is not a plan limit. When the source dataset changes, check the expected count against it.

How the algorithm works

  1. Start at /ecommerce/. The http mode selects ordinary HTML requests; this catalog does not require browser clicks.
  2. Add each URL to visited before requesting it. If the next-page link points back to a processed page, stop instead of looping indefinitely.
  3. Request fresh HTML and check the response. withoutCache() reads the current catalog rather than previously cached HTML.
  4. Find products using two markup variants: .product-item and li.product. Product names and prices also have selectors for both card layouts.
  5. Read the next-page link. absolutize resolves a relative address against the current page URL.
  6. Finish when there is no next link, then compare the number of unique products with expected.

Products are processed by collect(html). Read the name first, skip empty names, and use the card link as the key, falling back to the name when necessary. seen prevents the same key being emitted twice. This matters when a later HTML read includes products already collected.

Details that matter

The HTTP branch allows at most 50 distinct pages. This protects the algorithm against incorrect links; the expected product count is a separate setting.

Python details

The Python SDK is sequential here: no async or await is needed. Method names use snake_case: without_cache, wait_for, wait_for_response. The nested collect uses a shared key set and returns the current HTML card count. finally calls page.close().

Expected output

The configured demonstration dataset expects 188 unique records. The final check compares the actual unique key count with expected.

Field Contents
name Product name with leading and trailing whitespace removed.
price Price converted from text to a number.
image Image URL read from the card src attribute.

Complete scenario source

The source is shared by this collection. This example runs its http branch using the parameters above. Copy the complete block into the Python editor; no fixed sign-in credentials or service keys need to be added.

import re


def run(ctx):
    origin = "https://www.scrapingcourse.com"
    path = str(ctx.param("path", "/pagination"))
    mode = str(ctx.param("mode", "http"))
    auto = ctx.param("auto_captcha", True) is not False
    seen = set()

    def collect(html):
        dom = ctx.dom(html)
        for card in dom.qa(".product-item, li.product"):
            title = card.q(".product-name, .woocommerce-loop-product__title")
            name = title.text().strip() if title else ""
            link = card.q("a")
            key = (link.attr("href") if link else "") or name
            if not name or key in seen:
                continue
            price = card.q(".product-price, .price")
            image = card.q("img")
            seen.add(key)
            ctx.emit({"name": name, "price": ctx.text().money(price.text() if price else ""),
                      "image": image.attr("src") if image else ""}, key)
        return len(dom.qa(".product-item, li.product"))

    if mode == "http":
        url = origin + path
        visited = set()
        while url and len(visited) < 50:
            if url in visited:
                raise ValueError("Pagination returned an already processed page")
            visited.add(url)
            response = ctx.http().without_cache().get(url, auto)
            if not response.ok():
                raise ValueError("The source could not be opened: " + str(response.status))
            html = response.body
            collect(html)
            next_url = ctx.dom(html).attr('a[rel="next"], a.next.page-numbers', "href")
            url = ctx.url().absolutize(url, next_url) if next_url else None
        if url:
            raise ValueError("The pagination safety limit was reached")
    else:
        page = ctx.browser().open(origin + path, ["*/ajax/products*"], {}, auto)
        try:
            if mode == "login":
                dom = ctx.dom(page.content())
                hint = next((p.text() for p in dom.qa("p") if "Demo Credentials:" in p.text()), "")
                password = re.search(r"Password:\s*([^\s|]+)", hint, re.I)
                email_match = re.search(r"Email:\s*([^\s|]+@[^\s|]+)", hint, re.I)
                email = email_match.group(1) if email_match else ""
                encoded = dom.attr("[data-cfemail]", "data-cfemail")
                if not email and encoded and len(encoded) > 2:
                    key = int(encoded[:2], 16)
                    email = "".join(chr(int(encoded[i:i + 2], 16) ^ key) for i in range(2, len(encoded), 2))
                if not email or not password:
                    raise ValueError("The page does not contain demo credentials")
                page.type("#email", email)
                page.type("#password", password.group(1))
                page.click('button[type="submit"]')
            if mode == "challenge":
                text = re.sub(r"\s+", " ", ctx.dom(page.content()).text("body") or "")
                if not re.search(r"You (?:successfully )?bypassed|successfully bypassed|Congratulations", text, re.I):
                    raise ValueError("The expected page content was not received")
                ctx.emit({"name": path, "passed": True}, path)
            else:
                page.wait_for(".product-name:not(:empty)", 15000)
                if mode == "button":
                    complete = False
                    for step in range(30):
                        html = page.content()
                        before = ctx.dom(html)
                        collect(html)
                        button = before.q("#load-more-btn")
                        if not button or re.search(r"display:\s*none", button.attr("style") or ""):
                            complete = True
                            break
                        offset = button.attr("data-offset")
                        count = len(before.qa(".product-item"))
                        page.click("#load-more-btn")
                        response = page.wait_for_response("*offset=" + offset, 15000)
                        if not response or not 200 <= response["status"] < 300 or response["truncated"] or response["error"]:
                            raise ValueError("The next batch could not be loaded")
                        if not (response.get("body") or "").strip():
                            complete = True
                            break
                        page.wait_for(".product-item:nth-child(" + str(count + 1) + ")")
                    if not complete:
                        raise ValueError("The batch loading safety limit was reached")
                if mode == "scroll":
                    stable, previous = 0, 0
                    for step in range(40):
                        page.scroll(3, 400)
                        count = collect(page.content())
                        stable = stable + 1 if count == previous else 0
                        previous = count
                        if stable >= 3:
                            break
                    if stable < 3:
                        raise ValueError("The feed is still loading: increase the scroll limit")
                collect(page.content())
        finally:
            page.close()
    expected = int(ctx.param("expected", 1))
    if mode != "challenge" and len(seen) != expected:
        raise ValueError("Incomplete result: " + str(len(seen)) + " instead of " + str(expected))

Related reading

Shared algorithm and parameters · HTTP requests · Browser actions · Find HTML elements