Разбирать HTML ради данных — работа на крайний случай. Разметка меняется от редизайна, классы генерируются сборщиком, а подгрузка идёт через JavaScript. Если сайт отдаёт данные своему фронтенду, почти всегда есть JSON-эндпоинт, и работать с ним проще.

Когда API лучше HTML

JSON-ответ стабильнее: поля называются осмысленно, типы сохраняются, пагинация описана в самом ответе. Вам не нужен парсер DOM, не ломают кавычки и переносы, а объём ответа обычно меньше страницы целиком.

Ограничения тоже есть. API может требовать авторизацию, подпись запроса или токен, который живёт минуты. Внутренние эндпоинты не документированы и меняются без предупреждения. И юридическая сторона никуда не исчезает: условия использования и права на базу данных действуют независимо от формата.

Как найти эндпоинт

Откройте сайт в браузере, включите DevTools и перейдите на вкладку Network с фильтром Fetch/XHR. Пролистайте страницу, нажмите «показать ещё» — появится запрос. Посмотрите URL, метод, заголовки и тело ответа.

Часто рядом лежит параметр page, offset или cursor. Меняя его вручную, вы поймёте схему пагинации. Если запрос уходит с заголовком Authorization, эндпоинт закрыт, и обходить это не стоит.

Не копируйте cookie из браузера в скрипт: сессия истечёт, а использование чужой сессии может нарушать правила сервиса. Для публичных данных достаточно обычного клиента.

Запрос с правильными заголовками

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_TIMEOUT => 15,
    CURLOPT_HTTPHEADER => [
        'Accept: application/json',
        'User-Agent: my-parser/1.0 (+https://example.ru/about)',
    ],
]);

$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

Заголовок Accept честно говорит, что вам нужен JSON. User-Agent с контактом помогает администратору связаться с вами вместо блокировки по IP. Таймаут обязателен: без него зависший запрос остановит весь цикл.

Пагинация: offset, page и cursor

Три схемы встречаются чаще всего:

  • offset/limit — ?offset=100&limit=50; просто, но на больших сдвигах медленно;
  • page — ?page=3; номер страницы, размер задан сервером;
  • cursor — ?cursor=abc123; в ответе есть next_cursor или has_more.

Курсорная пагинация надёжнее: данные не съезжают, если между запросами добавили запись. С offset возможны дубли и пропуски. Всегда ориентируйтесь на признак из ответа, а не на «пока массив не пустой»: пустая страница может означать и конец, и ошибку фильтра.

Не забудьте ограничить число итераций. Если сервер зациклит курсор, скрипт будет работать вечно. Жёсткий лимит страниц и лог прогресса спасают от этого.

Подробный разбор цикла и защиты от дублей — в статье парсинг пагинации на PHP.

Лимиты и ответ 429

Код 429 Too Many Requests означает превышение частоты. В ответе часто есть Retry-After — число секунд или дата, когда можно повторить. Правильная реакция — подождать указанное время, а не долбить сервер снова.

Даже без 429 стоит держать паузу между запросами: 200–500 миллисекунд для небольшого сайта. Экспоненциальная задержка с случайным разбросом помогает, когда несколько воркеров работают параллельно и мешают друг другу.

Не пытайтесь обойти лимит сменой IP или прокси. Это уже не вежливый парсинг, а обход защиты; последствия — блокировка и, возможно, претензии. Если данных много, спросите у владельца сервиса про официальный доступ.

Проверка ответа и разбор JSON

if ($status !== 200) {
    throw new RuntimeException("HTTP {$status}: " . substr($body, 0, 200));
}

try {
    $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
    throw new RuntimeException('Некорректный JSON: ' . $e->getMessage(), 0, $e);
}

if (! isset($data['items']) || ! is_array($data['items'])) {
    throw new RuntimeException('Неожиданная структура ответа');
}

Проверяйте HTTP-код до разбора: сервер может вернуть HTML-страницу ошибки с кодом 200, и json_decode() упадёт с невнятным сообщением. JSON_THROW_ON_ERROR избавляет от проверки json_last_error() после каждого вызова.

Не доверяйте типам: число может прийти строкой, null — вместо массива, а поле исчезнуть в новой версии. Явные проверки структуры дешевле, чем отладка через неделю.

Кэш и условные запросы

Если API отдаёт ETag или Last-Modified, сохраняйте их и отправляйте обратно в If-None-Match и If-Modified-Since. Ответ 304 Not Modified означает, что данные не изменились, и тело читать не нужно. Это снижает и нагрузку, и риск упереться в лимит.

Локальный кэш полезен и без заголовков: сохраняйте ответы на диск и обновляйте по расписанию. Для разовых задач достаточно обычного файла, для регулярных — база или Redis.

Правовая сторона

Перед сбором данных прочитайте условия использования и robots.txt. Публичная доступность страницы не означает разрешение на массовую выгрузку. Персональные данные регулируются отдельно, а база данных может охраняться как объект смежных прав. Правила и ограничения разобраны отдельно: robots.txt и этичный парсинг.

Если данные нужны для бизнеса, надёжнее договориться об официальном API. Это дешевле, чем судебные риски и постоянная борьба с блокировками.

Коротко

  • ищите эндпоинт во вкладке Network, а не в исходном коде страницы;
  • ставьте Accept: application/json и честный User-Agent;
  • курсорная пагинация надёжнее offset;
  • на 429 ждите Retry-After, а не меняйте IP;
  • проверяйте код ответа и структуру до разбора.