К содержимому

Документация

Пагинация: переход по ссылке «Далее» (Python)

Пройти каталог от первой страницы до последней, не задавая заранее количество страниц. Сценарий ориентируется на ссылки самого источника.

Выберите язык Python, вставьте полный исходник из этой статьи и задайте параметры запуска ниже. Источник для всей подборки: ScrapingCourse. Разбор относится к указанной демонстрационной странице.

Настройки примера

{
  "path": "/pagination",
  "mode": "http",
  "expected": 147
}

path выбирает страницу, mode выбирает ветку алгоритма, expected задаёт контрольное число записей для этого примера. Значение expected не является лимитом тарифа. Если набор данных источника меняется, сверяйте ожидаемое количество с ним.

Как работает алгоритм

  1. Открываем /pagination с параметром mode: http. Переменная url хранит адрес страницы, которую нужно обработать сейчас.
  2. Проверяем, нет ли адреса в visited, и отмечаем его как посещённый. Это отдельная проверка страниц, а не товаров.
  3. Получаем HTML, затем передаём его функции сбора. Она добавляет только ещё не встречавшиеся товары.
  4. Ищем a[rel="next"] или a.next.page-numbers. Отсутствие такой ссылки завершает цикл; номер страницы не приходится угадывать.
  5. Относительную ссылку приводим к полному URL. Поэтому переход работает и с полным адресом, и с короткой ссылкой источника.
  6. После завершения сравниваем число уникальных товаров с expected. Проверка помогает заметить пропущенную страницу.

Карточки обрабатывает collect(html). Сначала читается название. Пустое название пропускается, ссылка карточки становится ключом, а при её отсутствии используется имя. seen не даёт повторно сохранить один и тот же ключ. Это важно, когда очередное чтение HTML включает уже собранные товары.

На что обратить внимание

У алгоритма две разные памяти: visited не даёт ходить по кругу, а seen не даёт сохранять один товар повторно.

Особенности Python

В Python SDK работает последовательно: async и await в этом примере не нужны. Имена методов записаны через подчёркивание: without_cache, wait_for, wait_for_response. Вложенная collect использует общий set с ключами и возвращает число карточек в текущем HTML. В finally вызывается page.close().

Что получится

Для настроенного демонстрационного набора ожидаются 147 уникальных записей. Общая проверка в конце сравнивает фактическое число ключей с expected.

Поле Что содержит
name Название товара после удаления пробелов по краям.
price Цена, преобразованная из текста в число.
image Адрес изображения из атрибута src карточки.

Полный код сценария

Исходник общий для всей подборки. Для этого примера в нём выполняется ветка http, выбранная параметрами выше. Блок можно целиком скопировать в редактор Python; реквизиты доступа и ключи сервисов в него добавлять не требуется.

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("Пагинация вернула уже обработанную страницу")
            visited.add(url)
            response = ctx.http().without_cache().get(url, auto)
            if not response.ok():
                raise ValueError("Источник не открылся: " + 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("Превышен защитный предел пагинации")
    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("Страница не содержит учебных реквизитов")
                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("Защищённое содержимое не получено")
                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("Не удалось загрузить следующую порцию")
                        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("Превышен защитный предел загрузки")
                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("Лента продолжает загружаться: увеличьте предел прокрутки")
                collect(page.content())
        finally:
            page.close()
    expected = int(ctx.param("expected", 1))
    if mode != "challenge" and len(seen) != expected:
        raise ValueError("Неполный результат: " + str(len(seen)) + " вместо " + str(expected))

Куда посмотреть дальше

Общий алгоритм и параметры · HTTP-запросы · Действия браузера · Поиск элементов HTML