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

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, 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);
    }
}

Оркестрування завдання

// 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 робочих днів