Реализация автоматического создания карточек товаров из спарсенных данных

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

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация автоматического создания карточек товаров из спарсенных данных
Средняя
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация автоматического создания карточек товаров из спарсенных данных

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

Общая схема пайплайна

Raw Scraped Data (JSON)
    ↓
DataValidator          – проверка обязательных полей
    ↓
DataNormalizer         – нормализация полей, единиц, форматов
    ↓
CategoryMatcher        – определение категории по данным
    ↓
DuplicateChecker       – проверка, не существует ли уже такой товар
    ↓
VariantBuilder         – сборка вариативных товаров из размеров/цветов
    ↓
ImageProcessor         – загрузка и обработка фотографий
    ↓
ProductCreator         – запись в БД через репозиторий платформы
    ↓
SEOGenerator           – заполнение мета-тегов, slug
    ↓
PublicationDecider     – draft / published в зависимости от настроек

Валидация входных данных

// app/Services/ProductImport/DataValidator.php
use Illuminate\Support\Facades\Validator;

class DataValidator
{
    private array $rules = [
        'sku'         => 'required|string|max:100',
        'name'        => 'required|string|max:500',
        'price'       => 'required|numeric|min:0.01|max:9999999',
        'description' => 'nullable|string',
        'images'      => 'nullable|array',
        'images.*'    => 'nullable|url',
        'specs'       => 'nullable|array',
        'in_stock'    => 'nullable|boolean',
    ];

    public function validate(array $data): ValidationResult
    {
        $validator = Validator::make($data, $this->rules);

        if ($validator->fails()) {
            return ValidationResult::fail($validator->errors()->toArray());
        }

        // Дополнительные бизнес-правила
        if (isset($data['price']) && $data['price'] < config('import.min_price', 1)) {
            return ValidationResult::fail(['price' => ['Цена подозрительно мала']]);
        }

        return ValidationResult::pass($validator->validated());
    }
}

Матчинг категорий

// app/Services/ProductImport/CategoryMatcher.php
class CategoryMatcher
{
    public function match(array $productData): ?int
    {
        // Стратегия 1: по маппингу поставщик → категория
        if ($supplierId = $productData['supplier_id'] ?? null) {
            $mapping = SupplierCategoryMapping::where('supplier_id', $supplierId)
                ->where('supplier_category', $productData['category'] ?? '')
                ->first();

            if ($mapping) return $mapping->local_category_id;
        }

        // Стратегия 2: нечёткий поиск по названию категории
        if ($categoryName = $productData['category'] ?? null) {
            $category = Category::where('name', 'ilike', "%{$categoryName}%")->first();
            if ($category) return $category->id;
        }

        // Стратегия 3: по ключевым словам в названии товара
        return $this->matchByKeywords($productData['name']);
    }

    private function matchByKeywords(string $name): ?int
    {
        $rules = CategoryKeywordRule::orderBy('priority', 'desc')->get();

        foreach ($rules as $rule) {
            foreach ($rule->keywords as $keyword) {
                if (mb_stripos($name, $keyword) !== false) {
                    return $rule->category_id;
                }
            }
        }

        return config('import.default_category_id');
    }
}

Детектор дублей

// app/Services/ProductImport/DuplicateChecker.php
class DuplicateChecker
{
    public function findExisting(array $data): ?Product
    {
        // Точное совпадение по SKU поставщика
        if ($supplierId = $data['supplier_id'] ?? null) {
            $existing = Product::whereHas('supplierMappings', function ($q) use ($data, $supplierId) {
                $q->where('supplier_id', $supplierId)
                  ->where('supplier_sku', $data['sku']);
            })->first();

            if ($existing) return $existing;
        }

        // Совпадение по EAN/GTIN
        if ($gtin = $data['gtin'] ?? null) {
            $existing = Product::where('gtin', $gtin)->first();
            if ($existing) return $existing;
        }

        // Нечёткое совпадение по названию + бренду (порог 90%)
        if (($name = $data['name'] ?? null) && ($brand = $data['brand'] ?? null)) {
            $candidates = Product::where('brand', $brand)
                ->get(['id', 'name']);

            foreach ($candidates as $candidate) {
                similar_text(
                    mb_strtolower($name),
                    mb_strtolower($candidate->name),
                    $pct
                );
                if ($pct >= 90) return $candidate;
            }
        }

        return null;
    }
}

Построитель вариативных товаров

// app/Services/ProductImport/VariantBuilder.php
class VariantBuilder
{
    /**
     * Группирует плоский список товаров в configurable products с вариантами
     *
     * Пример: 3 строки "Футболка Nike S/красный", "S/синий", "M/красный"
     * → 1 configurable product + 3 simple variants
     */
    public function buildVariants(array $products): array
    {
        // Группируем по базовому названию (без размера/цвета)
        $groups = [];

        foreach ($products as $product) {
            $baseKey = $this->extractBaseKey($product);
            $groups[$baseKey][] = $product;
        }

        $result = [];
        foreach ($groups as $baseKey => $variants) {
            if (count($variants) === 1) {
                $result[] = ['type' => 'simple', 'data' => $variants[0]];
            } else {
                $result[] = [
                    'type'     => 'configurable',
                    'base'     => $this->buildBase($variants),
                    'variants' => $variants,
                ];
            }
        }

        return $result;
    }

