Создание простого веб-скрейпера на 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-бэкенда.