---
title: "Когда есть API: JSON вместо HTML"
description: "Парсинг JSON API на Go: находим XHR-эндпоинт в devtools, читаем r.Body через OnResponse и json.Unmarshal и решаем, когда API вместо HTML стабильнее DOM."
course: "Colly: парсинг и веб-скрапинг на Go"
url: "https://golangmaster.ru/courses/colly/lessons/21-api-json-endpointy/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Когда есть API: JSON вместо HTML

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

Парсинг JSON API — приём, о котором часто забывают: витрина магазина или каталога почти всегда сама ходит на приватный JSON-эндпоинт, и тянуть данные из HTML нет смысла. Открыли devtools, нашли XHR-запрос, повторили его через colly — и вместо хрупких селекторов получили типизированные структуры. Разберём, как найти такой эндпоинт через вкладку Network, как забрать тело ответа через OnResponse и json.Unmarshal, чем API вместо HTML стабильнее DOM — и где у этого подхода граница.

Задача классическая: собрать названия и цены из каталога. Путь, которым идут чаще всего, — 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 и заголовки](/courses/colly/lessons/14-zagolovki-useragent/), а вход по сессии — в уроке про [формы и cookies](/courses/colly/lessons/13-formy-login/). Один нюанс: эндпоинты живут не только на странице каталога. Пагинация, живой поиск, ленивая подгрузка — у каждого своя пара 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

```go
package 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-вывод](/courses/colly/lessons/19-json-csv-vyvod/). Второй: поле то появляется, то нет — незаполненное поле останется нулевым значением, и ошибки не будет. Несовпадение типов ловится ошибкой, отсутствие поля — нулём: разные режимы отказа, разные проверки.

### Content-Type: проверка перед Unmarshal

```go
c.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 добавляет заголовки, механику разбирал урок про [формы и вход](/courses/colly/lessons/13-formy-login/). Практический порядок такой: скопируйте из devtools cURL-команду запроса (правая кнопка на запросе — Copy as cURL), посмотрите, что там с заголовками, и воспроизведите только необходимое. Обычно хватает пары штук — и не надо перебрасывать весь заголовочный свит браузера, это и бесполезно, и заметно.

### Один коллектор, два формата

Colly ничего не знает о форматах: OnHTML и OnResponse живут на одном коллекторе и срабатывают по своим событиям независимо. Практическая схема гибридного обхода выглядит так: OnHTML собирает ссылки на карточки и пагинацию с HTML-страниц, OnResponse ловит JSON-эндпоинты и раздаёт данные структурам; проверка Content-Type не даёт обработчикам путаться в чужих ответах. Ничего не мешает и другому делению — два коллектора, один под витрину, второй под API, у каждого свои лимиты и таймауты. Механику обоих обработчиков по слоям разбирали в уроке про [OnRequest и OnResponse](/courses/colly/lessons/09-onrequest-onresponse/): там же про то, в каком порядке колбеки видят запрос и ответ. Данные из HTML-ветки вынимаются приёмами из урока про [Text, Attr и ChildText](/courses/colly/lessons/06-izvlechenie-dannyh/). Структуры для обеих веток общие — те же Product и Catalog из урока про [модели данных](/courses/colly/lessons/07-struktury-dannyh/): разделение форматов не должно удваивать модель.

### Проверка: тело ответа

**Вопрос:** Что лежит в r.Body внутри OnResponse?

- Строка с отрендеренным браузером HTML
- ✅ Тело ответа как срез байтов — []byte
- Указатель на http.Request
- Путь к кэш-файлу colly

r.Body — сырые байты ответа: JSON, HTML или что угодно ещё. Именно поэтому json.Unmarshal принимает его напрямую, без конвертаций, а за определение формата отвечаете вы — например, через Content-Type.

### Проверка: HTML вместо JSON

**Вопрос:** В r.Body оказалась HTML-страница, вы вызвали json.Unmarshal. Что будет?

- Слайс останется пустым, ошибки не будет
- ✅ Вернётся ошибка вида invalid character '<': тело не начало JSON
- Паника: Unmarshal не проверяет вход
- Colly сама пропустит обработчик

Unmarshal сначала смотрит на первый значимый символ; открывающая угловая скобка сразу даёт ошибку синтаксиса. Слайс при этом остаётся пустым — потому проверка ошибки обязательна, иначе соберёте ноль данных и узнаете об этом по отчёту.

### Число в строковое поле

```go
type Item struct {
        Price string `json:"price"`
}

// сервер вернул: {"price": 1499}
var items []Item
err := json.Unmarshal(r.Body, &items)
```

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

