rzaripov.kz

Создание простого веб-скрейпера на PHP с cURL и DOMDocument

Сбор данных с веб-страниц — распространённая задача в разработке серверных приложений. PHP как язык, ориентированный на веб, предоставляет всё необходимое для её решения прямо из коробки, без подключения тяжёлых фреймворков. Сочетание расширения cURL и встроенного модуля DOMDocument позволяет построить надёжный парсер буквально за пару сотен строк кода.

Идея связки cURL и DOM не нова: первая библиотека отвечает за получение удалённого HTML, вторая превращает строку в древовидную структуру, по которой удобно перемещаться с помощью XPath. Подобный подход хорошо ложится и на другие платформенные задачи — например, когда одни и те же приёмы обработки XML применяются в мобильной и серверной части проекта. Кросс-платформенная разработка, где важно сохранить единую логику между Android, iOS и бэкендом, делает понимание базовых механизмов парсинга особенно ценным.

В этом материале разберём пошаговый процесс создания скрейпера: от настройки окружения и отправки первого запроса до извлечения структурированных данных и обхода типичных ограничений. Все примеры будут самодостаточными и запускаемыми на типичном shared-хостинге или локальной машине с PHP 8.x.

Прежде чем писать код, важно помнить о правовой и этической стороне. Скрейпинг публичных страниц обычно допустим, но владельцы сайтов могут ограничивать скорость запросов через файл robots.txt, требовать заголовок User-Agent или вовсе блокировать подозрительных клиентов. Хороший парсер всегда уважает эти ограничения, выдерживает паузы между запросами и корректно обрабатывает сетевые ошибки.

Инструменты и настройка рабочего окружения

PHP изначально включает всё необходимое для создания скрейпера. Расширение cURL поставляется в большинстве дистрибутивов и активируется одной строкой в php.ini, а классы DOMDocument, DOMXPath и DOMNode доступны без дополнительных установок. Проверить готовность окружения можно простым скриптом:

<?php
var_dump(extension_loaded('curl'));
var_dump(class_exists('DOMDocument'));

Если оба вывода возвращают true, можно переходить к коду. Ниже — сравнение популярных подходов к парсингу HTML в экосистеме PHP, которое помогает понять место связки cURL и DOMDocument среди альтернатив.

Инструмент HTTP-запросы Парсинг HTML XPath Внешние зависимости
cURL + DOMDocument встроенный cURL DOMDocument да нет
file_get_contents + SimpleXML streams SimpleXML нет нет
Guzzle + Symfony DomCrawler Guzzle 7+ DomCrawler да да
PHPQuery внешняя собственный движок частично да

Связка cURL + DOMDocument — самый лёгкий вариант: ничего не нужно ставить через Composer, всё работает даже на старых версиях PHP 7.4. Если проект уже использует Composer, Guzzle и DomCrawler дают более удобный API, но требуют дополнительной памяти и времени на инициализацию.

Выполнение HTTP-запросов через cURL

Расширение cURL — де-факто стандарт для HTTP в PHP. Оно поддерживает актуальные протоколы, позволяет тонко настраивать заголовки, прокси, таймауты и автоматически следовать редиректам. Минимальный рабочий запрос выглядит так:

$ch = curl_init('https://example.com/articles');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_USERAGENT => 'Mozilla/5.0 (compatible; MyScraper/1.0)',
    CURLOPT_TIMEOUT => 15,
]);
$html = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($code !== 200) {
    throw new RuntimeException("Сервер вернул код $code");
}

Опция CURLOPT_USERAGENT критична: многие сайты отдают пустую страницу или 403 ошибку клиентам без заголовка User-Agent. Таймаут в 10–15 секунд защищает от зависания, а CURLOPT_FOLLOWLOCATION избавляет от необходимости вручную обрабатывать цепочки редиректов. После получения ответа стоит проверить не только HTTP-код, но и наличие ожидаемых подстрок в теле — это помогает отличить реальный ответ от капчи или служебной страницы ошибки.

