Содержание урока · 10 разделов
- Пагинация по ссылке «следующая»
- Генерация номеров страниц: когда URL предсказуем
- Ошибки посреди диапазона: дыры в данных
- Счётчик-ограничитель: страховка, которая всегда должна быть
- Три подхода к пагинации
- Вечный каталог: как это выглядит изнутри
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
В предыдущих уроках скрапер обрабатывал одну страницу — максимум список адресов, выписанный руками. Каталог ломает этот комфорт: пятьдесят страниц, у каждой ссылка «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
GOpackage 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 вернёт ошибку, цикл обрывается. Плата за простоту — молчаливые пропуски. Если шаг нумерации иной — скажем, страницы идут десятками или адреса построены от идентификаторов, — цикл будет стучаться в пустоту или пропустит середину, и вы узнаете об этом по дырявым данным, а не по сообщению об ошибке.
Цикл по номерам страниц
GOpackage 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
GOtype 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 из урока про задержки перестают быть перестраховкой: пятьдесят страниц с паузой — норма, пятьсот без пауз — инцидент. Дедупликация colly и счётчик-лимит закрывают остальные сценарии бегства скрапера по каталогу.
Проверка: естественное стоп-условие
Обходчик ходит по 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 и постановка страницы в очередь.
Соберите обход с лимитом
От объявления счётчика до стартовой страницы.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: пагинация
Что дальше по курсу#
Пагинация — мост к масштабу: пятьдесят страниц по одной скучны, а параллельный обход каталога — тема урока про 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» доверять без ограничений.
Личные заметкиПусто
Вопросы и ответы по уроку
Как обойти пагинацию, у которой нет ссылки «следующая»?
Если URL предсказуем — идти циклом по номерам или параметрам, обрывая обход по ошибке Visit. Если адреса строятся из данных страницы — смещения, идентификаторы, — вынимайте их отдельным OnHTML и ставьте в очередь тем же e.Request.Visit. Лимит страниц стоит в любом варианте.
Почему скрапер ходит по кругу и собирает одни и те же страницы?
Либо ссылка «next» указывает на текущую страницу — баг каталога, либо рядом с next есть previous, и селектор ловит обе ссылки. Уточните селектор, включите лог URL в OnRequest и держите счётчик-лимит: он и есть предохранитель от круга.
Насколько большим можно ставить лимит страниц?
Достаточно большим, чтобы покрыть каталог с запасом, и достаточно малым, чтобы вечный цикл не съел ночь прогона. Практика: ожидаемое число страниц плюс небольшой запас, а превышение — отдельная запись в логе, а не тихий return.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Формы и вход: POST, cookies, сессии
- User-Agent и заголовки: когда сайт капризничает
- Прокси в Colly: ротация и пул адресов
- Ошибки и повторы: OnError и таймауты
- Параллельный скрапинг: Async и WaitGroup
- LimitRule: задержки и параллелизм
- OnRequest и OnResponse: запросы под контролем
- Навигация по сайту: обход ссылок и глубина
- Все курсы по Go
Следующая ступень учебного пути
Testify: тестирование Go с assert и require
Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- Писать читаемые проверки через assert и require, а не лестницы if
- Проверять ошибки, паники и краевые случаи без ручного кода
- Сравнивать структуры, слайсы, карты и числа с плавающей точкой
- Строить табличные тесты и сьюты с хуками подготовки