Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 12 из 25 ~18 мин +50 XP

Пагинация: обходим все страницы каталога

colly пагинация держится на одной идее: ставить следующую страницу в очередь изнутри колбэка. Разберём каноничный паттерн с books.toscrape.com, генерацию номеров страниц циклом, счётчик-лимит для вечных каталогов и стоп-условия, которые работают сами.

Содержание урока · 10 разделов

В предыдущих уроках скрапер обрабатывал одну страницу — максимум список адресов, выписанный руками. Каталог ломает этот комфорт: пятьдесят страниц, у каждой ссылка «next» внизу, а на последней ссылки нет. Пятьдесят ссылок руками — ошибка уже на второй странице, потому что шаг нумерации у каталогов разный: где-то /page/2/, где-то ?page=2, где-то вообще /catalogue/page-3.html. Задача пагинации формулируется коротко: начать с первой страницы и собирать следующие, пока они есть. Всё остальное — вариации двух вопросов: откуда брать адрес следующей страницы и когда останавливаться. Рабочих подхода три, и у каждого свой способ сломаться; к концу урока вы будете знать все три — и обязательно поставите лимит, без которого однажды потеряете ночь прогона.

Пагинация по ссылке «следующая»#

Каноничный паттерн colly пагинация держится на двух методах объекта e.Request: AbsoluteURL разворачивает относительный href в полный адрес от текущей страницы, а Visit ставит её в очередь того же коллектора. Схема: один OnHTML снимает данные со страницы, второй следит за ссылкой «next». Когда обходчик долистывает каталог до последней страницы, ссылки «следующая» там нет — колбэк рулевого не срабатывает, очередь пустеет, и c.Visit в main спокойно возвращается. Стоп-условие здесь не пишется: оно получается само из разметки. За это паттерн и любят: минимум кода, ноль арифметики с номерами страниц, устойчивость к росту — каталог вырос с пятидесяти страниц до восьмидесяти, код не заметил. Плата — зависимость от вёрстки: класс li.next — договорённость с конкретным сайтом, и смена вёрстки молча обрывает обход на первой странице.

Полный обход books.toscrape.com

GO
package main

import (
        "fmt"

        "github.com/gocolly/colly/v2"
)

func main() {
        c := colly.NewCollector(
                colly.AllowedDomains("books.toscrape.com"),
        )

        c.OnHTML("article.product_pod h3 a", func(e *colly.HTMLElement) {
                fmt.Println(e.Text)
        })

        c.OnHTML("li.next a", func(e *colly.HTMLElement) {
                e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href")))
        })

        c.Visit("http://books.toscrape.com/")
}

Два OnHTML: первый снимает названия книг со страницы, второй — рулевой. На books.toscrape.com ссылка «next» живёт в li.next a, и href там относительный: catalogue/page-2.html. AbsoluteURL разворачивает его в полный адрес от текущей страницы, e.Request.Visit ставит страницу в очередь того же коллектора — возвращаться в main и городить цикл не нужно. Обход закончится сам: на последней странице li.next нет, колбэк молчит, очередь пустеет.

Пара тонкостей, о которые спотыкаются на этом паттерне. Первая: Visit из колбэка увеличивает глубину обхода — MaxDepth из урока про навигацию по ссылкам действует и здесь, и с настройкой по умолчанию всё в порядке. Вторая: паттерн не отличает «next» от «previous», если в разметке есть оба; books.toscrape держит только next, но li.previous a на других сайтах встречается рядом — уточняйте селектор, иначе скрапер будет ходить по кругу. Третья — про фильтры: AllowedDomains в примере не украшение, а страховка. Ссылка «next» — обычная ссылка, и она может увести куда угодно, если каталог ссылается на чужой домен или проверяет вас редиректом. Правила «куда можно ходить» разобраны в уроке про URL-фильтры — для пагинации AllowedDomains почти обязателен.

Генерация номеров страниц: когда URL предсказуем#

Второй подход не смотрит в разметку вовсе: адреса страниц выводятся циклом. Он работает, когда пагинация вшита в URL: /catalogue/page-2.html, ?page=3, /page/7/ — закономерность видна на второй странице, дальше ничего не меняется. Такой вариант короче, не зависит от классов вёрстки и честно идёт до конца диапазона. Стоп-условие здесь — конец цикла или ошибка Visit: несуществующий page-51.html возвращает 404, Visit вернёт ошибку, цикл обрывается. Плата за простоту — молчаливые пропуски. Если шаг нумерации иной — скажем, страницы идут десятками или адреса построены от идентификаторов, — цикл будет стучаться в пустоту или пропустит середину, и вы узнаете об этом по дырявым данным, а не по сообщению об ошибке.

Цикл по номерам страниц

GO
package main

import (
        "fmt"

        "github.com/gocolly/colly/v2"
)

