Avatar ·

PHP фатальная ошибка: исчерпан лимит памяти 134217728 байт (128 МБ)

Всем привет! Я начинающий PHP-разработчик, и при выполнении скрипта на локальном сервере (OpenServer) постоянно вылетает ошибка: ``` Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 20480 bytes) in D:\projects\parser\index.php on line 45 ``` У меня простой парсер новостей с другого сайта — я собираю заголовки и ссылки через Simple HTML DOM Parser. Скрипт обрабатывает около 5000 страниц. Вот пример моего кода: ```php find('h2.news-title a') as $link) { $title = $link->plaintext; $href = $link->href; echo "$title - $href\n"; } // Я думал, что очищаю память $html->clear(); unset($html); } echo "Готово!"; ?> ``` Проблема возникает примерно на 200-й итерации. Я пробовал увеличить memory_limit в php.ini до 512M, но это не помогло — ошибка просто появляется позже. В чём дело? Как правильно освобождать память? Или нужно как-то иначе организовать код? Заранее спасибо за помощь! <<>>

Корень проблемы: утечка памяти в библиотеке Simple HTML DOM Parser

Вы столкнулись с классической проблемой при работе с парсерами. Даже после вызова clear() и unset(), библиотека Simple HTML DOM Parser не полностью освобождает память. Она создаёт множество внутренних объектов (узлы DOM), которые остаются в памяти из-за циклических ссылок. Сборщик мусора PHP (GC) не успевает их обработать при интенсивном цикле.

Основные решения

1. Принудительный вызов сборщика мусора

Самый простой способ — вручную запускать сборщик мусора после каждой итерации:

foreach ($urls as $url) {
    $html = file_get_html(trim($url));
    
    foreach ($html->find('h2.news-title a') as $link) {
        $title = $link->plaintext;
        $href = $link->href;
        echo "$title - $href\n";
    }
    
    $html->clear();
    unset($html);
    
    // Принудительно запускаем сборщик мусора
    if (gc_enabled()) {
        gc_collect_cycles();
    }
}

2. Использование cURL вместо Simple HTML DOM

Более надёжный подход — отказаться от этой библиотеки и использовать комбинацию cURL + регулярные выражения или DOMDocument:

foreach ($urls as $url) {
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, trim($url));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    $html = curl_exec($ch);
    curl_close($ch);
    
    // Парсим через DOMDocument
    $dom = new DOMDocument();
    @$dom->loadHTML($html);
    $xpath = new DOMXPath($dom);
    
    $nodes = $xpath->query("//h2[contains(@class, 'news-title')]/a");
    foreach ($nodes as $node) {
        $title = $node->nodeValue;
        $href = $node->getAttribute('href');
        echo "$title - $href\n";
    }
    
    // Освобождаем ресурсы
    unset($dom, $xpath, $html);
}

3. Пакетная обработка с лимитом

Если нужно обработать 5000 страниц, разбейте их на пакеты по 100-200 штук и используйте отдельный процесс для каждого пакета:

$batchSize = 100;
$batches = array_chunk($urls, $batchSize);

foreach ($batches as $batchIndex => $batch) {
    echo "Обработка пакета " . ($batchIndex + 1) . "\n";
    
    foreach ($batch as $url) {
        // Ваша логика парсинга
        processUrl(trim($url));
    }
    
    // После каждого пакета - принудительная очистка
    if (function_exists('memory_get_peak_usage')) {
        $memUsage = memory_get_peak_usage(true) / 1024 / 1024;
        echo "Пиковое использование памяти: " . round($memUsage, 2) . " МБ\n";
    }
    
    // Сброс всех статических переменных и кэша
    gc_collect_cycles();
}

function processUrl($url) {
    // Используйте DOMDocument или cURL внутри функции
    // После выхода из функции все локальные переменные уничтожаются
}

Дополнительные рекомендации

  • Увеличьте лимит только как временное решение. В php.ini или через ini_set('memory_limit', '512M'); — но это лишь отсрочит проблему.
  • Используйте memory_get_usage() для профилирования: вставляйте вызов после каждой итерации, чтобы увидеть, как растёт потребление.
  • Рассмотрите асинхронные подходы: библиотеки типа Guzzle с параллельными запросами (на основе cURL мульти-обработчика) могут быть эффективнее.
  • Если Simple HTML DOM критичен, оберните каждый парсинг в отдельный shell-скрипт и вызывайте через exec() — тогда память освобождается полностью после завершения процесса.

В вашем случае рекомендую начать с варианта №2 (cURL + DOMDocument) — он не только решит проблему памяти, но и будет работать в 3-5 раз быстрее за счёт отсутствия накладных расходов на парсер.

<<>>

Альтернативный подход: использование генераторов (yield)

Если вы хотите оставить Simple HTML DOM Parser, но избавиться от утечки памяти, можно переписать скрипт с использованием генераторов. Это позволит обрабатывать URL-адреса лениво, не загружая все данные в память сразу:

function getUrls($filename) {
    $file = fopen($filename, 'r');
    while (($line = fgets($file)) !== false) {
        yield trim($line);
    }
    fclose($file);
}

foreach (getUrls('urls.txt') as $url) {
    // Ограничиваем время выполнения для каждого URL
    $html = @file_get_contents($url, false, stream_context_create([
        'http' => ['timeout' => 5]
    ]));
    
    if ($html === false) {
        continue; // Пропускаем недоступные URL
    }
    
    // Используем встроенный парсер вместо Simple HTML DOM
    preg_match_all('/<h2 class="news-title"><a href="([^"]+)">([^<]+)<\/a><\/h2>/', $html, $matches);
    
    foreach ($matches[1] as $i => $href) {
        echo $matches[2][$i] . ' - ' . $href . "\n";
    }
    
    unset($html);
    flush(); // Сбрасываем буфер вывода
}

Этот подход решает проблему за счёт того, что:

  • Файл читается построчно, а не целиком
  • Регулярные выражения работают быстрее и потребляют меньше памяти, чем DOM-парсер
  • Каждая итерация полностью очищает память после себя

Если же вам обязательно нужен DOM-парсинг, рассмотрите библиотеку DiDOM — она легче и не имеет проблем с утечками памяти, характерными для Simple HTML DOM Parser.