    private function extractBaseKey(array $product): string
    {
        // Убираем из имени паттерны размеров и цветов
        $name = preg_replace('/\b(xs|s|m|l|xl|xxl|\d+\s*(см|мм|дюйм))/iu', '', $product['name']);
        $name = preg_replace('/\b(красный|синий|чёрный|белый|зелёный|red|blue|black|white)/iu', '', $name);

        // Группируем по SKU-префиксу, если доступен
        if (preg_match('/^([A-Z0-9]+)-/i', $product['sku'], $m)) {
            return strtoupper($m[1]);
        }

        return trim($product['brand'] ?? '') . '|' . trim($name);
    }
}

Основной сервис создания карточки

// app/Services/ProductImport/ProductCreator.php
class ProductCreator
{
    public function create(array $data, string $type = 'simple'): Product
    {
        $slug = $this->generateUniqueSlug($data['name']);

        $product = Product::create([
            'type'        => $type,
            'sku'         => $data['sku'],
            'name'        => $data['name'],
            'slug'        => $slug,
            'description' => $data['description'] ?? '',
            'price'       => $data['price'],
            'brand'       => $data['brand'] ?? null,
            'gtin'        => $data['gtin'] ?? null,
            'category_id' => $data['category_id'],
            'status'      => $data['auto_publish'] ? 'active' : 'draft',
            'meta_title'  => $this->generateMetaTitle($data),
            'meta_description' => $this->generateMetaDesc($data),
        ]);

        // Привязка к поставщику
        if ($supplierId = $data['supplier_id'] ?? null) {
            $product->supplierMappings()->create([
                'supplier_id'  => $supplierId,
                'supplier_sku' => $data['supplier_sku'] ?? $data['sku'],
                'supplier_url' => $data['source_url'] ?? null,
            ]);
        }

        // Характеристики
        if (!empty($data['specs'])) {
            foreach ($data['specs'] as $key => $value) {
                $attribute = ProductAttribute::firstOrCreate(['code' => $this->slug($key)], ['name' => $key]);
                $product->attributeValues()->create([
                    'attribute_id' => $attribute->id,
                    'value'        => $value,
                ]);
            }
        }

        // Изображения (через Job)
        if (!empty($data['images'])) {
            DownloadAndAttachProductImages::dispatch($product->id, $data['images'])
                ->onQueue('image-processing');
        }

        return $product;
    }

    private function generateUniqueSlug(string $name): string
    {
        $base = \Str::slug($name);
        $slug = $base;
        $i = 1;

        while (Product::where('slug', $slug)->exists()) {
            $slug = "{$base}-{$i}";
            $i++;
        }

        return $slug;
    }

    private function generateMetaTitle(array $data): string
    {
        $title = $data['name'];
        if ($brand = $data['brand'] ?? null) {
            $title = "{$brand} {$title}";
        }
        return mb_substr($title, 0, 70);
    }
}

Orchestrating Job

// app/Jobs/CreateProductFromScrapedData.php
class CreateProductFromScrapedData implements ShouldQueue
{
    public int $tries = 3;

    public function handle(
        DataValidator $validator,
        CategoryMatcher $matcher,
        DuplicateChecker $checker,
        ProductCreator $creator
    ): void {
        // Валидация
        $result = $validator->validate($this->rawData);
        if (!$result->passes()) {
            ImportLog::create([
                'source'  => $this->source,
                'sku'     => $this->rawData['sku'] ?? 'unknown',
                'status'  => 'validation_failed',
                'errors'  => $result->errors(),
            ]);
            return;
        }

        $data = $result->data();

        // Проверка дублей
        if ($existing = $checker->findExisting($data)) {
            // Обновляем существующий товар, не создаём дубль
            $existing->update(['price' => $data['price'], 'in_stock' => $data['in_stock']]);
            ImportLog::create(['status' => 'updated', 'product_id' => $existing->id]);
            return;
        }

        // Матчинг категории
        $data['category_id'] = $matcher->match($data);

        // Создание
        $product = $creator->create($data);

        ImportLog::create([
            'status'     => 'created',
            'product_id' => $product->id,
            'source'     => $this->source,
        ]);
    }
}

Срок разработки

Компонент Срок
Валидатор + нормализатор 1-2 дня
Матчинг категорий 1-2 дня
Детектор дублей 1 день
Построитель вариантов 2-3 дня
Создатель карточек + SEO 1-2 дня
Логирование + дашборд импорта 1-2 дня
Итого 7-12 рабочих дней