Skip to content

Documentation

Load more: collect products in batches (PHP)

Collect a catalog that appends another batch after a button click. Wait for both the server response and the new cards in the page.

Choose PHP, 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": "/button-click",
  "mode": "button",
  "expected": 147
}

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. Open /button-click in a browser. The */ajax/products* pattern starts observing product responses before the first click.
  2. Wait for a nonempty product name, then collect the cards already displayed. An empty container alone does not mean that products are ready.
  3. Find #load-more-btn. A missing button or an inline display: none style indicates that loading is complete.
  4. Before clicking, remember data-offset and the current card count. The offset identifies the response for this batch rather than an unrelated page request.
  5. Click and wait up to 15 seconds for that response. Check its HTTP status, error field and truncation flag. An empty body ends the list, including a 204 response.
  6. For a nonempty response, wait for card number count + 1. Start the next iteration only after the DOM has changed.
  7. Read the final page once more, verify the unique product count and close the browser in finally.

Products are processed by courseCollect($ctx, $html, $seen). 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

Each loop iteration checks or loads a batch. The 30-iteration limit prevents endless clicking; seen removes duplicates when the whole page is read again.

PHP details

PHP calls network and browser methods sequentially with ->. courseCollect receives $seen and returns its updated array, so assign each result back. Close the page in finally; $ctx->fail() stops the run with a readable message.

Expected output

The configured demonstration dataset expects 147 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 button branch using the parameters above. Copy the complete block into the PHP editor; no fixed sign-in credentials or service keys need to be added.

<?php

function run(\Sdk\Context $ctx): void
{
    $origin = 'https://www.scrapingcourse.com';
    $path = (string) $ctx->param('path', '/pagination');
    $mode = (string) $ctx->param('mode', 'http');
    $auto = $ctx->param('auto_captcha', true) !== false;
    $seen = [];
    if ($mode === 'http') {
        $url = $origin.$path;
        $visited = [];
        while ($url !== null && count($visited) < 50) {
            if (isset($visited[$url])) {
                $ctx->fail('Pagination returned an already processed page');
            }
            $visited[$url] = true;
            $response = $ctx->http()->withoutCache()->get($url, $auto);
            if (! $response->ok()) {
                $ctx->fail('The source could not be opened: '.$response->status);
            }
            $html = $response->body;
            $seen = courseCollect($ctx, $html, $seen);
            $next = $ctx->dom($html)->attr('a[rel="next"], a.next.page-numbers', 'href');
            $url = $next ? $ctx->url()->absolutize($url, $next) : null;
        }
        if ($url !== null) {
            $ctx->fail('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 = '';
                foreach ($dom->qa('p') as $p) {
                    if (str_contains($p->text(), 'Demo Credentials:')) {
                        $hint = $p->text();
                    }
                }
                preg_match('/Password:\s*([^\s|]+)/i', $hint, $password);
                preg_match('/Email:\s*([^\s|]+@[^\s|]+)/i', $hint, $emailMatch);
                $email = $emailMatch[1] ?? '';
                if ($email === '' || ! isset($password[1])) {
                    $ctx->fail('The page does not contain demo credentials');
                }
                $page->type('#email', $email)->type('#password', $password[1])->click('button[type="submit"]');
            }
            if ($mode === 'challenge') {
                $text = preg_replace('/\s+/', ' ', $ctx->dom($page->content())->text('body') ?? '');
                if (! preg_match('/You (?:successfully )?bypassed|successfully bypassed|Congratulations/i', $text)) {
                    $ctx->fail('The expected page content was not received');
                }
                $ctx->emit(['name' => $path, 'passed' => true], $path);
            } else {
                $page->waitFor('.product-name:not(:empty)', 15000);
                if ($mode === 'button') {
                    $complete = false;
                    for ($i = 0; $i < 30; $i++) {
                        $html = $page->content();
                        $before = $ctx->dom($html);
                        $seen = courseCollect($ctx, $html, $seen);
                        $button = $before->q('#load-more-btn');
                        if ($button === null || preg_match('/display:\s*none/', $button->attr('style') ?? '')) {
                            $complete = true;
                            break;
                        }
                        $offset = $button->attr('data-offset');
                        $count = count($before->qa('.product-item'));
                        $page->click('#load-more-btn');
                        $response = $page->waitForResponse('*offset='.$offset, 15000);
                        if ($response === null || ($response['status'] < 200 || $response['status'] >= 300) || $response['truncated'] || $response['error']) {
                            $ctx->fail('The next batch could not be loaded');
                        }
                        if (trim($response['body'] ?? '') === '') {
                            $complete = true;
                            break;
                        }
                        $page->waitFor('.product-item:nth-child('.($count + 1).')');
                    }
                    if (! $complete) {
                        $ctx->fail('The batch loading safety limit was reached');
                    }
                }
                if ($mode === 'scroll') {
                    $stable = 0;
                    $previous = 0;
                    for ($i = 0; $i < 40 && $stable < 3; $i++) {
                        $page->scroll(3, 400);
                        $html = $page->content();
                        $seen = courseCollect($ctx, $html, $seen);
                        $count = count($ctx->dom($html)->qa('.product-item'));
                        $stable = $count === $previous ? $stable + 1 : 0;
                        $previous = $count;
                    }
                    if ($stable < 3) {
                        $ctx->fail('The feed is still loading: increase the scroll limit');
                    }
                }
                $seen = courseCollect($ctx, $page->content(), $seen);
            }
        } finally {
            $page->close();
        }
    }
    $expected = (int) $ctx->param('expected', 1);
    if ($mode !== 'challenge' && count($seen) !== $expected) {
        $ctx->fail('Incomplete result: '.count($seen).' instead of '.$expected);
    }
}

function courseCollect(\Sdk\Context $ctx, string $html, array $seen): array
{
    $dom = $ctx->dom($html);
    foreach ($dom->qa('.product-item, li.product') as $card) {
        $name = trim($card->q('.product-name, .woocommerce-loop-product__title')?->text() ?? '');
        $key = $card->q('a')?->attr('href') ?? $name;
        if ($name === '' || isset($seen[$key])) {
            continue;
        }
        $seen[$key] = true;
        $ctx->emit(['name' => $name, 'price' => $ctx->text()->money($card->q('.product-price, .price')?->text() ?? ''),
            'image' => $card->q('img')?->attr('src') ?? ''], $key);
    }

    return $seen;
}

Related reading

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