Как сформировать большой файл для маркета

На сайте интернет-магазина с большим количеством товаров (от 8000) возникает проблема с формированием XML-фида для Яндекс Маркета – товаров в БД так много, что лимитов хостинга по выделяемой памяти и времени не хватает для формирования файла.

Решение проблемы – разбить генерацию файла на несколько этапов и запаковать готовый файл в ZIP-архив.

О работе скрипта

При первом запуске скрипта cron.php создается файл market_status.json для хранения необходимых переменных, формируется «шапка» файла с данными о магазине и списком категорий, далее эти данные сохраняются в файл market.xml.

В последующих запусках скрипта из базы данных берутся по 1000 товаров и дописываются в конец файла market.xml. В последнем запуске цикла, в файл дописываются закрывающие теги и происходит архивация файла в market.zip.

<?php

// Локаль.
setlocale(LC_ALL, 'ru_RU.utf8');
date_default_timezone_set('Europe/Moscow');
header('Content-type: text/html; charset=utf-8');

// Интервал обновления файла market.zip (сек.)
$interval = 12000; // Обновление раз в 3,5 часа

// Количество товаров в одном шаге
$limit = 1000;

// Подключение к БД
$dbh = new PDO('mysql:dbname=db_name;host=localhost', 'логин', 'пароль');

// Загрузка файла market_status.json
if (is_file(__DIR__ . '/market_status.json')) {
	$status = json_decode(file_get_contents(__DIR__ . '/market_status.json'), true);
} else {
	$status = array(
		'date'  => 0,
		'total' => 0,
		'step'  => 0
	);
}

if ($status['total'] == $status['step'] && $status['date'] + $interval < time()) {
	// Шаг 0 - создаем новый файл market.xml
	$out = '<?xml version="1.0" encoding="UTF-8"?>
	<yml_catalog date="' . date('Y-m-d H:i') . '">
	<shop>
		<name>Название магазина</name>
		<company>Полное наименование компании</company>
		<url>https://example.com</url>
		<currencies>
			<currency id="RUR" rate="1"/>
		</currencies>';
 
	// Список категорий магазина:
	$sth = $dbh->prepare("SELECT `id`, `parent`, `name` FROM `category`");
	$sth->execute();
	$category = $sth->fetchAll(PDO::FETCH_ASSOC);

	$out .= '<categories>' . PHP_EOL;
	foreach ($category as $row) {
		$out .= '<category id="' . $row['id'] . '" parentId="' . $row['parent'] . '">' . $row['name'] . '</category>' . PHP_EOL;
	}
	$out .= '</categories>' . PHP_EOL;
	$out .= '<offers>' . PHP_EOL;
	
	// Сохраняем в файл
	file_put_contents(__DIR__ . '/market.xml', $out);
	
	// Получаем общее кол-во товаров
	$sth = $dbh->prepare("SELECT COUNT(`id`) FROM `prods`");
	$sth->execute();
	$total = $sth->fetch(PDO::FETCH_COLUMN);

	$status = array(
		'date'  => time(),
		'total' => ceil($total / $limit),
		'step'  => 0
	);

	// Обновляем данные в market_status.json
	file_put_contents(__DIR__ . '/market_status.json', json_encode($status));

	echo 'Шаг ' . $status['step'] . ' из ' . $status['total'];
} elseif ($status['total'] != $status['step']) {
	// Выполнение остальных шагов
	$start = $status['step'] * $limit;
	$sth = $dbh->prepare("SELECT * FROM `prods` LIMIT {$start}, {$limit}");
	$sth->execute();	
	$prods = $sth->fetchAll(PDO::FETCH_ASSOC);

	$out = '';
	foreach ($prods as $row) {
		$out .= '
		<offer id="'.$row['id'] . '" available="true">
			<name>' . $row['name'] . '</name>
			<url>https://example.com/prods/' . $row['id'] . '</url>
			<vendor>' . $row['brand'] . '</vendor>
			<price>' . $row['price'] . '</price>
			<currencyId>RUR</currencyId>
			<categoryId>' . $row['category'] . '</categoryId>
			<picture>https://example.com/uploads/' . $row['image'] . '</picture>
			<description><![CDATA[' . $row['text'] . ']]></description>
		</offer>';	
	}

	// Обновляем данные в market_status.json
	$status['step']++;
	file_put_contents(__DIR__ . '/market_status.json', json_encode($status));

	// Если последний шаг, то нужно закрыть теги
	if ($status['total'] == $status['step']) {
		$out .= '</offers></shop></yml_catalog>';
	}
	
	// Сохраняем данные в market.xml
	$fp = fopen(__DIR__ . '/market.xml', 'a');  
	fwrite($fp, PHP_EOL . $out);  
	fclose($fp);

	// Если последний шаг, то ZIP-архивация
	if ($status['total'] == $status['step']) {
		$zip = new ZipArchive();
		$zip->open(__DIR__ . '/market.zip', ZipArchive::CREATE|ZipArchive::OVERWRITE);
		$zip->addFile(__DIR__ . '/market.xml', 'market.xml');
		$zip->close();
	}

	echo 'Шаг ' . $status['step'] . ' из ' . $status['total'];
} else {
	// Файл недавно сформирован, пока отдыхаем
	echo 'Файл сформирован, следующее обновление в ' . date('d.m.Y H:s', $status['date'] + $interval);
}
PHP

Данный скрип должен вызываться поочередно, например по крону c интервалом раз в 15 минут:

Минута  Час  День  Месяц  День недели  Команда
*/15    *    *     *      *            /usr/local/bin/wget -O - -q "https://example.com/cron.php"

Пример работы скрипта

Первый запуск скрипта cron.php
Первый запуск скрипта cron.php
Последующие запуски скрипта
Последующие запуски скрипта
Завершение процесса, файл market.zip сформирован
Завершение процесса, файл market.zip сформирован
14.01.2021
Предыдущая запись Работа с cookie в JavaScript
Следующая запись Работа с MIME-типами в PHP

Комментарии

, чтобы добавить комментарий.

Другие публикации

Класс ZipArchive позволяет быстро и удобно работать с ZIP-архивам, рассмотрим основные возможности класса.
5680
0
В статье приведены основные примеры работы с расширением PHP PDO. Такие как подключение к БД, получение, изменение и...
42700
0
Класс значительно упрощает работу с PDO, сокращает код. Реализован на статических классах и не требует создание экземпляра класса.
10915
+6
Изображения нужно сжимать для ускорения скорости загрузки сайта, но как это сделать? На многих хостингах нет...
9393
+4
В статье приведен пример формы и php-скрипта для безопасной загрузки файлов на сервер, возможные ошибки и рекомендации при работе с данной темой.
30014
+9
Так как Instagram и Fasebook ограничили доступ к API, а фото с открытого аккаунта всё же нужно периодически получать и...
11849
+6