Парсинг HTML при помощи DOMDocument и XPath

DOMDocument принимает на вход строку с HTML и возвращает дерево узлов. Чтобы избежать проблем с кодировкой и предупреждений о malformed-разметке, разумно отключить строгий режим:

$dom = new DOMDocument();
libxml_use_internal_errors(true);
$dom->loadHTML('<?xml encoding="UTF-8">' . $html);
libxml_clear_errors();
$xpath = new DOMXPath($dom);

Добавление XML-декларации с указанием кодировки важно, когда сайт отдаёт текст в UTF-8, иначе DOMDocument может интерпретировать кириллицу некорректно. После загрузки DOMXPath открывает доступ к мощному языку запросов. Например, чтобы собрать все заголовки статей внутри тега <article>:

$titles = $xpath->query('//article//h2');
foreach ($titles as $title) {
    echo trim($title->textContent), "\n";
}

XPath-запрос //article//h2 означает «найти все h2 на любом уровне вложенности внутри элементов article». Такой синтаксис позволяет обходить даже сложную разметку, где CSS-селекторы оказались бы слишком громоздкими.

Извлечение данных и сохранение результатов

Чистый текст из узла — только начало. Реальные проекты требуют дополнительной обработки: удаления HTML-сущностей, обрезки пробелов, приведения дат к единому формату, разбора цен из строк вида «1 299 ₽». Удобно вынести такую логику в набор функций-хелперов:

function cleanText(?string $value): string {
    return trim(html_entity_decode(strip_tags((string)$value), ENT_QUOTES, 'UTF-8'));
}

function parsePrice(string $raw): float {
    $digits = preg_replace('/[^0-9,.]/', '', $raw);
    return (float)str_replace(',', '.', $digits);
}

Результаты стоит сохранять в структурированном виде. Для разовых задач подойдёт запись в JSON-файл — PHP содержит встроенные функции json_encode и file_put_contents. Для регулярного сбора данных лучше подключить SQLite через PDO: база создаётся одной командой, а запросы к ней выполняются быстрее, чем повторный парсинг JSON при каждом запуске.

Обход ограничений, пагинация и обработка ошибок

Любой серьёзный скрейпер должен уметь ходить по страницам каталога, где товары или статьи распределены по 20–50 элементов на каждом URL вида ?page=N. Алгоритм прост: в цикле увеличиваем номер страницы, пока запрос возвращает непустой результат. Между итерациями обязательна пауза:

foreach (range(1, $maxPages) as $page) {
    $url = sprintf('%s?page=%d', $baseUrl, $page);
    $html = fetch($url);
    if (empty($html) || !str_contains($html, 'Следующая')) {
        break;
    }
    parsePage($html);
    sleep(rand(2, 5));
}

Случайная задержка в несколько секунд снижает нагрузку на чужой сервер и уменьшает вероятность блокировки по IP. Если сайт активно сопротивляется автоматическим клиентам, помогают ротация прокси из открытых списков и подмена User-Agent на популярные браузерные строки. Важно обрабатывать все исключения на каждом шаге: одна битая страница не должна прерывать сбор всего каталога.

Оптимизация, кеширование и расширение скрейпера

Когда прототип работает, наступает время оптимизаций. Самая эффективная — кеширование уже полученных страниц на диске. Проверка по хешу URL позволяет пропускать повторные запросы и значительно ускоряет повторные запуски. Параллельное выполнение через curl_multi_init сокращает время обхода большого каталога в разы, но требует аккуратной синхронизации записи в общий файл или базу.

Наработанные функции парсинга удобно вынести в отдельный класс или набор небольших компонентов, которые затем переиспользовать в смежных проектах. Кросс-платформенная разработка опирается на похожие приёмы: при сборке из единого кода приходится иметь дело с разной обработкой одной логики в зависимости от платформы. Универсальные методы вроде XPath, нормализации строк и аккуратной обработки ошибок остаются неизменными, что делает опыт веб-скрейпинга полезным далеко за пределами PHP-бэкенда.