func main() {
        c := colly.NewCollector(
                colly.AllowedDomains("books.toscrape.com"),
        )

        c.OnHTML("article.product_pod h3 a", func(e *colly.HTMLElement) {
                fmt.Println(e.Text)
        })

        for p := 1; p <= 50; p++ {
                pageURL := fmt.Sprintf("http://books.toscrape.com/catalogue/page-%d.html", p)
                if err := c.Visit(pageURL); err != nil {
                        fmt.Println("стоп на странице", p, err)
                        break
                }
        }
}

У books.toscrape.com пагинация вшита в адрес: catalogue/page-N.html, всего 50 страниц по 20 книг — тысяча позиций, удобно для обучения. Цикл генерирует адреса, не глядя в разметку. 404 на несуществующей странице вернёт ошибку из Visit, и break обрывает обход с понятным сообщением, на какой странице остановились.

Выбор между паттерном с «next» и циклом — вопрос разметки, а не вкуса: кнопка next есть и стабильна — берите паттерн; адреса предсказуемы, а кнопка спрятана в скриптах — берите цикл. Здоровый скрапер часто совмещает: цикл по известному диапазону с проверкой разметки внутри. И ещё одна деталь, которую легко недооценить: colly дедуплицирует адреса — повторный Visit того же URL в пределах одного коллектора игнорируется, поэтому случайный заход на уже собранную страницу не удвоит данные. Но подстраховки это не отменяет: дедупликация работает на точное совпадение адреса, а ?page=2 и /page-2.html для неё — разные страницы, даже если сервер отдаёт одно и то же.

Ошибки посреди диапазона: дыры в данных#

Break в цикле останавливает обход на первой же ошибке, и это не всегда то, чего вы хотите. Кратковременный сбой сети на page-7 обрывает сбор, хотя page-8 и все следующие живы: отчёт соберётся до середины, дыру найдёт читатель, а не скрипт. Разумнее различать ошибки по смыслу. 404 на несуществующей странице — законный конец диапазона: каталог кончился, и остановка здесь корректна. Таймаут или обрыв соединения — сигнал, что страницу стоит повторить, а не прекращать обход. Практичная схема: ошибки не прерывают цикл, а копят счётчик подряд идущих неудач; одиночная неудача — продолжаем, три подряд — останавливаемся, каталог, скорее всего, кончился или лёг. Страницы, на которых собрать данные не удалось, запоминаются отдельным списком — их прогоняют вторым заходом после окончания основного. Что положено в OnError и как строить повторы руками — урок про ошибки и таймауты; здесь достаточно принять, что break на каждой ошибке — решение для учебного скрапера, а не для боевого.

Счётчик-ограничитель: страховка, которая всегда должна быть#

Теперь живое место — самое важное в уроке. Пагинация, которая ходит по ссылкам «следующая», однажды встретит каталог без конца: календарь, где next переводит на следующий месяц вечно; бесконечная лента, где сервер генерирует страницу от параметра смещения; каталог с багом, где ссылка «next» указывает на ту же страницу. Скрапер честно ходит по кругу часами: очередь не пустеет, диски заполняются дублями, источник получает тысячи запросов. Лимит страниц — страховка, которая должна стоять всегда, даже когда кажется, что каталог конечен: заранее посчитать «сколько страниц ожидаем» и завести счётчик с полем max. Пятёрка в примере ниже — не про скромность, а про демонстрацию; в рабочем скрапере лимит выносится в конфиг или флаг командной строки, а его срабатывание логируется отдельно: если каталог внезапно вырос, вы хотите знать, что лимит отработал, а не молча потерять страницы.

Структура-замыкание с полем max

GO
type pager struct {
        count int
        max   int
}

lim := pager{max: 5}

c.OnHTML("li.next a", func(e *colly.HTMLElement) {
        if lim.count >= lim.max {
                fmt.Println("лимит страниц достигнут, обход остановлен")
                return
        }
        lim.count++
        e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href")))
})

lim — структура, которую колбэк видит по ссылке из замыкания: count копится между вызовами OnHTML, max задаёт потолок. Превышение — тихий return: следующая страница просто не ставится в очередь, и обход останавливается сам. В боевом скрапере на этом месте пишут лог: остановка по лимиту должна отличаться в отчёте от естественного конца каталога.

Три подхода к пагинации#

ПодходКогда братьГлавные риски
Ссылка «следующая»кнопка next стабильна, каталог конечензависимость от классов вёрстки; вечный цикл без лимита
Цикл по номерамадреса предсказуемы: page-N.html, ?page=Nмолчаливые пропуски при ином шаге; лишние 404 в конце
Гибрид: цикл плюс разметкабольшой каталог с ненадёжной кнопкой nextсложнее отлаживать; два источника истины о конце обхода

Вечный каталог: как это выглядит изнутри#

