Содержание урока · 11 разделов
- colly onrequest: последняя точка перед отправкой
- colly onresponse: тело уже скачано
- Порядок вызовов: конвейер от Visit до OnScraped
- OnResponseHeaders: решение до скачивания тела
- Куда уходят 4xx, 5xx и редиректы
- Abort: отсекаем мусор до отправки
- Кто что видит на каждом этапе
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
До сих пор наш скрапер был чёрным ящиком: c.Visit со стартовой страницы — и через мгновение OnHTML уже перебирает карточки товаров. Между этими двумя строками colly успевает построить HTTP-запрос, отправить его, получить заголовки, скачать тело и отдать его парсеру — и весь этот конвейер остаётся невидимым, пока вы сами не подпишетесь на его этапы. Невидимость дорого стоит. Когда сайт отвечает 403, вы узнаёте об этом по пустому выводу, а не по логу. Когда каталог редиректит на страницу входа, скрапер честно «работает»: разбирает страницу логина, ничего там не находит и молчит — вы тратите вечер на перечитывание селекторов, хотя проблема случилась на два шага раньше. А когда обход тянет картинки и архивы, вы платите трафиком и временем, потому что отменять неучтённые запросы негде. В этом уроке вешаем датчики на каждый этап конвейера: увидим запрос до отправки, ответ после загрузки, точку между заголовками и телом — и научимся отсекать мусор до того, как он уйдёт в сеть.
colly onrequest: последняя точка перед отправкой#
Регистрируется обработчик методом OnRequest с сигнатурой func(r colly.Request), и вызывается colly в последний момент перед уходом запроса в сеть — когда адрес уже разобран, глубина посчитана, очередь дала добро. В руках у вас весь запрос целиком. r.URL — это url.URL с хостом, путём и query-параметрами: от проверки префикса пути до разбора расширения файла доступно без строковых фокусов. r.Method говорит, что предстоит — GET или POST. r.Depth — та самая глубина обхода из восьмого урока, по которой colly отсекает рекурсию; в журнале она подсказывает, откуда пришёл запрос. У объекта есть и номер: r.ID уникален в пределах коллектора, и по нему удобно склеивать запрос с ответом в общем журнале. Самое ценное — r.Headers: это указатель на реальные заголовки запроса, и всё, что здесь выставить методом Set, отправится на сервер как есть. Именно в OnRequest живут User-Agent, Referer и Accept-Language — механике и границам этих манипуляций посвящён отдельный урок про заголовки и User-Agent. И последнее: OnRequest — последняя точка, где запрос ещё можно отменить, об этом ниже.
Журнал запроса и ответа
GOc.OnRequest(func(r *colly.Request) {
log.Println("->", r.Method, r.URL, "глубина", r.Depth)
})
c.OnResponse(func(r *colly.Response) {
log.Printf("<- %d %s, %d байт", r.StatusCode, r.Request.URL, len(r.Body))
})OnRequest печатает метод и адрес до отправки, OnResponse — статус, итоговый адрес и размер тела после загрузки. r.Request.URL в OnResponse — адрес последнего запроса цепочки: после редиректа он отличается от стартового. Две стрелки в логе превращают невидимый конвейер в читаемый диалог — любая пауза, пустой вывод или подозрительный код теперь имеют адрес и причину.
colly onresponse: тело уже скачано#
OnResponse с сигнатурой func(r *colly.Response) вызывается, когда тело ответа целиком лежит в памяти. Что внутри: r.StatusCode — число, а не строка; r.Body — срез байт, тот самый HTML или JSON; r.Headers — заголовки ответа, из них r.Headers.Get("Content-Type") достаёт тип содержимого; r.Request — связанный объект запроса с адресом и глубиной. Обратите внимание: отдельного поля ContentType у Response нет — тип берётся из заголовков, и запомнить это стоит заранее, чтобы не искать несуществующее поле по советам старых статей. Тело — сырые байты, и это удобно: string(r.Body) позволяет поискать подстроку по сырой разметке ещё до селекторов, len(r.Body) — посчитать размер, а сравнение размеров между прогонами — заметить, что сайт начал отдавать заглушку вместо каталога. На этот колбэк приходят и не-HTML ответы: JSON-эндпоинты, файлы, заглушки — всё, что вернул сервер с успешным кодом. Тело можно отправить в свой парсер или сохранить на диск методом r.Save("page.html") — быстрый снапшот страницы для отладки селекторов без браузера и curl. Имя файла, если сервер передал его в Content-Disposition, подскажет r.FileName().
Порядок вызовов: конвейер от Visit до OnScraped#
Колбэки colly срабатывают в строгом порядке, и он одинаков для каждой страницы, сколько бы обработчиков вы ни зарегистрировали. Знать конвейер наизусть полезно по двум причинам: сразу видно, на каком этапе пропала строка лога, и ясно, где ставить проверку — раньше значит дешевле. Выглядит конвейер так: OnRequest, затем сеть, затем OnResponseHeaders, OnResponse, все OnHTML по порядку регистрации — и финальный OnScraped, который замыкает страницу целиком. Разберём шаги по одному.
- OnRequest — запрос собран и скоро уйдёт; здесь живут заголовки, журнал и Abort.
- Сеть — HTTP-клиент отправляет запрос; редиректы транспорт догоняет сам, colly видит финальную страницу.
- OnResponseHeaders — получены статус и заголовки ответа, тело ещё не читалось.
- OnResponse — тело скачано целиком и лежит в r.Body как срез байт.
- OnHTML (и OnXML) — DOM разобран, селекторы отрабатывают в порядке регистрации обработчиков.
- OnScraped — страница обработана целиком; удобная финальная точка для сводки по ней.
OnResponseHeaders: решение до скачивания тела#
Между заголовками и телом есть отдельный колбэк — OnResponseHeaders(func(r *colly.Response)). Тело в этот момент ещё не скачивалось: доступен r.StatusCode, доступны r.Headers, а r.Body пуст. Это идеальное место для решений, которым хватает заголовков. Видали Content-Type text/html — отдаём страницу парсеру; увидели application/json — это эндпоинт, и данные достаются без разбора DOM; заметили image/ — можно сразу отказаться от ответа. То же с размером из заголовков: огромный файл виден в очереди до того, как съест трафик. Тонкость, о которой молчат туториалы: OnResponseHeaders срабатывает у всех дошедших ответов, включая 403 и 404 — заголовки-то уже приехали, и вы успеете залогировать код раньше, чем colly решит судьбу ответа. Abort работает и здесь: вызванный в этом колбэке, он обрывает докачку тела, а в OnError приходит ошибка ErrAbortedAfterHeaders — так экономят трафик на тяжёлых ответах, которые заведомо не нужны. Разница с OnResponse только в моменте, но момент дорогой: после заголовков тело ещё можно не качать, после — уже поздно.
Фильтр по типу содержимого
GOc.OnResponseHeaders(func(r *colly.Response) {
ct := r.Headers.Get("Content-Type")
if strings.HasPrefix(ct, "image/") {
r.Request.Abort() // тело картинки не докачиваем
return
}
log.Println("тип:", ct, "статус:", r.StatusCode)
})Content-Type приезжает в заголовках, поэтому решение принимается до чтения тела. Отмена делается через r.Request.Abort(): проверка в колбэке вернёт false, библиотека закроет тело не читая, а в OnError прилетит ErrAbortedAfterHeaders — это штатный способ отказаться от ответа, а не поломка, и пугаться такой строки в журнале не нужно.
Куда уходят 4xx, 5xx и редиректы#
Успешные ответы проходят конвейер целиком и заканчиваются в OnHTML и OnScraped. Всё остальное colly проводит по другой ветке: ответы с кодами ошибок — 403, 404, 500 — попадают в OnError, и OnResponse с OnHTML их не видят; подробно эту ветку и повтор запросов мы разбирали в уроке про OnError и повторы. Разделение логичное: страница с 404 обычно не несёт данных, и гонять её в DOM — только тратить память, а вот журнал ошибок обязан знать о каждом провале. Редиректы — отдельная история: перенаправления с заголовком Location HTTP-транспорт проходит сам, colly даже не узнаёт о промежуточных адресах — в OnResponse прилетает уже финальная страница с кодом 200, а r.Request.URL показывает её итоговый адрес. Поэтому «проверить статус 301 в OnResponse» не получится: до вас дойдёт только конец цепочки, и это к лучшему — бегать за редиректами вручную не нужно. Тело ответа с ошибкой по умолчанию не разбирается вовсе — опция ParseHTTPErrorResponse меняет это поведение, если страницы ошибок несут данные.
Самая частая потеря новичка: обработку ошибок пишут в OnResponse, потому что «ответ же пришёл». Ответ с кодом 404 или 500 для colly — ошибка: OnResponse не вызовется, OnHTML не увидит ни одного элемента, и всё замолчит, если OnError не зарегистрирован. Проверка if r.StatusCode >= 400 внутри OnResponse не сработает никогда — без ParseHTTPErrorResponse до неё дело просто не доходит.
Abort: отсекаем мусор до отправки#
В восьмом уроке мы фильтровали ссылки до Visit — это санитария очереди. Но запрос может оказаться мусорным и позже: пагинация ведёт на страницы-приложения в PDF, каталог раздаёт картинки прямыми ссылками, а вы по ошибке собрали их в очередь. Для таких случаев в OnRequest есть рубильник r.Abort(): запрос, помеченный aborted, в сеть не уходит вообще. Причём это не ошибка — OnError не вызовется, Visit завершится тихо, очередь продолжит работу со следующим адресом. Связка получается двухуровневой: фильтр ссылок держит очередь чистой на этапе планирования, Abort — последняя проверка на этапе исполнения, когда URL уже поставлен в очередь и до сети остался один шаг. Разницу видно в журнале: провал попадает в OnError, а отмена — тишина, поэтому отмены стоит логировать прямо в OnRequest, печатая адрес перед Abort-ом. Мой рабочий минимум: Abort на расширения статики и архивные форматы, которые точно не несут данных, — трафик экономится заметнее всего именно здесь, потому что отменённый запрос не отправляется вовсе.
Отмена статики в OnRequest
GOc.OnRequest(func(r *colly.Request) {
for _, ext := range []string{".jpg", ".png", ".gif", ".pdf", ".zip"} {
if strings.HasSuffix(r.URL.Path, ext) {
r.Abort()
return
}
}
log.Println("грузим:", r.URL)
})Путь запроса достаётся из r.URL.Path — это разобранный *url.URL, без схемы и хоста. Суффикс проверяется по списку расширений, и совпавший запрос помечается Abort-ом прямо в колбэке. Отменённые адреса не попадают ни в OnResponse, ни в OnError — в журнале их видно только по отсутствию парной стрелки: request ушёл, response не пришёл.
Кто что видит на каждом этапе#
| Этап | Что доступно | Типичное применение |
|---|---|---|
| OnRequest | r.URL, r.Headers, r.Depth, r.Abort | журнал, заголовки запроса, отсечение статики |
| OnResponseHeaders | StatusCode и заголовки ответа; Body ещё пуст | фильтр по Content-Type, ранний лог |
| OnResponse | StatusCode, Body, Headers, Request, Save | разбор JSON, снапшоты, подсчёт байт |
| OnHTML | DOM-элементы, e.Request | извлечение данных селекторами |
| OnError | Response без тела, err | журнал провалов, очередь повторов |
| OnScraped | тот же Response | сводка по обработанной странице |
Проверка: судьба 404
Сайт ответил 404. Какие колбэки вызовутся и в каком порядке?
Проверка: Abort
В OnRequest для всех адресов с суффиксом .pdf вызван r.Abort(). Что увидит программа?
Что напечатает программа при 500?
Прочитайте код и выберите, что он выведет:
c.OnRequest(func(r *colly.Request) {
fmt.Println("->", r.URL)
})
c.OnResponse(func(r *colly.Response) {
fmt.Println("<-", r.StatusCode)
})
c.OnError(func(r *colly.Response, err error) {
fmt.Println("!", r.StatusCode, err)
})
c.Visit("http://books.toscrape.com/broken")Соберите пару запрос-ответ
Восстановите регистрацию колбэков, отмену статики и сохранение страницы.
Соберите конвейер по строкам
Расставьте строки в порядке конвейера: от запроса до ответа, ошибки — рядом с ответом.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: запрос и ответ
Что дальше#
Конвейер виден, каждый запрос подписан — остался вопрос темпа: обходчик ставит запросы в очередь так быстро, как успевает сеть, и без ограничителей это подозрительно похоже на DDoS. Успокоим его в следующем уроке про LimitRule: там появятся Delay, RandomDelay и лимит одновременных запросов на домен. Примеры этого урока, как и весь сетевой код курса, запускаются локальным Go — в браузерной песочнице сети нет. Соберите журнал запроса и ответа, прогоните его по books.toscrape.com и полистайте терминал: каталог из пары страниц прочитывается как диалог, и каждая стрелка в нём — вызов одного из колбэков этого урока. Полный маршрут по курсу — на странице коли-курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- OnRequest вызывается перед отправкой: отсюда видны r.URL, r.Method и r.Depth, здесь выставляются заголовки и работает Abort.
- Порядок конвейера: OnRequest → сеть → OnResponseHeaders → OnResponse → OnHTML → OnScraped.
- OnResponseHeaders видит статус и Content-Type до чтения тела — место для решений, которым HTML не нужен.
- OnResponse получает StatusCode и r.Body []byte, но только у успешных ответов; 4xx и 5xx уходят в OnError.
- Abort в OnRequest отменяет запрос до сети: трафик не тратится, OnError не вызывается, очередь идёт дальше.
- Редиректы http-транспорт проходит сам: в OnResponse приходит финальная страница, r.Request.URL показывает её адрес.
- r.Save(fileName) пишет тело на диск — быстрый снапшот страницы для отладки селекторов.
Домашнее задание
Добавьте в обходчик из восьмого урока пару OnRequest и OnResponse с журналом: адрес, метод, статус, размер тела. Отсеките Abort-ом картинки и архивы в OnRequest, а в OnResponseHeaders — ответы с типом image/. Запустите локально на books.toscrape.com и посчитайте по логу, сколько запросов сэкономил Abort.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему OnResponse не срабатывает на странице с кодом 404?
Ответ с ошибкой colly проводит по ветке OnError: OnResponse и OnHTML видят только успешные ответы. Заголовки при этом доходят — OnResponseHeaders успевает отработать. Тело страницы ошибки можно получить опцией ParseHTTPErrorResponse, но селекторам там обычно ловить нечего.
Чем OnResponseHeaders отличается от OnResponse?
Моментом вызова и наполнением: OnResponseHeaders срабатывает сразу после заголовков, когда Body ещё пуст — видно только StatusCode и Headers. OnResponse вызывается после скачивания тела. Если решение зависит только от заголовков, не ждите тело — фильтруйте или отменяйте раньше.
Можно ли в OnRequest узнать, куда именно уйдёт запрос?
Да: r.URL — разобранный *url.URL с хостом, путём и query-параметрами, r.Method — метод, r.Depth — глубина обхода. Заголовки меняются прямо через r.Headers.Set. Объект тот же, что и e.Request в OnHTML, поэтому AbsoluteURL и Visit доступны и отсюда.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- LimitRule: задержки и параллелизм
- Параллельный скрапинг: Async и WaitGroup
- Пагинация: обходим все страницы каталога
- Формы и вход: POST, cookies, сессии
- Навигация по сайту: обход ссылок и глубина
- Структуры для спарсенного: модели и слайсы
- Извлечение данных: Text, Attr, ChildText
- OnHTML: колбэки, порядок вызовов и цепочки
- Все курсы по Go
Следующая ступень учебного пути
Testify: тестирование Go с assert и require
Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- Писать читаемые проверки через assert и require, а не лестницы if
- Проверять ошибки, паники и краевые случаи без ручного кода
- Сравнивать структуры, слайсы, карты и числа с плавающей точкой
- Строить табличные тесты и сьюты с хуками подготовки