- err == nil, у первого товара Price станет "1499"
- err != nil: число нельзя положить в строку без тега
- ✅ err != nil, но Unmarshal дочитает остальное: UnmarshalTypeError, поле Price останется пустым
- Паника при обращении к items[0]

Несовпадение типов — не фатал: значение пропускается, декодирование продолжается, а наверх уходит UnmarshalTypeError с описанием, где именно промахнулись. Поле остаётся нулевым — пустой строкой. Отсюда правило: проверять err, даже когда часть данных, кажется, распаковалась.

### Живое место: приватный API без контракта

Моё рабочее правило — начинать с эндпоинта: быстрее, тише, меньше трафика. Но у приватного API нет контракта: формат меняется без предупреждения, письма о deprecated никто не рассылает, и однажды поле price тихо превращается в объект pricing с валютой и суммой — между двумя вашими запусками, без чьей-либо вины. У HTML хотя бы витрина живёт для людей: сломанную страницу магазина чинят быстро, потому что на ней не продают; внутренний эндпоинт, нужный только старому коду витрины, могут месяцами держать в состоянии «работает — не трогай», а потом выключить одним коммитом. Отсюда граница применимости без религии: что стабильнее на конкретном сайте, то и берём. Проверяется наблюдением, а не вкусом — посмотрите, как часто меняется вёрстка против того, как часто меняется JSON, подержите оба варианта пару недель и остановитесь на том, что не падает. И какой бы слой вы ни выбрали, проверку ошибок не ослабляйте: её ещё ни разу не жалели.

### Чек-лист: когда API вместо HTML оправдан

- в Network виден XHR с JSON, повторяющий данные страницы
- ответ открывается в браузере без авторизации — или вы умеете приносить cookies
- схема ответа не менялась месяцами, а вёрстка меняется каждую зиму
- данных много: JSON экономит и трафик, и время разбора
- нужны точные значения, а не текст из разметки с пробелами и валютой
- есть план Б: OnHTML-селекторы на случай, если эндпоинт закроют

### Соберите обработчик ответа

Восстановите фрагмент: подписка на событие ответа, проверка заголовка и распаковка тела.

```go
c.On___(func(r *colly.Response) {
        ct := r.___.Get("Content-Type")
        if !strings.Contains(ct, "application/json") {
                return
        }

        var page Catalog
        if err := json.___(r.___, &page); err != nil {
                log.Println(err)
                return
        }
})
```

Подсказка: Событие называется OnResponse; заголовки живут в r.Headers; распаковка — json.Unmarshal; тело — r.Body.

Правильные ответы в пропусках: `Response`, `Headers`, `Unmarshal`, `Body`

### Скелет JSON-скрапера

Расставьте строки программы в правильном порядке — от объявления пакета до запуска обхода.

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

1. `package main`
2. `import (`
3. `  "encoding/json"`
4. `  "github.com/gocolly/colly/v2"`
5. `)`
6. `var items []Item`
7. `c := colly.NewCollector()`
8. `c.OnResponse(func(r *colly.Response) { json.Unmarshal(r.Body, &items) })`
9. `c.Visit("https://shop.example.com/api/catalog")`

### Карточки: словарь урока

- **XHR-запрос** — Фоновый запрос страницы к серверу за данными (вкладка Network, фильтр XHR). Часто возвращает JSON, из которого витрина рисует содержимое.
- **r.Body** — Тело ответа в OnResponse как []byte. Для JSON-эндпоинта отдаётся напрямую в json.Unmarshal.
- **json.Unmarshal** — Функция encoding/json: раскладывает []byte с JSON в структуры по тегам json и совпадению имён полей. Ошибку синтаксиса и несовпадение типов возвращает как error.
- **Content-Type** — Заголовок ответа с типом содержимого; для JSON — application/json. Проверяется через r.Headers.Get и strings.Contains перед Unmarshal.
- **Приватный API** — Внутренний эндпоинт сайта для его собственной витрины. Контракта нет: формат может измениться без предупреждения.
- **OnHTML vs OnResponse** — OnHTML реагирует на разметку и селекторы, OnResponse — на сырой ответ. На одном коллекторе живут вместе и не мешают друг другу.

### Что дальше

Данные собраны в структуры — дальше им дорога на диск: сериализация в JSON-файл и CSV-отчёт разбирались в уроке про [сохранение результата](/courses/colly/lessons/19-json-csv-vyvod/). А если JSON-эндпоинт вы так и не нашли, а вместе с ним молчат и селекторы — следующий урок про [защиту от скрапинга и честные пределы colly](/courses/colly/lessons/22-antibot-zaschita/): там robots.txt, капчи и JS-рендеринг, то есть случаи, когда HTML не приходит вообще или приходит пустым.

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

- Витрина почти всегда тянет данные из приватного 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.
