Для начала представлюсь. Меня зовут Евгений, я работаю в компании OpticsTrade, должность IT-разнорабочий. Компания занимается продажей оптических приборов с 1992 года, а последние несколько лет делает упор на ночную оптику и в частности - тепловизионные прицелы и приборы. По мере роста бизнеса и расширения ассортимента, компания столкнулась с проблемой остатков товаров и актуальными ценами. Если в начале моей работы, количество товара на сайте было в районе 3 тысяч, то на текущий момент позиций более 15 тысяч. Обновлять руками такое количество позиций нереально.

Об о всем по порядку

С чем предстоит работать:

  • Сайт на OpenCart;

  • Расширение АОП (Автоматическая обработка прайс-листов);

  • Поставщики - более 20;

  • Товары - более 15 тысяч.

Начальство любит экспериментировать и проверять гипотезы, но не всегда задумывается о последствиях, связанными с этими экспериментами.

Во время COVID-19, спрос на дорогие товары для охоты сильно упал, компания приняла решение расширить ассортимент и выйти на рынок туризма. Основная причина этого шага - закрытые границы. Отдел маркетинга и анализ рынка предсказывал увеличение внутреннего туризма и спроса на соответствующие товары.

После того, как выбрали новых поставщиков и договорились с ними о сотрудничестве, на мне повисла задача наполнить сайт огромным количеством новых позиций. 

Обсудив с коллегами, принял решение спарсить товары с сайтов поставщиков/конкурентов. В этом мне помог АОП, универсальное решение для работы с товарами и ценами в OpenCart.

При поиске донора для парсинга есть одно важное условие - унифицированные характеристики.

Многие конкуренты и поставщики не обращают на это внимание, но для правильной работы фильтров и сравнения товаров, нужно чтобы название характеристики и ее значение было в едином стиле.

Пример:

Увеличение

Увеличение, х

1

Это две разные характеристики и два разных значения. Соответственно будут дубли, а это чревато тем, что какое-то количество товара при выборе первого фильтра пользователь не увидит.

Пример ошибки дублей характеристик на сайте.
Пример ошибки дублей характеристик на сайте.

У основных поставщиков были проблемы с характеристиками, у конкурентов внутренние артикулы отличаются от прайсов поставщиков. При парсинге товаров, мой выбор пал на конкурентов и это стало огромной ошибкой в последующем.

В моменте никто и не думал о том, каким образом будут обновляться цены. 

Но когда количество товара переросло отметку в 15 тысяч, компания столкнулась с:

  1. Нестабильный курс

  2. Санкции и новые пути поставок

  3. РРЦ

Цены менялись ежедневно. Совместно с отделами продаж и контента, провели мозговой штурм, о том, как быстро свести тысячи артикулов из прайсов поставщиков и изменять цены в автоматическом режиме. Назвали нашу идею - ЦУЦ (Центр управления ценами).

"Всякая экономия в конечном счете сводится к экономии времени" - Карл Маркс.
"Всякая экономия в конечном счете сводится к экономии времени" - Карл Маркс.

ЦУЦ 1.0

Для того, чтобы быстро свести артикулы и обновлять цены нам понадобится:

  • АОП

  • KeyCollector

  • Excel

Делаем выгрузку всех товаров с помощью АОП.

Выгрузка товаров в формате .xml
Выгрузка товаров в формате .xml

Добавляем название или H1 товара в KeyCollector и собираем данные из нужной поисковой системы. В нашем случае это Яндекс.

Мы на 5 месте.
Мы на 5 месте.

Экспортируем данные ПС, получаем таблицу:

А здесь на 4 месте.
А здесь на 4 месте.

Сталкиваюсь с проблемой нерелевантных страниц. Товары отличающиеся каким-то свойством, но имеющие одинаковое описание, характеристики. ПС может отдавать приоритет одной карточке товара. В итоге мы имеем то, что на два разных запроса получаем одну ссылку. Такие случаи приходилось проверять руками.

80% ссылок после анализа ПС ссылалось на верный товар.

Создал таблицу, где отсортировал товары по сайтам конкурентов и поставщиков, добавил внутренние артикулы, распределил товар по брендам и принялся настраивать АОП.

Артикул

Название товара

Бренд

Домен поставщика

Ссылка конкурента 1

Ссылка конкурента 2

Настройка АОП для обновления цен.
Настройка АОП для обновления цен.

