loader
Заказать услуги

Скрипт чистки товаров с файла по артикулам «1С-Битрикс»

Example blog post alt

Пятница, 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 секунд. Причины:

  1. Память. file() читает файл в массив. На 37 тысячах строк это не страшно, но что если завтра будет 500 тысяч?
  2. Время выполнения. Каждый CIBlockElement::Delete() — это не просто DELETE FROM. Битрикс дёргает обработчики, чистит свойства, разделы, цены, склады. На 20-30 товарах скрипт падает по max_execution_time.
  3. Отсутствие прогресса. Если скрипт упадёт на середине, вы не узнаете, где.

Наш скрипт решает эти проблемы элегантно.

Как это работает: построчный разбор

Шаг 1. Подключаемся правильно

 define('NO_KEEP_STATISTIC', true);
define('NOT_CHECK_PERMISSIONS', true);
include "/bitrix/modules/main/include/prolog_before.php";
CModule::IncludeModule("iblock");

Константы отключают статистику и проверку прав — скрипт будет запускаться из браузера без авторизации. Без них Битрикс пошлёт нас с ошибкой доступа.

Шаг 2. Параметры

 $filePath = "/remsku.txt";
$stepSize = 150;

150 строк за раз — цифра подобрана опытным путём. Меньше — слишком много перезагрузок, дольше процесс. Больше — скрипт не успевает до тайм-аута. Для лёгких товаров можно ставить 300, для тяжёлых — 50.

Шаг 3. Магия сессий

 session_start();
$currentPointer = isset($_SESSION['file_pointer']) ? $_SESSION['file_pointer'] : 0;

Вот где начинается волшебство. Скрипт не глотает весь файл разом. Он запоминает в сессии номер строки, на которой остановился. Это как закладка в книге: прочитали 150 страниц, заложили, закрыли книгу, через секунду открыли снова и продолжили.

Шаг 4. Чтение без загрузки в память

 $file = new SplFileObject($filePath);
$file->seek($currentPointer);

SplFileObject — стандартный класс PHP. Он итерируется по файлу построчно, не загружая его в память. seek() мгновенно переходит к нужной строке. Даже если в файле миллион строк, скрипт не упадёт.

Шаг 5. Поиск и удаление

 $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 оставил бы кучу мусора.

Шаг 6. Сохранение позиции и перезагрузка

 $_SESSION['file_pointer'] = $file->key();
if (!$file->eof()) { echo ""; echo "Обработано " . $_SESSION['file_pointer'] . " строк. Ждите...";
} else { echo "

Готово! Все товары удалены.

"; echo $_SESSION['file_pointer']; unset($_SESSION['file_pointer']); }

После пачки строк сохраняем позицию. Если файл не закончился — выводим JavaScript, который через секунду перезагружает страницу. Получается конвейер: пачка → перезагрузка → пачка → перезагрузка. В браузере это выглядит как мигающая надпись с растущим счётчиком.

Три инженерные хитрости

Хитрость №1: самодельный прогресс-бар

Никакого AJAX, никакого WebSocket. Просто location.reload() с таймером. Решение, достойное музея простоты.

Хитрость №2: сессия вместо GET-параметра

Можно было передавать позицию через URL, но тогда любой, кто подсмотрит ссылку, мог бы вмешаться. Сессия на сервере — безопаснее.

Хитрость №3: SplFileObject вместо file_get_contents

В 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 "Файл не найден.";

}

Что можно улучшить

  • Логирование. Писать удалённые позиции в файл или отправлять отчёт в Telegram, Макс, E-Mail.
  • Защита от повторного запуска. Создавать файл-флаг completed.flag и проверять его в начале.
  • Режим «сухого прогона». Сначала вывести список того, что будет удалено, без реального удаления.
  • Прогресс в процентах. Посчитать общее число строк в начале и выводить процент выполнения.

Производительность

37 216 строк / 150 = 249 перезагрузок. Каждая итерация — около 4 секунд. Итого ~17 минут. Вручную это заняло бы недели. Скрипт отработал за ночь, пока все спали.

Почему именно API, а не SQL

Прямой DELETE FROM b_iblock_element удалил бы всё за секунду. Но:

  • Товар в Битриксе — это множество таблиц: свойства, разделы, ТП, цены, склады. SQL оставит мусор.
  • На удаление завязаны события: синхронизация с 1С, маркетплейсы, учётные системы. Они должны узнать об удалении.
  • Поисковый индекс очищается только через API.

Так что CIBlockElement::Delete() — это не медлительность, а аккуратность.

Послесловие

В 2:17 ночи на экране загорелось: «Готово! Все товары удалены. 37216». Я отхлебнул тот самый остывший чай и поехал домой. Утром клиент написал: «Быстро ты. Я думал, это на неделю».

Знайте свои инструменты, не пытайтесь объять необъятное за один проход и всегда оставляйте закладку в книге. Особенно если эта книга — текстовый файл на 37 тысяч строк.

Код тестировался на «1С-Битрикс: Управление сайтом» (редакция «Бизнес»), PHP 7.4+. Для старых ядер может потребоваться адаптация.

0 Комментариев
Что-бы оставить свой комментарий Вы должны авторизоваться на сайте
x
Тема пуша
Сообщение пуша
Наверх
Отправить заявку
Нажимая на кнопку, вы даете согласие на обработку своих персональных данных