Разберу, как вечная пагинация выглядит на практике, чтобы её было проще узнать. Календарные страницы: у события «следующий месяц» всегда есть ссылка «следующий» — обход по ссылкам генерирует URL бесконечно, потому что следующего месяца не бывает только в маркетинге. Смещения вместо номеров: ?offset=50, ?before=2024-01-01 — сервер берёт параметр и строит следующую страницу от него; адресов всегда ровно столько, на сколько хватит терпения генерировать. Луп на самом себе: баг вёрстки или шаблона, где next указывает на текущую страницу; colly тут спасает дедупликация, но ?page=2 и ?page=2&sort=asc — уже разные адреса, и круг замыкается. Общий знаменатель один: заранее неизвестный конец. Поэтому лимит — не опция, а гигиена, как ремень в машине: пристёгиваются не потому, что сегодня авария, а потому что цена забывания несоразмерна.

Проверка: естественное стоп-условие

Обходчик ходит по OnHTML("li.next a"). На текущей странице ссылки «следующая» нет. Что произойдёт?

Проверка: AbsoluteURL

Что делает e.Request.AbsoluteURL(e.Attr("href"))?

Сколько страниц обойдёт скрапер?

Прочитайте код и выберите, что он выведет:

lim := pager{max: 2}

c.OnHTML("li.next a", func(e *colly.HTMLElement) {
        if lim.count >= lim.max {
                return
        }
        lim.count++
        e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href")))
})

c.Visit("http://books.toscrape.com/")

Восстановите рулевой колбэк

Селектор ссылки «next», разворачивание href и постановка страницы в очередь.

c.OnHTML("", func(e *colly.HTMLElement) { e.Request.(e.Request.(e.Attr("href"))) })

Соберите обход с лимитом

От объявления счётчика до стартовой страницы.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: пагинация

Термин

Пагинация

нажмите, чтобы перевернуть
Определение

Обход страниц каталога по ссылкам «следующая» или по предсказуемым URL /page=N. Стоп — когда ссылки нет или сработал лимит.

1 / 6

Что дальше по курсу#

Пагинация — мост к масштабу: пятьдесят страниц по одной скучны, а параллельный обход каталога — тема урока про Async и WaitGroup: поставьте Parallelism, и каталог соберётся в разы быстрее. Ошибки на полпути — 404 посреди диапазона, обрыв связи — живут в уроке про OnError и повторы. А правила «куда вообще можно ходить» — URL-фильтры, которые для пагинации почти обязательны.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Паттерн colly пагинация: OnHTML по ссылке «next» ставит следующую страницу в очередь через e.Request.Visit и AbsoluteURL.
  • Стоп-условие «нет ссылки — нет обхода» получается само: колбэк не срабатывает, очередь пустеет.
  • Предсказуемые URL (/page-N.html, ?page=N) позволяют идти циклом: короче и без зависимости от вёрстки, но пропуски молчаливы.
  • Лимит страниц — страховка от вечных каталогов: структура-замыкание с полем max останавливает обход принудительно.
  • Календари и смещения генерируют адреса бесконечно; лимит обязателен всегда, а его срабатывание логируется.
  • AllowedDomains и LimitRule при пагинации — гигиена: запросы умножаются, и всплеск становится инцидентом.

Домашнее задание

Соберите полный обход books.toscrape.com двумя способами: паттерном с li.next a и циклом по page-N.html. Сравните количество собранных названий — должно совпасть. Затем сломайте свой скрапер осознанно: поставьте lim.max равным трём и убедитесь, что обход останавливается на четвёртой странице. После этого уберите AllowedDomains и посмотрите в логе OnRequest, куда пытается ходить скрапер, если ссылке «next» доверять без ограничений.

Личные заметкиПусто
Хранятся только в вашем браузере (Colly · Пагинация: обходим все страницы каталога)

Вопросы и ответы по уроку

Как обойти пагинацию, у которой нет ссылки «следующая»?

Если URL предсказуем — идти циклом по номерам или параметрам, обрывая обход по ошибке Visit. Если адреса строятся из данных страницы — смещения, идентификаторы, — вынимайте их отдельным OnHTML и ставьте в очередь тем же e.Request.Visit. Лимит страниц стоит в любом варианте.

Почему скрапер ходит по кругу и собирает одни и те же страницы?

Либо ссылка «next» указывает на текущую страницу — баг каталога, либо рядом с next есть previous, и селектор ловит обе ссылки. Уточните селектор, включите лог URL в OnRequest и держите счётчик-лимит: он и есть предохранитель от круга.

Насколько большим можно ставить лимит страниц?

Достаточно большим, чтобы покрыть каталог с запасом, и достаточно малым, чтобы вечный цикл не съел ночь прогона. Практика: ожидаемое число страниц плюс небольшой запас, а превышение — отдельная запись в логе, а не тихий return.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Testify25 уроков~9 чпрактика25 уроков уже доступны

Testify: тестирование Go с assert и require

Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.

  • Писать читаемые проверки через assert и require, а не лестницы if
  • Проверять ошибки, паники и краевые случаи без ручного кода
  • Сравнивать структуры, слайсы, карты и числа с плавающей точкой
  • Строить табличные тесты и сьюты с хуками подготовки