Запускаем, цены обновились. Ура! Воскликнули все с облегчением. Но, на самом деле, это было только начало.

В данном способе обновления цен были нюансы. Нужно каждый раз проверять файл с ошибками из-за того, что конкуренты не стоят на месте. Многие вносят правки на свои сайты, часто после редактирования ломаются параметры парсинга. Некоторые конкуренты используют js скрипты в своем коде, привязаться к данным становится очень сложно.

Так же, метод обновления через сайты конкурентов/поставщиков, не дал нормально свести артикулы.

В компании пользовались этим способом на протяжении года, пока не решили выйти на рынок маркетплейсов.


ЦУЦ 2.0

Работа с МП подразумевала под собой наличие точной информации по количеству остатков и актуальных цен у поставщиков. Иначе есть высокий риск продать по цене ниже рынка или не доставить заказанный товар из-за его отсутствия.

Причина этого формат работы: МП - Наша компания - Поставщик

Нужно знать точное количество остатков на складе поставщика. Такую информацию поставщики предоставляют только в прайс листах. Но вот незадача, каждый поставщик имеет индивидуальный формат таблицы.

Прайс лист поставщика 1
Прайс лист поставщика 1
Прайс лист поставщика 2
Прайс лист поставщика 2

Использовав PHP SimpleXML и SimpleXLSX создаю скрипт, который форматирует прайсы в единый стиль таблицы.

<?php
$curl = curl_init();
$url = 'https://site.com/price.xml';
$fp = fopen("price.xml", "w");
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_FILE, $fp);
curl_setopt($curl, CURLOPT_USERAGENT, "Opera/10.00 (Windows NT 5.1; U; ru) Presto/2.2.0");
$result = curl_exec($curl);
curl_close($curl);
fclose($fp);
error_reporting(E_ALL);
ini_set('display_errors', 'on');
$previous_use_error = libxml_use_internal_errors(true);
//LOADXML
$xml = simplexml_load_file('price.xml');
ob_start();
echo ('<?xml version="1.0" encoding="UTF-8"?>');
echo ('<price>');
foreach ($xml->offers->offer as $offer) {
    $offer->name = preg_replace('/</', '&lt;', $offer->name);
    $offer->name = preg_replace('/&/', '&amp;', $offer->name);
    $offer->name = preg_replace('/ /', ' ', $offer->name);
    $offer->stock_msk = preg_replace('/\+\d*/', '', $offer->stock_msk);
    $offer->stock_msk = (int)$offer->stock_msk + (int) $offer->stock_msk_shops;
    echo '<offer>
    <sku>' . $offer->code . '</sku>
    <name>' . $offer->name . '</name>
    <stock>' . $offer->stock_msk . '</stock>
    <price>' . $offer->retail_price . '</price>
    </offer>';
}
echo ('</price>');
file_put_contents('price_new.xml', ob_get_contents());
?>

Получаю таблицу формата xml:

Артикул

Название

Остаток

Цена

0001

Товар1

0

99

0002

Товар2

99

199

Добавляю товары на сайт прокладку с помощью АОП.

Товары с актуальным ценами и остатками.
Товары с актуальным ценами и остатками.

Встроенный функционал АОП позволяет создать CRON для автоматического обновления остатков и цен. Настраиваю CRON для обновления прайсов поставщиков.

export2com формирует один файл со всеми товарами.
export2com формирует один файл со всеми товарами.

Так же настраиваю CRON АОП на основном сайте и сверяю артикулы с прайсами поставщиков.

Работает, ура!
Работает, ура!

Конечно прайсы поставщиков не идеальны, существует проблема вывода остатков в формате 0 и 1, в таких случаях все равно приходится запрашивать информацию.

В будущем планируется версия 3.0 - гибрид 1 и 2 версии ЦУЦа.

Я долго шел к настройке ЦУЦ, чтобы автоматизировать магазин, в итоге получил то, что хотел. И самое главное, что отдел продаж остался доволен :) Всем спасибо за внимание!

