PHP фатальная ошибка: исчерпан лимит памяти 134217728 байт (128 МБ)
Корень проблемы: утечка памяти в библиотеке 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.