Содержание урока · 11 разделов
- AllowedDomains: периметр в одну опцию
- Ошибка до запроса: где живёт Forbidden domain
- Кейс: каталог и блог на двух поддоменах
- URLFilters: регулярка на каждый URL
- MaxDepth: глубина по вашей линейке
- AllowURLRevisit: один адрес дважды
- Крючки, на которых периметр рвётся
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Утро после ночного запуска: скрапер каталога отработал восемь часов, в журнале — десятки тысяч запросов, в выборке — половина интернета. Товаров, как было триста, так и осталось, зато в логе LinkedIn, Telegram-канал, форум о рыбалке и пара личных блогов: всё это были ссылки из футера, и обходчик им поверил. В colly навигация по ссылкам — одна строка, e.Request.Visit из колбэка; библиотека делает ровно то, что ей велели, и ходит по каждой ссылке без разбору. Правила «куда можно» она сама не придумает — их нужно задать до первого Visit, а не после первого бана. Четыре ограничителя закрывают практически весь спектр задач: AllowedDomains держит хост, URLFilters — пути, MaxDepth — глубину, AllowURLRevisit — повторные адреса. Разница между двумя утрами — в четырёх строках конфигурации, которые пишутся один раз и дальше работают сами.
AllowedDomains: периметр в одну опцию#
colly.AllowedDomains принимает список хостов и превращает его в жёсткое правило: перед каждым запросом colly сверяет хост адреса с этим списком, и если хоста в нём нет, Visit возвращает ошибку Forbidden domain, а запрос не уходит в сеть — ни одного байта на чужой домен не полетит. По умолчанию список пуст, и пустой список означает «разрешено всё» — самый дорогой дефолт в библиотеке. Сравнение точное, по строке: shop.example.com не покрывает ни www.shop.example.com, ни cdn.shop.example.com — все реальные варианты хостов перечисляйте явно. Схема роли не играет: http и https одного хоста для AllowedDomains — один и тот же домен, ограничения по протоколу эта опция не задаёт. И есть второй выключатель, DisallowedDomains: чёрный список, который работает всегда, даже когда AllowedDomains пуст; домен, попавший в оба списка, считается запрещённым — запрет сильнее разрешения. Разрешений обычно два-три, зато они покрывают весь ландшафт: витрину, блог, поддомен со статикой — что не перечислено, для скрапера не существует.
Скрапер с периметром из двух хостов
GOpackage main
import (
"log"
"github.com/gocolly/colly/v2"
)
func main() {
c := colly.NewCollector(
colly.AllowedDomains("shop.example.com", "blog.example.com"),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Request.AbsoluteURL(e.Attr("href"))
if link == "" {
return
}
if err := e.Request.Visit(link); err != nil {
log.Println("не пошёл:", link, "—", err)
}
})
if err := c.Visit("https://shop.example.com/catalog/"); err != nil {
log.Fatal(err)
}
}Оба хоста — витрина и блог — в белом списке, поэтому внутренние переходы между ними работают, а каждая посторонняя ссылка печатает в журнал Forbidden domain и в сеть не уходит. Лог результата Visit здесь не украшение: по нему видно, какие адреса отклонил периметр, и видно же, когда отклонений становится подозрительно много. Проверка AbsoluteURL на пустоту отсекает href без адреса — якоря и пустые ссылки.
Ошибка до запроса: где живёт Forbidden domain#
Все отклонения периметра происходят до сети: проверка срабатывает при Visit, до того как colly соберёт и отправит HTTP-запрос. Отсюда два следствия, которые экономят часы отладки. Первое: ошибку вы получаете как возвращаемое значение Visit, а не через OnError — OnError вызывается на сетевые и HTTP-сбои уже ушедших запросов, а здесь до сети дело просто не дошло; OnRequest тоже молчит. Второе: ошибка таится, если результат Visit из колбэка не проверять — в половине примеров на GitHub пишут вызов без присваивания, и обходчик вежливо игнорирует каждый запрет без единого сообщения в лог. Ошибки навигации однотипны и легко узнаются по тексту, а в пакете colly они вынесены в именованные константы: ErrForbiddenDomain для чужого домена, ErrForbiddenURL для ссылки, отклонённой регуляркой, ErrMaxDepth при превышении глубины, ErrAlreadyVisited при повторном адресе. Логируйте их всегда, а игнорируйте осознанно — сверяя с этими константами, а не со строками. Практичная привычка — один журнал навигации на весь обход: адрес, глубина и результат Visit. Такой журнал превращает отладку периметра из гадания в чтение: видно, где маршрут пошёл не туда, где упёрся в глубину и где чужой домен отклонён честно. Заодно он станет аргументом в разговоре с владельцем сайта, если тот спросит, что это за трафик и по каким адресам вы ходили.
Кейс: каталог и блог на двух поддоменах#
Типовой ландшафт магазина: витрина на shop.example.com, контент-блог на blog.example.com, внутренние ссылки ходят между ними туда и обратно. Периметр задаётся одним списком: оба поддомена в AllowedDomains — и обход свободно гуляет из каталога в статью и назад, собирая и товары, и тексты. Как только список сужается до одного хоста, вторая половина маршрута перестаёт существовать: ссылки на блог начинают возвращать Forbidden domain, и это не сбой, а ровно то, что вы просили. Отсюда рабочий приём отладки: журнал отклонений — карта вашего маршрута. Если в нём масса отклонённых адресов нужного раздела, расширяйте список; если там только соцсети и партнёрские баннеры, периметр подобран верно. И ещё раз про поддомены: shop и blog принадлежат одному владельцу, но для colly это два разных хоста — один не покрывает другой, пишите оба. Тот же список стоит пополнить хостом стейджинга, если витрина ссылается на него из превью: лучше держать обход на тестовом контуре под явным контролем, чем однажды обнаружить полстейджинга в своей выборке.
URLFilters: регулярка на каждый URL#
Домены отвечают на вопрос «куда», но не «по каким путям»: AllowedDomains впустит и каталог, и корзину, и поиск — всё на своём хосте. За пути отвечает второй слой: colly.URLFilters принимает набор регулярных выражений, и посетятся только те адреса, которые совпали хотя бы с одной из них. Регулярка применяется к полному адресу — со схемой и хостом, поэтому шаблон пишут целиком, например со строки https:// и до конца пути. colly.DisallowedURLFilters — зеркало: адрес, совпавший с запретной регуляркой, не посетится вовсе. Когда заданы и тот и другой набор, запрет побеждает разрешение — так удобно вырезать точечный мусор вроде сортировок и UTM-меток изнутри разрешённого раздела, не переписывая весь белый список. Регулярки компилируются один раз при старте, а сверяются с каждым кандидатом в очередь — на тысячах адресов это работает незаметно, поэтому усложнять шаблоны предостережением о производительности не стоит: усложнение оправдано только точностью. Типовой сетап каталога: URLFilters пропускает только пути, начинающиеся с раздела каталога, а DisallowedURLFilters отсекает страницы с параметром сортировки — обход перестаёт гонять один и тот же список в десяти вариантах.
Два слоя фильтров: раздел и мусорные пути
GOc := colly.NewCollector(
colly.AllowedDomains("shop.example.com"),
colly.URLFilters(
regexp.MustCompile(`^https://shop\.example\.com/catalog/.*$`),
),
colly.DisallowedURLFilters(
regexp.MustCompile(`\?sort=`),
),
)Обе регулярки скомпилированы один раз на старте: MustCompile паникует при ошибке в шаблоне, и это правильное поведение для константы, известной заранее. Фильтр сверяется с полным адресом, поэтому в шаблон включены схема и хост. Двухслойная защита: URLFilters пускает только в каталог, DisallowedURLFilters вырезает сортировки; ссылка, попавшая под обе, не посетится — запрет сильнее. Домен при этом дублируем AllowedDomains: фильтры и домены — разные уровни периметра.
Классика жанра: AllowedDomains забыли, скрапер стартовал вечером, утром в журнале — сотни посторонних хостов. Механика простая: на каждой странице есть футер, в футере — соцсети, мессенджеры и партнёрские баннеры; на каждом чужом сайте — свой футер со своими ссылками. Обход растёт как снежный ком, полезных данных ноль, а владелец каждого посещённого сайта получил ваши запросы и повод для бана — по IP, на всю подсеть. Второй крючок мельче, но больнее: сайт внутренними ссылками ходит на www.example.com, а в AllowedDomains записан голый example.com — и обход замирает после первой же ссылки, потому что каждый Visit возвращает Forbidden domain. Лечится одной строкой в списке, но находится часами, потому что выглядит как «скрапер сломался на середине». Перечисляйте оба варианта хоста сразу.
MaxDepth: глубина по вашей линейке#
Механику глубины разбирали в уроке про навигацию по ссылкам, здесь — то, что важно для периметра. colly.MaxDepth(n) ограничивает рекурсию обхода: стартовый Visit имеет глубину 1, каждый вложенный — на единицу глубже, и адрес, чья глубина превысила n, отклоняется с ошибкой Max depth limit reached. Дефолт — ноль, то есть без ограничений. Глубина и доменные фильтры решают разные задачи и не заменяют друг друга: AllowedDomains не даст уйти на чужой хост, но на своём позволит забрести в архив трёхлетней давности; MaxDepth ограничивает вглубь, но на каждом уровне разрешены все пути. Текущую глубину видно в колбэках через e.Request.Depth — удобно логировать её рядом с адресом, чтобы маршрут читался как лестница, а неожиданный скачок глубины сразу бросался в глаза. Пагинация — отдельная ловушка: она идёт цепочкой, и при MaxDepth(2) третья страница каталога не загрузится, хотя домен и путь в полном порядке; для сквозной пагинации нужен стоп-крючок из урока про пагинацию каталога, а не фиксированная глубина. Практический ориентир для каталога: категории и детальные карточки укладываются в глубину 2-3, и этого обычно достаточно — как только маршрут начинает требовать глубины 5+, это повод пересмотреть не цифру, а маршрут.
AllowURLRevisit: один адрес дважды#
По умолчанию повторный Visit того же адреса не отправляет запрос в сеть и возвращает ошибку URL already visited: colly ведёт список посещённых и отклоняет дубли. Это встроенная защита от циклов — без неё две страницы, ссылающиеся друг на друга, закольцевали бы обход насовсем. Обратное включается опцией colly.AllowURLRevisit() или присваиванием полю AllowURLRevisit = true, и нужно это в трёх сценариях: адрес-эндпоинт возвращает разные данные при повторных запросах, страницу надо собрать заново после действия формы, тесты сверяют поведение на одних и тех же URL. Для монотонного обхода каталога дедупликацию не выключают: она экономит и трафик, и чужое терпение, и место в вашем же журнале. Нюанс на стыке: список посещённых живёт внутри одного коллектора и умирает вместе с процессом, поэтому повторные Visit — норма между запусками программы; внутри одного обхода дубли отсекаются сами, и это ровно то поведение, которое хочется по умолчанию.
Крючки, на которых периметр рвётся#
- ссылки из футера и шапки: соцсети, мессенджеры, партнёрские баннеры, счётчики аналитики
- www и голый домен: example.com и www.example.com — два разных хоста для точного сравнения
- поддомены сервисов: img., cdn., static. — картинки и скрипты каталога часто живут отдельно
- http и https в регулярках путей: шаблон с одной схемой отсекает половину адресов
- параметризованные дубли: ?sort=, ?utm_source=, ?from= — один каталог в десятке вариантов
- href без http: mailto:, tel:, javascript: — AbsoluteURL их пропустит, а http-клиент схему не поймёт; фильтруйте по схеме до Visit
- страницы действий: /logout, /delete, /checkout — ссылка на выход из аккаунта в обходчике с cookies дорого обойдётся; режьте их DisallowedURLFilters
Итоговый периметр: все ограничители вместе
GOc := colly.NewCollector(
colly.AllowedDomains("shop.example.com", "blog.example.com"),
colly.URLFilters(
regexp.MustCompile(`^https://shop\.example\.com/catalog/.*$`),
regexp.MustCompile(`^https://blog\.example\.com/posts/.*$`),
),
colly.DisallowedURLFilters(
regexp.MustCompile(`\?sort=`),
),
colly.MaxDepth(2),
)
c.OnHTML("a[href]", func(e *colly.HTMLElement) {
link := e.Request.AbsoluteURL(e.Attr("href"))
if link == "" {
return
}
err := e.Request.Visit(link)
if err != nil && err != colly.ErrForbiddenDomain &&
err != colly.ErrForbiddenURL &&
err != colly.ErrAlreadyVisited {
log.Println("не пошёл:", link, "—", err)
}
})Финальный сетап собирает всё из урока: два домена, два пути-разрешения, запрет сортировок и глубина два. В колбэке отбрасываются известные ошибки навигации — Forbidden domain, ForbiddenURL и уже посещённые адреса — а любая другая печатается в журнал: сетевые сбои и сюрпризы не должны проходить мимо. Сравнение идёт с именованными константами пакета, а не со строками, поэтому опечатка в тексте ошибки не пройдёт мимо проверки.
Colly ходит по сети, которой в браузерном интерпретаторе нет, поэтому примеры урока выполняются локальным Go: go mod init demo, go get github.com/gocolly/colly/v2, go run main.go. Витрина для безопасных экспериментов — books.toscrape.com: учебный каталог, собранный специально под практику скрапинга. Проверьте на нём все ограничители урока: сузьте AllowedDomains до одного хоста и посмотрите в журнале, как обход отказывается идти на второй — это тот же Forbidden domain, только на безобидном примере.
Проверка: белый список хостов
В AllowedDomains указан shop.example.com. Колбэк передаёт в Visit ссылку на пост стороннего сервиса. Что произойдёт?
Проверка: два набора фильтров
Заданы URLFilters, пропускающие только раздел каталога, и DisallowedURLFilters с шаблоном на сортировки. Ссылка на каталог с параметром sort попадает под обе регулярки. Что сделает colly?
Что загрузится при MaxDepth(1)?
Прочитайте код и выберите, что он выведет:
c := colly.NewCollector(
colly.MaxDepth(1),
)
c.OnHTML("a.category", func(e *colly.HTMLElement) {
// ссылки на категории: /catalog/mebel/, /catalog/svet/
e.Request.Visit(e.Attr("href"))
})
// в main:
// err := c.Visit("https://shop.example.com/")Соберите периметр каталога
Восстановите опции коллектора и обработку ссылок в колбэке навигации.
Скелет ограниченного обхода
Расставьте строки программы от объявления пакета до запуска обхода. Проверки ошибок опущены для краткости.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь периметра
Что дальше#
Периметр — техника; за её границей остаются правила игры: что сайт просит не собирать и где colly останавливается перед капчами и JS-рендерингом. Об этом следующий урок — про защиту от скрапинга и честные пределы Colly: там robots.txt, лимиты по IP и альтернативы для сайтов, которые не отдают HTML без браузера. А если нужна карта всего маршрута — от первого Visit до итогового проекта с монитором цен — она на странице курса Colly. Фильтры из этого урока лягут в основу каждого следующего скрапера курса, поэтому стоит дойти до домашнего задания руками: четыре строки конфигурации экономят ночи, а иногда и репутацию вашего IP.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- AllowedDomains — белый список хостов: пустой список означает «разрешено всё», чужой хост — ошибку Forbidden domain до отправки запроса.
- Сравнение хостов точное: www и поддомены перечисляются явно; DisallowedDomains — чёрный список, он сильнее белого.
- URLFilters и DisallowedURLFilters — регулярки на полный адрес; при конфликте запрет побеждает разрешение.
- MaxDepth ограничивает глубину: старт — 1, вложенный Visit — плюс один; дефолт 0 — без ограничений; текущая глубина — в e.Request.Depth.
- Повторный Visit без AllowURLRevisit возвращает URL already visited — встроенная защита от циклов.
- Все отклонения возвращаются из Visit до сети: OnError их не видит, сверяйтесь с константами ErrForbiddenDomain и ErrForbiddenURL.
- Журнал отклонённых ссылок — диагностика периметра: подозрительно много Forbidden domain — проверяйте футерные ссылки и www-вариант хоста.
Домашнее задание
Возьмите обходчик из урока про навигацию и закройте периметр: AllowedDomains на два хоста — витрина и блог, URLFilters только на раздел каталога, MaxDepth(2). Запустите на books.toscrape.com и по журналу Visit посчитайте отклонённые ссылки. Затем уберите из списка www-вариант хоста, если он встретился, и объясните по логу, на чём именно встал обход.
Личные заметкиПусто
Вопросы и ответы по уроку
Что вернёт colly при Visit на домен вне AllowedDomains?
Ошибку Forbidden domain — как обычное значение error из Visit. Запрос не уходит в сеть, OnHTML и OnError не срабатывают. В колбэке навигации ошибку проверяют и логируют: молчаливое игнорирование прячет и нормальные отклонения, и поломку маршрута.
Как разрешить поддомены вроде cdn.shop.example.com?
Точным перечислением: AllowedDomains сравнивает хосты по строке, и одна запись shop.example.com не покрывает ни cdn-, ни www-вариант. Перечислите все нужные хосты списком; широкого механизма «все поддомены» в библиотеке нет — глоб-шаблоны живут в LimitRule и к доменному периметру отношения не имеют. Альтернатива — URLFilters с регуляркой, где поддоменная часть шаблона шире.
Обход остановился на второй странице пагинации при MaxDepth(2) — это баг?
Нет: пагинация — цепочка переходов, каждая следующая страница на единицу глубже предыдущей. Фиксированная глубина для неё не подходит — ставьте стоп-условие по отсутствию ссылки «следующая» или по номеру страницы, как в уроке про пагинацию.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Сохраняем результат: JSON и CSV
- База данных: складируем спарсенное в SQLite
- Когда есть API: JSON вместо HTML
- Защита от скрапинга: честные пределы Colly
- Кэш и офлайн-режим: не дёргаем сайт дважды
- Ошибки и повторы: OnError и таймауты
- Прокси в Colly: ротация и пул адресов
- User-Agent и заголовки: когда сайт капризничает
- Все курсы по Go
Следующая ступень учебного пути
Testify: тестирование Go с assert и require
Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- Писать читаемые проверки через assert и require, а не лестницы if
- Проверять ошибки, паники и краевые случаи без ручного кода
- Сравнивать структуры, слайсы, карты и числа с плавающей точкой
- Строить табличные тесты и сьюты с хуками подготовки