Разбирать 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; - проверяйте код ответа и структуру до разбора.