Парсинг данных на PHP с CURL через прокси: обходим блокировки cloudflare правильно

Парсинг данных на PHP с CURL через прокси: обходим блокировки cloudflare правильно

Написали парсер, запустили, работает. Через час бан по IP. Знакомо? Сайты научились определять и блокировать автоматические запросы, а простой скрипт на CURL без подготовки живёт недолго. При этом задачи парсинга никуда не делись: мониторинг цен, сбор данных, анализ конкурентов.

Для стабильного парсинга на PHP понадобится библиотека CURL для запросов, пул прокси-серверов (подобрать подходящий сервис можно на toproxylab.com где собраны рейтинги провайдеров под разные задачи), и немного хитростей с заголовками и задержками. Разберём как всё это собрать в рабочий код.

Базовый запрос через CURL с прокси

Начнём с простого. Вот минимальный код для запроса через прокси:


$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://example.com/page');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_PROXY, '192.168.1.1:8080');

$response = curl_exec($ch);
curl_close($ch);

echo $response;

Работает? Работает. Но недолго. Проблема в том, что запрос выглядит как запрос бота. Нет заголовков, нет cookies, подозрительно чистый.

Добавляем реалистичные заголовки:


$ch = curl_init();

$headers = [
    'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
    'Accept-Encoding: gzip, deflate, br',
    'Connection: keep-alive',
];

curl_setopt($ch, CURLOPT_URL, 'https://example.com/page');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_PROXY, '192.168.1.1:8080');
curl_setopt($ch, CURLOPT_ENCODING, ''); // автоматическая распаковка gzip

$response = curl_exec($ch);
curl_close($ch);

Уже лучше. Теперь запрос похож на запрос браузера.

Авторизация и типы прокси

Бесплатные прокси обычно без авторизации. Платные почти всегда требуют логин и пароль. В CURL это делается так:

curl_setopt($ch, CURLOPT_PROXY, '192.168.1.1:8080');
curl_setopt($ch, CURLOPT_PROXYUSERPWD, 'username:password');

Типы прокси тоже важны. HTTP прокси работает по умолчанию. Для SOCKS нужно указать тип явно:

// SOCKS5 прокси
curl_setopt($ch, CURLOPT_PROXY, '192.168.1.1:1080');
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);

// SOCKS5 с DNS через прокси (рекомендуется)
curl_setopt($ch, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5_HOSTNAME);

Разница между SOCKS5 и SOCKS5_HOSTNAME в том, где резолвится DNS. Во втором случае DNS запросы тоже идут через прокси, что скрывает вашу активность от провайдера.

Подробнее о параметрах CURL можно почитать в официальной документации PHP https://www.php.net/manual/ru/function.curl-setopt.php.

Ротация прокси и User-Agent

Один прокси, один User-Agent, это путь к бану. Нужна ротация.

Вот класс для работы с пулом прокси:


class ProxyRotator
{
    private $proxies = [];
    private $userAgents = [];
    private $currentIndex = 0;
    
    public function __construct(array $proxies, array $userAgents)
    {
        $this->proxies = $proxies;
        $this->userAgents = $userAgents;
    }
    
    public function getProxy()
    {
        $proxy = $this->proxies[$this->currentIndex];
        $this->currentIndex = ($this->currentIndex + 1) % count($this->proxies);
        return $proxy;
    }
    
    public function getRandomUserAgent()
    {
        return $this->userAgents[array_rand($this->userAgents)];
    }
}

// Использование
$proxies = [
    '192.168.1.1:8080',
    '192.168.1.2:8080',
    '192.168.1.3:8080',
];

$userAgents = [
    'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15',
    'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36',
];

$rotator = new ProxyRotator($proxies, $userAgents);

Теперь каждый запрос будет с новым прокси и случайным User-Agent:


function fetchPage($url, $rotator)
{
    $ch = curl_init();
    
    $headers = [
        'User-Agent: ' . $rotator->getRandomUserAgent(),
        'Accept: text/html,application/xhtml+xml',
        'Accept-Language: ru-RU,ru;q=0.9',
    ];
    
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_PROXY, $rotator->getProxy());
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    
    curl_close($ch);
    
    return ['code' => $httpCode, 'body' => $response];
}

Обработка ошибок и повторные попытки

Прокси падают. Сайты отвечают ошибками. Соединения обрываются. Нужна обработка:


function fetchWithRetry($url, $rotator, $maxRetries = 3)
{
    $attempt = 0;
    
    while ($attempt < $maxRetries) {
        $ch = curl_init();
        
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_PROXY, $rotator->getProxy());
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        
        curl_close($ch);
        
        // Успешный ответ
        if ($httpCode >= 200 && $httpCode < 300) {
            return $response;
        }
        
        // Ошибка прокси или таймаут
        if ($error || $httpCode == 407 || $httpCode == 0) {
            $attempt++;
            sleep(1);
            continue;
        }
        
        // Бан или блокировка
        if ($httpCode == 403 || $httpCode == 429) {
            $attempt++;
            sleep(rand(5, 15)); // Увеличенная пауза
            continue;
        }
        
        break;
    }
    
    return false;
}

Код 429 (Too Many Requests) означает что сайт заметил подозрительную активность. Нужно замедлиться.

Задержки между запросами

Машинный трафик легко отличить по скорости. Человек не делает 100 запросов в секунду. Добавляем случайные паузы:


function humanDelay($min = 1, $max = 5)
{
    $delay = rand($min * 1000, $max * 1000);
    usleep($delay * 1000); // микросекунды
}

// Между запросами
foreach ($urls as $url) {
    $result = fetchWithRetry($url, $rotator);
    processResult($result);
    humanDelay(2, 7); // пауза 2-7 секунд
}

Для агрессивного парсинга паузы можно уменьшить, но тогда нужно больше прокси в ротации. Баланс между скоростью и выживаемостью каждый находит сам.

Работа с cookies и сессиями

Некоторые сайты требуют cookies. Без них даже не покажут контент. CURL умеет сохранять и отправлять cookies:


$cookieFile = '/tmp/cookies_' . md5($proxyAddress) . '.txt';

curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile);  // сохранять
curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile); // отправлять

Важно: для каждого прокси лучше использовать отдельный файл cookies. Иначе сессия одного прокси уйдёт на другой, что выглядит подозрительно.

Обход Cloudflare и других защит

Cloudflare, DataDome, PerimeterX, эти системы анализируют поведение и блокируют ботов. Простой CURL их не пройдёт.

Варианты решения:

  1. Первый: использовать браузерные решения типа https://pptr.dev или Selenium. Они рендерят JavaScript и проходят проверки. Но это уже не чистый PHP.
  2. Второй: специализированные прокси-сервисы с встроенным обходом защит. Они сами решают капчи и проходят проверки.
  3. Третий: для простых случаев иногда помогает правильный набор заголовков и cookies. Но это работает не всегда и не со всеми защитами.

Парсинг на PHP через CURL это рабочий инструмент когда всё настроено правильно. Прокси скрывают IP, ротация распределяет нагрузку, заголовки маскируют бота под браузер, задержки имитируют человека.

Главные правила: не жадничать со скоростью, иметь запас прокси, обрабатывать ошибки и следить за кодами ответов. 403 и 429 это сигналы замедлиться, а не повод долбить сервер ещё сильнее.

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