Пятница, 23:00. В офисе темно, только мой монитор отбрасывает синеватый свет. На столе — кружка остывшего чая и телефон с сообщением от клиента: «Слушай, у нас тут каталог захламлён. Половина позиций — мёртвый груз. Надо удалить всё, что есть в файле remsku.txt. Там примерно 37 тысяч артикулов. Сделаешь к понедельнику?»
Я открыл админку «1С-Битрикс», представил, как буду искать каждый артикул руками и нажимать «Удалить» 37 000 раз... Честно? Мне стало немного грустно. А потом я сел и за 15 минут написал скрипт, который решил задачу за в течение часа. Сегодня я расскажу, как он устроен, почему он не падает, и что чуть не пошло не так.
Клиент держал интернет-магазин электроники на «Битриксе». За три года номенклатура распухла до 80 тысяч позиций (плюсом не корректный обмен с 1с). Беда в том, что примерно половина — дубли, тестовые товары, позиции, снятые с производства. Всё это лежало в инфоблоке с ID=2 и отравляло жизнь: тормозило фильтры, путало менеджеров, искажало отчёты.
Имеем файл remsku.txt со списком артикулов на удаление. Внутри — одна строка, один артикул. Всего 37 216 штук. Требование: удалить товары полностью, вместе со всеми торговыми предложениями, ценами и остатками.
Новичок написал бы так:
$lines = file('remsku.txt');
foreach ($lines as $sku) { $res = CIBlockElement::GetList([], ["PROPERTY_SKU" => $sku]); while ($elem = $res->Fetch()) { CIBlockElement::Delete($elem["ID"]); }
}И получил бы белую страницу через 15 секунд. Причины:
file() читает файл в массив. На 37 тысячах строк это не страшно, но что если завтра будет 500 тысяч?CIBlockElement::Delete() — это не просто DELETE FROM. Битрикс дёргает обработчики, чистит свойства, разделы, цены, склады. На 20-30 товарах скрипт падает по max_execution_time.Наш скрипт решает эти проблемы элегантно.
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
include "/bitrix/modules/main/include/prolog_before.php";
CModule::IncludeModule("iblock");Константы отключают статистику и проверку прав — скрипт будет запускаться из браузера без авторизации. Без них Битрикс пошлёт нас с ошибкой доступа.
$filePath = "/remsku.txt"; $stepSize = 150;
150 строк за раз — цифра подобрана опытным путём. Меньше — слишком много перезагрузок, дольше процесс. Больше — скрипт не успевает до тайм-аута. Для лёгких товаров можно ставить 300, для тяжёлых — 50.
session_start(); $currentPointer = isset($_SESSION['file_pointer']) ? $_SESSION['file_pointer'] : 0;
Вот где начинается волшебство. Скрипт не глотает весь файл разом. Он запоминает в сессии номер строки, на которой остановился. Это как закладка в книге: прочитали 150 страниц, заложили, закрыли книгу, через секунду открыли снова и продолжили.
$file = new SplFileObject($filePath); $file->seek($currentPointer);
SplFileObject — стандартный класс PHP. Он итерируется по файлу построчно, не загружая его в память. seek() мгновенно переходит к нужной строке. Даже если в файле миллион строк, скрипт не упадёт.
$sku = trim($file->current());
$res = CIBlockElement::GetList( [], ["IBLOCK_ID" => 2, "PROPERTY_SKU" => $sku . "%"], false, false, ["ID"]
);
while ($elem = $res->Fetch()) { CIBlockElement::Delete($elem["ID"]); echo "Удалён: " . $sku . " (ID: " . $elem["ID"] . ")
";
}Заметьте $sku . "%" — поиск по подстроке. Артикулы в файле могут быть без пробелов, а в базе — с пробелом или дефисом. Например, в файле ABC123, а в Битриксе ABC-123. Знак процента даёт гибкость.
CIBlockElement::Delete() делает всё правильно: удаляет товар, свойства, разделы, торговые предложения, цены, складские остатки, поисковый индекс. Медленно, но корректно. Прямой SQL оставил бы кучу мусора.
$_SESSION['file_pointer'] = $file->key();
if (!$file->eof()) { echo ""; echo "Обработано " . $_SESSION['file_pointer'] . " строк. Ждите...";
} else { echo "Готово! Все товары удалены.
"; echo $_SESSION['file_pointer']; unset($_SESSION['file_pointer']);
}После пачки строк сохраняем позицию. Если файл не закончился — выводим JavaScript, который через секунду перезагружает страницу. Получается конвейер: пачка → перезагрузка → пачка → перезагрузка. В браузере это выглядит как мигающая надпись с растущим счётчиком.
Никакого AJAX, никакого WebSocket. Просто location.reload() с таймером. Решение, достойное музея простоты.
Можно было передавать позицию через URL, но тогда любой, кто подсмотрит ссылку, мог бы вмешаться. Сессия на сервере — безопаснее.
В 95% случаев file_get_contents() нормально. Но с большим файлом он съест всю память. SplFileObject элегантно обходит проблему.
Я запустил скрипт и пошёл делать бутерброд. Вернулся — на экране ошибка: «Maximum execution time of 30 seconds exceeded». Как?! Я же предусмотрел перезагрузку!
Оказалось, в файле затесалась строка-мусор — обрывок XML: <Товар><Артикул>ABC-123. Фильтр с ней ломал запрос, и GetList зацикливался внутри одной итерации, не доходя до проверки счётчика и перезагрузки.
С тех пор всегда добавляю валидацию:
if (preg_match('/^[A-Za-z0-9\-_]+$/', $sku)) { // ищем
} else { echo "ПРОПУЩЕН (некорректный артикул): {$sku}
"; continue;
}Доверяй, но проверяй.
<?php
// Блок подключения Битрикса
define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
include "/bitrix/modules/main/include/prolog_before.php";
CModule::IncludeModule("iblock");
$filePath = "skypka.com/remsku.txt";
$stepSize = 150; // Сколько строк обрабатывать за один раз (подберите опытным путем)
// Начинаем работу с сессией, чтобы помнить, где остановились
session_start();
$currentPointer = isset($_SESSION['file_pointer']) ? $_SESSION['file_pointer'] : 0;
if (file_exists($filePath)) {
$file = new SplFileObject($filePath);
$file->seek($currentPointer); // Переходим к нужной строке
$count = 0;
while (!$file->eof() && $count < $stepSize) {
$sku = trim($file->current());
if (!empty($sku)) {
// Поиск товара по артикулу (замените ARTNUMBER на ваш код свойства)
$res = CIBlockElement::GetList(
array(),
array("IBLOCK_ID" => 2, "PROPERTY_SKU" => $sku. "%"),
false,
false,
array("ID")
);
while ($elem = $res->Fetch()) {
CIBlockElement::Delete($elem["ID"]);
echo "Удален: " . $sku . " (ID: " . $elem["ID"] . ")<br>";
}
}
$file->next();
$count++;
}
// Сохраняем новую позицию в сессии
$_SESSION['file_pointer'] = $file->key();
if (!$file->eof()) {
// Если файл не закончился, перезагружаем страницу через 1 секунду
echo "<script>setTimeout(function(){ /*location.reload();*/ }, 1000);</script>";
echo "<strong>Обработано " . $_SESSION['file_pointer'] . " строк. Ждите...</strong>";
} else {
// Если всё готово
echo "<h1>Готово! Все товары удалены.</h1>";
echo $_SESSION['file_pointer'];
unset($_SESSION['file_pointer']); // Очищаем сессию
}
} else {
echo "Файл не найден.";
}
completed.flag и проверять его в начале.37 216 строк / 150 = 249 перезагрузок. Каждая итерация — около 4 секунд. Итого ~17 минут. Вручную это заняло бы недели. Скрипт отработал за ночь, пока все спали.
Прямой DELETE FROM b_iblock_element удалил бы всё за секунду. Но:
Так что CIBlockElement::Delete() — это не медлительность, а аккуратность.
В 2:17 ночи на экране загорелось: «Готово! Все товары удалены. 37216». Я отхлебнул тот самый остывший чай и поехал домой. Утром клиент написал: «Быстро ты. Я думал, это на неделю».
Знайте свои инструменты, не пытайтесь объять необъятное за один проход и всегда оставляйте закладку в книге. Особенно если эта книга — текстовый файл на 37 тысяч строк.
Код тестировался на «1С-Битрикс: Управление сайтом» (редакция «Бизнес»), PHP 7.4+. Для старых ядер может потребоваться адаптация.
Мы используем файлы cookie для улучшения вашего опыта на нашем сайте. Прочтите наше соглашение о конфиденциальности для получения дополнительной информации.