Содержание урока · 11 разделов
- Где прячется JSON-эндпоинт: devtools и вкладка Network
- API вместо HTML: что выигрываем и чем платим
- Типизация структуры под JSON-ответ
- Сессия и заголовки для приватного эндпоинта
- Один коллектор, два формата
- Живое место: приватный API без контракта
- Чек-лист: когда API вместо HTML оправдан
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Задача классическая: собрать названия и цены из каталога. Путь, которым идут чаще всего, — OnHTML по классам: отладили селекторы, получили таблицу, радуемся — до следующей перевёрстки. Между тем витрина, с которой вы собираете данные, рисует страницу не из воздуха: при загрузке она делает XHR-запрос к внутреннему эндпоинту и получает те же товары аккуратным JSON-ом. Откройте devtools раньше, чем писать код, — и вместо хрупких селекторов у вас типизированные структуры, а вместо парсинга разметки — чтение готового ответа. Не для каждого сайта такой трюк сработает, но проверять его стоит первым делом: это дешевле любого обхода DOM. Ниже — механика через colly: где искать эндпоинт, как забрать тело ответа, как типизировать данные и когда всё-таки вернуться к HTML.
Где прячется JSON-эндпоинт: devtools и вкладка Network#
Механика поиска одинакова во всех браузерах. Откройте инструменты разработчика, вкладку Network, фильтр XHR или Fetch и перезагрузите страницу. Сначала сыплются скрипты и стили — они не нужны; ищите запросы, чей ответ в превью раскрывается в дерево с осмысленными ключами: title, price, items, products. Обычно это GET с параметрами вида page=2 или format=json, реже POST с телом-фильтром. Нашли кандидата — скопируйте адрес и вставьте в новую вкладку: если JSON открывается без авторизации, colly получит его тем же GET-ом; если сервер требует cookies или особые заголовки, повторите их через OnRequest — механику заголовков разбирали в уроке про User-Agent и заголовки, а вход по сессии — в уроке про формы и cookies. Один нюанс: эндпоинты живут не только на странице каталога. Пагинация, живой поиск, ленивая подгрузка — у каждого своя пара XHR-запросов, и часто они чище основного списка. И последнее: если запрос в списке POST с телом-фильтром, это не приговор — colly умеет отправлять формы через c.Post, а тело повторяется тем же набором полей, что вы видели в devtools на вкладке Payload.
API вместо HTML: что выигрываем и чем платим#
Выигрыш у подхода «api вместо html» тройной, и он не про вкус. Стабильность: вёрстку магазина правят заметно чаще, чем формат внутреннего JSON — классы и вложенность div-ов живут месяцами под угрозой редизайна, а схема данных меняется только вместе с самими данными. Объём: JSON без меню, футеров и счётчиков аналитики весит в разы меньше страницы, и colly не гоняет его через движок селекторов — Unmarshal быстрый и предсказуемый. Точность: в JSON значения уже разделены — цена числом, ссылка отдельным полем, остаток флагом, тогда как в разметке всё склеено в текст узлов и вынимается с оговорками про пробелы и валюту. Платите зависимостью от формата, о котором ничего не гарантировано: это внутренний API для собственной витрины, а не публичный контракт, и его ненадёжность заслуживает отдельного разбора — ниже. Для учебных витрин, собранных специально под скрапинг, вопрос снимается сам: там HTML и есть самый стабильный слой.
Одно предупреждение перед кодом: примеры урока требуют локального Go — браузерная песочница сеть не исполняет, ей нечем сделать даже Visit до localhost. Поставьте модуль командой go get github.com/gocolly/colly/v2, сохраните пример в main.go и запускайте go run: пять минут настройки, дальше всё живёт в вашем терминале.
OnResponse: r.Body — это []byte
GOpackage main
import (
"encoding/json"
"log"
"github.com/gocolly/colly/v2"
)
type Item struct {
Title string `json:"title"`
Price int `json:"price"`
URL string `json:"url"`
}
func main() {
var items []Item
c := colly.NewCollector()
c.OnResponse(func(r *colly.Response) {
if err := json.Unmarshal(r.Body, &items); err != nil {
log.Printf("в ответе не JSON: %v", err)
return
}
log.Printf("получено товаров: %d", len(items))
})
if err := c.Visit("https://shop.example.com/api/catalog?format=json"); err != nil {
log.Fatal(err)
}
}Весь механизм — в двух строках внутри OnResponse. r.Body — тело ответа как []byte, именно то, что json.Unmarshal ждёт вторым аргументом после адреса приёмника; &items — указатель на слайс, куда лягут распакованные элементы. Теги json:"title" сопоставляют ключи ответа с полями структуры; регистр при совпадении без тегов не важен, но явные теги надёжнее. OnResponse срабатывает на каждый ответ — включая HTML, если вы зайдёте на обычную страницу: защита от этого ниже. Ошибку Unmarshal проверяем и логируем, а не молчим: битый ответ — норма жизни скрапера.
Типизация структуры под JSON-ответ#
Структуру выводят из ответа, а не из фантазии: откройте превью в devtools и пройдитесь сверху вниз. Верхний уровень — массив? Тогда []Item. Объект с ключами items, total, next_page? Заведите обёртку — type Catalog struct с полем Items []Item json:"items" и остальными, потому что Unmarshal кладёт данные строго по иерархии. Вложенные объекты становятся вложенными структурами, массивы — срезами. Два сюрприза встречаются постоянно. Первый: числа приходят строками — "price":"1499.00". Поле Price int с таким ответом не соберётся: Unmarshal пропустит значение, дочитает остальное и вернёт UnmarshalTypeError — держите строку и разбирайте strconv.ParseFloat-ом, как в уроке про JSON и CSV-вывод. Второй: поле то появляется, то нет — незаполненное поле останется нулевым значением, и ошибки не будет. Несовпадение типов ловится ошибкой, отсутствие поля — нулём: разные режимы отказа, разные проверки.
Content-Type: проверка перед Unmarshal
GOc.OnResponse(func(r *colly.Response) {
ct := r.Headers.Get("Content-Type")
if !strings.Contains(ct, "application/json") {
return // обычная страница, её разберёт OnHTML
}
var page Catalog
if err := json.Unmarshal(r.Body, &page); err != nil {
log.Printf("битый JSON: %v", err)
return
}
items = append(items, page.Items...)
})r.Headers.Get("Content-Type") возвращает значение заголовка или пустую строку, если заголовка нет. Заголовок обычно выглядит как application/json; charset=utf-8, поэтому сравнивают через strings.Contains, а не на равенство — иначе упрётесь в charset. Проверка стоит копейки и отсекает целый класс багов: страница ошибки, редирект на логин и капча приходят HTML-ом, и без фильтра вы получите загадочную ошибку Unmarshal вместо внятной картины происходящего.
Сессия и заголовки для приватного эндпоинта#
Часть приватных эндпоинтов требует больше, чем один GET: cookies сессии, Referer, иногда заголовок с токеном, который страница получает при загрузке. Всё это повторяется штатными средствами: SetCookies раскладывает cookies, OnRequest добавляет заголовки, механику разбирал урок про формы и вход. Практический порядок такой: скопируйте из devtools cURL-команду запроса (правая кнопка на запросе — Copy as cURL), посмотрите, что там с заголовками, и воспроизведите только необходимое. Обычно хватает пары штук — и не надо перебрасывать весь заголовочный свит браузера, это и бесполезно, и заметно.
Один коллектор, два формата#
Colly ничего не знает о форматах: OnHTML и OnResponse живут на одном коллекторе и срабатывают по своим событиям независимо. Практическая схема гибридного обхода выглядит так: OnHTML собирает ссылки на карточки и пагинацию с HTML-страниц, OnResponse ловит JSON-эндпоинты и раздаёт данные структурам; проверка Content-Type не даёт обработчикам путаться в чужих ответах. Ничего не мешает и другому делению — два коллектора, один под витрину, второй под API, у каждого свои лимиты и таймауты. Механику обоих обработчиков по слоям разбирали в уроке про OnRequest и OnResponse: там же про то, в каком порядке колбеки видят запрос и ответ. Данные из HTML-ветки вынимаются приёмами из урока про Text, Attr и ChildText. Структуры для обеих веток общие — те же Product и Catalog из урока про модели данных: разделение форматов не должно удваивать модель.
Проверка: тело ответа
Что лежит в r.Body внутри OnResponse?
Проверка: HTML вместо JSON
В r.Body оказалась HTML-страница, вы вызвали json.Unmarshal. Что будет?
Число в строковое поле
Прочитайте код и выберите, что он выведет:
type Item struct {
Price string `json:"price"`
}
// сервер вернул: {"price": 1499}
var items []Item
err := json.Unmarshal(r.Body, &items)Живое место: приватный API без контракта#
Моё рабочее правило — начинать с эндпоинта: быстрее, тише, меньше трафика. Но у приватного API нет контракта: формат меняется без предупреждения, письма о deprecated никто не рассылает, и однажды поле price тихо превращается в объект pricing с валютой и суммой — между двумя вашими запусками, без чьей-либо вины. У HTML хотя бы витрина живёт для людей: сломанную страницу магазина чинят быстро, потому что на ней не продают; внутренний эндпоинт, нужный только старому коду витрины, могут месяцами держать в состоянии «работает — не трогай», а потом выключить одним коммитом. Отсюда граница применимости без религии: что стабильнее на конкретном сайте, то и берём. Проверяется наблюдением, а не вкусом — посмотрите, как часто меняется вёрстка против того, как часто меняется JSON, подержите оба варианта пару недель и остановитесь на том, что не падает. И какой бы слой вы ни выбрали, проверку ошибок не ослабляйте: её ещё ни разу не жалели.
Чек-лист: когда API вместо HTML оправдан#
- в Network виден XHR с JSON, повторяющий данные страницы
- ответ открывается в браузере без авторизации — или вы умеете приносить cookies
- схема ответа не менялась месяцами, а вёрстка меняется каждую зиму
- данных много: JSON экономит и трафик, и время разбора
- нужны точные значения, а не текст из разметки с пробелами и валютой
- есть план Б: OnHTML-селекторы на случай, если эндпоинт закроют
Соберите обработчик ответа
Восстановите фрагмент: подписка на событие ответа, проверка заголовка и распаковка тела.
Скелет JSON-скрапера
Расставьте строки программы в правильном порядке — от объявления пакета до запуска обхода.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: словарь урока
Что дальше#
Данные собраны в структуры — дальше им дорога на диск: сериализация в JSON-файл и CSV-отчёт разбирались в уроке про сохранение результата. А если JSON-эндпоинт вы так и не нашли, а вместе с ним молчат и селекторы — следующий урок про защиту от скрапинга и честные пределы colly: там robots.txt, капчи и JS-рендеринг, то есть случаи, когда HTML не приходит вообще или приходит пустым.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Витрина почти всегда тянет данные из приватного JSON-эндпоинта — ищите его в devtools на вкладке Network с фильтром XHR.
- OnResponse даёт тело ответа как r.Body ([]byte); json.Unmarshal раскладывает его в структуры по тегам json.
- Структуру выводят из реального ответа: массив — срез, вложенный объект — вложенная структура, числа-строки разбирают отдельно.
- Content-Type проверяют через r.Headers.Get и strings.Contains, чтобы не скормить Unmarshal HTML-страницу ошибки.
- OnHTML и OnResponse живут на одном коллекторе: HTML-ветка — ссылки и пагинация, JSON-ветка — данные.
- Приватный API не даёт контракта: формат меняется без предупреждения. Что стабильнее на конкретном сайте — то и берём.
Домашнее задание
Найдите на любом магазине или каталоге XHR-запрос, который отдаёт список товаров: devtools, Network, фильтр XHR, перезагрузка. Напишите скрапер с OnResponse и json.Unmarshal: структура по реальному ответу, проверка Content-Type, лог количества собранных элементов. Затем сознательно скормите обработчику HTML-страницу — убедитесь, что ошибка ловится и скрапер не падает.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем парсинг JSON API лучше скрапинга HTML?
JSON меньше и стабильнее: без меню, рекламы и вложенных div-ов, со значениями, уже разделёнными по полям. Разбор быстрее, селекторы не нужны. Минус — отсутствие контракта: приватный формат меняется без предупреждения, поэтому наблюдайте за обоими слоями и берите тот, что стабильнее на вашем сайте.
Как понять, что у сайта есть JSON-эндпоинт?
Откройте devtools, вкладку Network, включите фильтр XHR и перезагрузите страницу: запросы с JSON-ответом видны сразу, их удобно проверять в превью. Скопируйте URL и вставьте в новую вкладку — если JSON открывается без авторизации, colly получит его тем же GET-ом.
Что будет, если скормить json.Unmarshal HTML?
Ошибка синтаксиса вида invalid character '<': Unmarshal смотрит на первый символ и отказывается дальше. Ваша обязанность — проверить err и, лучше, отфильтровать ответы заранее по Content-Type через r.Headers.Get.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Защита от скрапинга: честные пределы Colly
- Тестирование скрапера: httptest и фикстуры
- CLI-скрапер: связка Cobra и Colly
- Итоговый проект: монитор цен с алертами
- База данных: складируем спарсенное в SQLite
- Сохраняем результат: JSON и CSV
- URL-фильтры: AllowedDomains, URLFilters, Depth
- Кэш и офлайн-режим: не дёргаем сайт дважды
- Все курсы по Go
Следующая ступень учебного пути
Testify: тестирование Go с assert и require
Интерактивный курс Testify для Go: assert и require, табличные тесты, suite, mock-объекты, Eventually, HTTP-тесты. 25 уроков с примерами и практикой в браузере.
- Писать читаемые проверки через assert и require, а не лестницы if
- Проверять ошибки, паники и краевые случаи без ручного кода
- Сравнивать структуры, слайсы, карты и числа с плавающей точкой
- Строить табличные тесты и сьюты с хуками подготовки