Комментарии (8)


  1. Phil_itch
    00.00.0000 00:00
    +2

    Статье лайк, но хорошим тоном считается давать расшифровки всех встречающихся аббревиатур, я про РРЦ: присутствует на КДПВ (Картинка Для Привлечения Внимания =)) ) и в тексте. Не все пользователи Хабра погружены в продажную тему...


    1. udts Автор
      00.00.0000 00:00
      +2

      Спасибо, учту в следующий раз!


  1. FanatPHP
    00.00.0000 00:00
    +2

    Я тоже поставил лайк, и в целом понимаю, что эти придирки не совсем в тему, поскольку статья в основном про другое. Но все же она размещена в хабе РНР, а от единственного фрагмента кода на этом языке просто кровь из глаз.
    Опять же, я понимаю, что вины автора нет, все мы ищем решение в интернете, и находим не правильное, а то, которое гуголь счел самым авторитетным сиречь как можно более древним, в идеале — из прошлого века. Но просто на будущее:


    1. Самым примечательным, конечно, является блок preg_replace.
      • регулярки для простых строковых замен — это чересчур. Тем более аж две на одну и ту же строку. Не говоря уже о том, что замены HTML сущностей в РНР есть специальные функции, которые делают все сами.
      • смысл операции $offer->name = preg_replace('/ /', ' ', $offer->name); от меня ускользает :) Наверное, имелось в виду $offer->name = preg_replace('/ +/', ' ', $offer->name);?
      • preg_replace('/\+\d*/', '', $offer->stock_msk) тоже выглядит довольно странно. Оно действительно делает именно то, что задумано?
    2. Странное отношение к обработке ошибок. Общие ошибки РНР включаем не почему-то не сразу, а в середине кода. Курл на ошибки вообще не проверяем, как и simplexml, заодно отключив ему возможность сообщать об ошибках самостоятельно. В итоге непонятно, хотим мы видеть ошибки, или нет? По идее, программист, который отключает генерацию ошибок, стреляет себе сразу в обе ноги.
    3. Курл для простого запроса использовать нет смысла, практически любая функция в РНР умеет работать с НТТР напрямую. Весь этот многострочник заменяется на copy('https://site.com/price.xml', "price.xml");. Подделывать юзер агент я не вижу смысла. Даже наоборот это будет выглядеть подозрительно — XML браузерами не скачивают.
    4. В РНР есть такая крутая вещь, как двойные кавычки :) (не говоря уже про heredoc, который здесь подойдет даже лучше, но не будем перегружать)
    5. Совсем уж мелочь, но использовать буферизацию вывода для собирания строки как-то странно.

    Как минимум, я бы переписал так


    <?php
    error_reporting(E_ALL);
    ini_set('display_errors', 'on');
    
    $url = 'https://site.com/price.xml';
    $xml = simplexml_load_file($url);
    
    $out = '<?xml version="1.0" encoding="UTF-8"?>';
    $out .= "\n<price>";
    foreach ($xml->offers->offer as $offer) {
        $offer->name = htmlspecialchars($offer->name);
        $offer->stock_msk = preg_replace('/\+\d*/', '0', $offer->stock_msk);
        $offer->stock_msk = (int)$offer->stock_msk + (int) $offer->stock_msk_shops;
        $out .= "
    <offer>
        <sku>$offer->code</sku>
        <name>$offer->name</name>
        <stock>$offer->stock_msk</stock>
        <price>$offer->retail_price</price>
    </offer>";
    }
    $out .= "\n</price>\n";
    file_put_contents('price_new.xml', $out);

    хотя по-хорошему, генерацию XML стоит делать не вручную, а тем же smilexml.


    1. udts Автор
      00.00.0000 00:00
      +2

      Прошу прощения за кровь из глаз! Безумно благодарен за пояснения и разбор кода. Почерпнул для себя новую информацию! Буду стараться сильнее вникать в PHP.


  1. FulgerX2007
    00.00.0000 00:00
    +1

    ???? думаю все кто работает с сайтами по продажам встречали такие задачи.

    P.S. вспоминается молодость


  1. kolobo4ek10
    00.00.0000 00:00

    А зачем мучить Яндекс для определения релевантности страницы? Разве нет более оптимального решения?


    1. udts Автор
      00.00.0000 00:00

      Для быстрого сведения товаров с прайсами поставщиков. При первоначальной работе с товарами не было нормальной связки с прайсами поставщиков. Артикулы не совпадали. Можно было сделать связку по названию товара, но они изменялись для поисковой оптимизации. Так что просто ВПР и Эксель тут не подходит :)


  1. TheTrueRikkiTikki
    00.00.0000 00:00

    Не ну круто, а как теперь к этой всей суете прицепить анализ наличия складских остатков?