Выгрузка больших объемов данных на маркетплейсы
В примере выгружаем около 115 тысяч товаров на rozetka.ua
В этой статье я рассказывал предысторию обращения владельца интернет магазина в большим количеством товара. Ему потребовалось отдавать на маркетплейс розетка около 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МБ оперативной памяти. Выгрузка получилась отличная. Теперь осталось только настроить генерацию по крону раз в сутки ночью и будет отлично!