Выгрузка больших объемов данных на маркетплейсы

В примере выгружаем около 115 тысяч товаров на rozetka.ua

ноябрь 16 , 2018

В этой статье я рассказывал предысторию обращения владельца интернет магазина в большим количеством товара. Ему потребовалось отдавать на маркетплейс розетка около 120 000 товаров. Конечно, стандартные выгрузки падали и тушили магазин.

Замеры работы выгрузки

Пришлось делать кастомное решение что бы выгрузить все товары в xml. Некоторые замеры в хоте работы скрипта:

Количество товара Время генерации (сек) Оперативная память (МБ) Объем xml файла (МБ)
10 000 22 203.14 94.69
30 000 70 234.85 227.90
60 000 132 282.71 444.76
90 000 195 330.62 708.32
115 457 (весь) 260 370.00 854.68

Проанализировав таблицу видим что для генерации 30к товаров надо 60сек и 50МБ оперативной памяти. Исходя из этого можно смело заявить что для такого лимита оперативной памяти выгрузки "хватит" на более чем 350 000 товаров что порадует в перспективе владельца магазина.

Как и что было сделано

Для начала переписана выборка всех данных из базы.

$sql = "SELECT
  p.product_id AS id,
  p.model AS md,
  p.quantity AS qt,
  p.image AS im,
  m.name AS mn,
  p2c.category_id AS cid,
  IFNULL(ps.price, p.price) AS pr
  FROM " . DB_PREFIX . "product p
  JOIN " . DB_PREFIX . "product_to_category AS p2c ON (p.product_id = p2c.product_id) " . ($vendor_required ? '' : 'LEFT ') .
  "JOIN " . DB_PREFIX . "manufacturer m ON (p.manufacturer_id = m.manufacturer_id)
  LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id)
  LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id)
  LEFT JOIN " . DB_PREFIX . "product_special ps ON (p.product_id = ps.product_id)
  AND ps.customer_group_id = '" . (int)$this->config->get('config_customer_group_id') . "'
  AND ps.date_start < NOW() AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())
  WHERE
  p2c.category_id IN (" . $this->db->escape($allowed_categories) . ")
  AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'
  AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "'
  AND p.date_available <= NOW()
  AND p.status = '1' ";
  if($zero_stock_view == 1){
    $sql .= "AND p.quantity >= 0 AND p.stock_status_id IN(".$out_of_stock_id.") ";
  } else {
    $sql .= "AND p.quantity > 0 ";
  }
  $sql .=" GROUP BY p.product_id LIMIT " . $limit; // LIMIT 0,60000
$query = $this->db->query($sql);

Далее циклом обходим товар и делаем выборку описания и имени

$product_info = $this->db->query("SELECT name, description FROM " . DB_PREFIX . "product_description WHERE product_id = '" . $product['id'] . "'");

Сделано это намеренно т.к. в случае выборки в первом запросе оперативной памяти не хватило бы на все описания товара. Очень много его и большие описания из-за чего переполняется оперативная память и сервер падает. Делая выборку по каждому товару мы просто экономим лимитированные ресурсы.

Что бы не дергать каждый раз фото, было решено одним запросов вначале забирать все фото в массив: (весит он около 80МБ)

$all_image = array();
$query_images = $this->db->query("SELECT product_id, image FROM " . DB_PREFIX . "product_image WHERE image != 'no_image.jpg'");
foreach($query_images->rows as $row){
  $all_image[$row['product_id']][] = $row['image'];
}

и из массива потом брать без каких либо запросов:

$pictures = isset($all_image[(int)$product['product_id']])?$all_image[(int)$product['product_id']]:array();

Таким образом забирая под фото немного памяти, мы экономим много времени.

Атрибуты и другие данные забираются стандартными методами в Opencart - это нет необходимости оптимизировать.

По сути логика такая: где-то экономим время выполнения сразу загоняя данные в оперативную память, где-то экономим память забирая для каждого товара нужные данные из базы. Здесь важно соблюдать баланс.

После чего мы все это пишем в файл xml. Но пишем сначала верхние данные:

$file = str_replace("image/", "xml/", DIR_IMAGE) . 'rozetka.xml';
$handle = fopen($file, 'a');
fwrite($handle, iconv("CP1251","UTF-8", $yml));

потом в цикле перебирая каждый товар записываем и его в файл

$yml = $this->setOffer($data);
fwrite($handle, iconv("CP1251","UTF-8", $yml));

и в конце завершаем запись прописывая закрывающие теги в файл и сам файл

$yml = '</offers>' . $this->eol;
$yml .= '</shop>' . $this->eol;
$yml .= '</yml_catalog>';
fwrite($handle, iconv("CP1251","UTF-8", $yml));
fclose($handle);

Выводы

Таким вот способом мы обошли все ограничения на сервере и создали файл. Время работы скрипта на сервере достаточно долгое, но и данных очень много, тем более код разрабатывался конкретно под данные ограничения что бы войти в лимит 768МБ оперативной памяти. Выгрузка получилась отличная. Теперь осталось только настроить генерацию по крону раз в сутки ночью и будет отлично!