---
title: "Пагинация: обходим все страницы каталога"
description: "colly пагинация на books.toscrape: ссылка «следующая» через e.Request.Visit, цикл по ?page=N, счётчик-лимит для вечных каталогов. Разбор парсера каталога страниц."
course: "Colly: парсинг и веб-скрапинг на Go"
url: "https://golangmaster.ru/courses/colly/lessons/12-paginaciya/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «Colly: парсинг и веб-скрапинг на Go», урок 12 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/colly/lessons/12-paginaciya/*

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

В предыдущих уроках скрапер обрабатывал одну страницу — максимум список адресов, выписанный руками. Каталог ломает этот комфорт: пятьдесят страниц, у каждой ссылка «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 из [урока про навигацию по ссылкам](/courses/colly/lessons/08-navigaciya-ssylki/) действует и здесь, и с настройкой по умолчанию всё в порядке. Вторая: паттерн не отличает «next» от «previous», если в разметке есть оба; books.toscrape держит только next, но li.previous a на других сайтах встречается рядом — уточняйте селектор, иначе скрапер будет ходить по кругу. Третья — про фильтры: AllowedDomains в примере не украшение, а страховка. Ссылка «next» — обычная ссылка, и она может увести куда угодно, если каталог ссылается на чужой домен или проверяет вас редиректом. Правила «куда можно ходить» разобраны в [уроке про URL-фильтры](/courses/colly/lessons/18-url-filtry/) — для пагинации 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 и как строить повторы руками — [урок про ошибки и таймауты](/courses/colly/lessons/16-oshibki-retry/); здесь достаточно принять, что 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 — уже разные адреса, и круг замыкается. Общий знаменатель один: заранее неизвестный конец. Поэтому лимит — не опция, а гигиена, как ремень в машине: пристёгиваются не потому, что сегодня авария, а потому что цена забывания несоразмерна.

> **Сочетайте с фильтрами и темпом.** Пагинация умножает количество запросов, поэтому именно здесь AllowedDomains и LimitRule из [урока про задержки](/courses/colly/lessons/10-limitrule/) перестают быть перестраховкой: пятьдесят страниц с паузой — норма, пятьсот без пауз — инцидент. Дедупликация colly и счётчик-лимит закрывают остальные сценарии бегства скрапера по каталогу.

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

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

- Visit вернёт ошибку и программа упадёт
- ✅ Колбэк не сработает, очередь опустеет, обход завершится сам
- colly повторит запрос к текущей странице
- colly зациклится на последней странице

Стоп-условие паттерна — отсутствие ссылки: нет li.next a — нет вызова колбэка, некому ставить следующую страницу в очередь. Visit в main к этому моменту давно вернулся, программа завершается чисто, без ошибок и повторов.

### Проверка: AbsoluteURL

**Вопрос:** Что делает e.Request.AbsoluteURL(e.Attr("href"))?

- Убирает query-параметры из href
- ✅ Разворачивает относительный href в полный URL от текущей страницы
- Проверяет, что ссылка ведёт на разрешённый домен
- Кодирует кириллицу в адресе

href в каталоге обычно относительный: catalogue/page-2.html. AbsoluteURL разрешает его относительно адреса текущей страницы, и Visit получает полный URL. Visit с относительной ссылкой не сработает — http-клиенту нужен абсолютный адрес.

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

```go
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/")
```

**Что выведет программа?**

- 1: только стартовую
- 2: стартовую и одну следующую
- ✅ 3: стартовая плюс два разрешённых перехода
- Все 50: лимит на переходы не влияет

На стартовой странице колбэк срабатывает впервые: count 0 меньше 2, счётчик растёт до 1, ставится page-2. На второй: 1 меньше 2, счётчик до 2, ставится page-3. На третьей: 2 не меньше 2 — return. Итого три страницы. Лимит считает переходы, а не страницы — это видно из кода, и именно здесь его чаще всего неправильно интерпретируют.

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

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

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

Подсказка: Класс-обёртка кнопки next на books.toscrape — li.next; внутренний вызов разворачивает href в полный адрес, внешний ставит страницу в очередь.

Правильные ответы в пропусках: `li.next a`, `Visit`, `AbsoluteURL`

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

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

Правильный порядок строк:

1. `lim := pager{max: 5}`
2. `c := colly.NewCollector()`
3. `c.OnHTML("li.next a", func(e *colly.HTMLElement) {`
4. `  if lim.count >= lim.max { return }`
5. `  lim.count++`
6. `  e.Request.Visit(e.Request.AbsoluteURL(e.Attr("href")))`
7. `})`
8. `c.Visit("http://books.toscrape.com/")`

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

- **Пагинация** — Обход страниц каталога по ссылкам «следующая» или по предсказуемым URL /page=N. Стоп — когда ссылки нет или сработал лимит.
- **e.Request.Visit** — Visit из колбэка: ставит URL в очередь того же коллектора. Так colly ходит по страницам, не возвращаясь в main.
- **e.Request.AbsoluteURL** — Разворачивает относительный href (catalogue/page-2.html) в полный адрес от текущей страницы.
- **Стоп-условие** — Естественное: нет ссылки next — колбэк не срабатывает и очередь пустеет. Искусственное: счётчик-лимит переходов с полем max.
- **Вечная пагинация** — Каталог, где next генерируется бесконечно: календари, смещения, бесконечные ленты. Без лимита страниц обход не закончится никогда.
- **Дедупликация colly** — Повторный Visit того же URL в одном коллекторе игнорируется. Работает на точное совпадение адреса: ?page=2 и /page-2.html — разные адреса.

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

Пагинация — мост к масштабу: пятьдесят страниц по одной скучны, а параллельный обход каталога — тема [урока про Async и WaitGroup](/courses/colly/lessons/11-async-skreping/): поставьте Parallelism, и каталог соберётся в разы быстрее. Ошибки на полпути — 404 посреди диапазона, обрыв связи — живут в [уроке про OnError и повторы](/courses/colly/lessons/16-oshibki-retry/). А правила «куда вообще можно ходить» — [URL-фильтры](/courses/colly/lessons/18-url-filtry/), которые для пагинации почти обязательны.

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

- Паттерн 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» доверять без ограничений.

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

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

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

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

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

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

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