Кнопка «Показать ещё»: загрузка порциями (Python)
Собрать каталог, в котором следующая порция появляется после нажатия кнопки. Важно дождаться не только ответа сервера, но и новых карточек на странице.
Выберите язык Python, вставьте полный исходник из этой статьи и задайте параметры запуска ниже. Источник для всей подборки: ScrapingCourse. Разбор относится к указанной демонстрационной странице.
Настройки примера
{
"path": "/button-click",
"mode": "button",
"expected": 147
}
path выбирает страницу, mode выбирает ветку алгоритма, expected задаёт контрольное число записей для этого примера. Значение expected не является лимитом тарифа. Если набор данных источника меняется, сверяйте ожидаемое количество с ним.
Как работает алгоритм
- Открываем
/button-clickв браузере. Маска*/ajax/products*включает наблюдение за ответами загрузки товаров ещё до первого нажатия. - Ждём заполненное название товара, затем читаем уже показанные карточки. Наличие пустого контейнера само по себе не означает, что товары готовы.
- Находим
#load-more-btn. Если кнопка исчезла или в её inline-стиле указаноdisplay: none, считаем выдачу завершённой. - Перед кликом запоминаем
data-offsetи число карточек. По offset затем узнаём ответ именно на эту порцию, а не случайный запрос страницы. - После клика ждём соответствующий ответ до 15 секунд. Проверяем HTTP-статус, отсутствие ошибки и признака усечённого тела. Пустое тело завершает выдачу, в том числе при ответе 204.
- Если тело непустое, ждём появления карточки с номером
count + 1. Только после обновления DOM начинаем следующий проход. - В конце ещё раз читаем страницу, сверяем уникальные товары и закрываем браузер в
finally.
Карточки обрабатывает collect(html). Сначала читается название. Пустое название пропускается, ссылка карточки становится ключом, а при её отсутствии используется имя. seen не даёт повторно сохранить один и тот же ключ. Это важно, когда очередное чтение HTML включает уже собранные товары.
На что обратить внимание
Один проход цикла соответствует одной проверке или загрузке порции. Предел 30 проходов защищает от бесконечных нажатий; 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 карточки. |
Полный код сценария
Исходник общий для всей подборки. Для этого примера в нём выполняется ветка button, выбранная параметрами выше. Блок можно целиком скопировать в редактор 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